diff --git a/.beads/export-state.json b/.beads/export-state.json index 4212b19c..cd359e22 100644 --- a/.beads/export-state.json +++ b/.beads/export-state.json @@ -1 +1 @@ -{"last_dolt_commit":"gti423bd3bhcledmn7g16da4sho428d8","timestamp":"2026-04-23T20:46:24.660646-07:00","issues":51,"memories":0} \ No newline at end of file +{"last_dolt_commit":"seovg8a6i7ompl8m58c6ckvt5grfi0rl","timestamp":"2026-04-24T21:08:54.011323-07:00","issues":52,"memories":0} \ No newline at end of file diff --git a/.beads/issues.jsonl b/.beads/issues.jsonl index 0e47bfc9..04c09f5b 100644 --- a/.beads/issues.jsonl +++ b/.beads/issues.jsonl @@ -12,19 +12,20 @@ {"id":"stackpanel-os2.1","title":"Add Nix deployment output validation to flake checks","description":"The repo already exposes deployment-oriented flake outputs through nix/stackpanel/lib/deploy.nix and nix/flake/global-outputs.nix, but I could not find matching check coverage that forces broken nixosConfigurations / colmena wiring to fail before someone runs a real deploy. Add validation so deploy regressions are caught during normal flake checks and CI.","design":"Keep the deploy output path pure and reviewable; add checks in the flake/check layer rather than baking deploy-time behavior into runtime commands.","acceptance_criteria":"- nix flake check --impure validates the generated nixosConfigurations for configured machines (or an explicitly documented equivalent check)\n- Broken deploy module wiring fails before runtime deployment commands are attempted\n- Coverage includes unprovisioned-machine stubs vs provisioned hardware/disk layouts where relevant\n- Any added checks are documented near the flake output wiring","status":"closed","priority":1,"issue_type":"task","owner":"me@cooperm.com","created_at":"2026-03-28T15:02:34Z","created_by":"Cooper Maruyama","updated_at":"2026-03-28T20:19:29Z","closed_at":"2026-03-28T20:19:29Z","close_reason":"Superseded by pluggable-deploy-backends restructure. Work absorbed into new phase-based tasks. See openspec/changes/pluggable-deploy-backends/","external_ref":"https://linear.app/darkmatterlabs/issue/ENG-378","labels":["deployment"],"dependencies":[{"issue_id":"stackpanel-os2.1","depends_on_id":"stackpanel-os2","type":"parent-child","created_at":"2026-03-28T08:02:33Z","created_by":"Cooper Maruyama","metadata":"{}"}],"dependency_count":0,"dependent_count":2,"comment_count":0} {"id":"stackpanel-os2.2","title":"Finish structured deploy CLI flags and status output","description":"apps/stackpanel-go/cmd/cli/deploy.go currently supports basic app deploys with --dry-run plus human-readable status, but the design doc calls for env overrides, machine-readable output, and richer status metadata. Finish the command surface so deployment can run cleanly in CI and other tooling without scraping terminal text.","design":"Centralize deploy result formatting in deploy.go and keep non-interactive output first-class from the start.","acceptance_criteria":"- stackpanel deploy \u003capp\u003e supports --env to override deployment.defaultEnv\n- stackpanel deploy status [app] supports --json with stable machine-readable output\n- Success and failure paths include backend/target/env information without relying on TUI-only formatting\n- Add Go tests for flag resolution and JSON/status serialization","status":"closed","priority":1,"issue_type":"task","owner":"me@cooperm.com","created_at":"2026-03-28T15:02:34Z","created_by":"Cooper Maruyama","updated_at":"2026-03-28T20:19:29Z","closed_at":"2026-03-28T20:19:29Z","close_reason":"Superseded by pluggable-deploy-backends restructure. Work absorbed into new phase-based tasks. See openspec/changes/pluggable-deploy-backends/","external_ref":"https://linear.app/darkmatterlabs/issue/ENG-379","labels":["deployment"],"dependencies":[{"issue_id":"stackpanel-os2.2","depends_on_id":"stackpanel-os2","type":"parent-child","created_at":"2026-03-28T08:02:34Z","created_by":"Cooper Maruyama","metadata":"{}"},{"issue_id":"stackpanel-os2.2","depends_on_id":"stackpanel-os2.1","type":"blocks","created_at":"2026-03-28T08:02:38Z","created_by":"Cooper Maruyama","metadata":"{}"}],"dependency_count":1,"dependent_count":2,"comment_count":0} {"id":"stackpanel-os2","title":"Complete the deployment feature","description":"Deployment is partially implemented across Nix outputs, Go CLI commands, host-specific backends, docs, and the Studio deploy panel. The current repo has the core Nix scaffolding in nix/stackpanel/modules/deploy/module.nix and nix/stackpanel/lib/deploy.nix, app deploy/provision commands in apps/stackpanel-go/cmd/cli/{deploy,provision}.go, and a Colmena-centric UI in apps/web/src/components/studio/panels/deploy/deploy-panel.tsx. This epic tracks the remaining work needed to make deployment feel complete and coherent against docs/design/deploy-command.md and docs/design/provisioning.md.","design":"Use docs/design/deploy-command.md and docs/design/provisioning.md as the design source of truth, but scope child tasks to repo realities already present in code.","acceptance_criteria":"- Break remaining deployment work into executable child issues\n- Finish CLI, backend, provisioning, UI, and docs gaps\n- Land a validated end-to-end deployment story for NixOS machines and supported hosted backends","status":"closed","priority":1,"issue_type":"epic","owner":"me@cooperm.com","created_at":"2026-03-28T15:02:33Z","created_by":"Cooper Maruyama","updated_at":"2026-03-28T20:19:30Z","closed_at":"2026-03-28T20:19:30Z","close_reason":"all steps complete","external_ref":"https://linear.app/darkmatterlabs/issue/ENG-380","labels":["deployment"],"dependency_count":0,"dependent_count":0,"comment_count":0} -{"id":"stackpanel-i5r","title":"Studio: Marketplace browse + install panel","description":"In-studio UI for discovering, purchasing, and installing modules.\n\n## Scope\n\n- apps/web/src/components/studio/panels/marketplace-panel.tsx\n- Sections: Featured, Official (stackpanel's own paid modules), Community, Installed\n- Listing detail view: README, versions, pricing, screenshots (MDX + images from the listing)\n- Install button: free → instant; paid → Polar checkout in a popup, webhook-driven refresh on return\n- Installed view: updates available, usage (if module reports it), remove\n- Calls into the CLI via agent-local endpoints for the actual install/uninstall (so studio doesn't need Nix directly)","acceptance_criteria":"- Browse renders paginated list with search\n- Listing detail shows full MDX description + pricing\n- Free install works without leaving the studio\n- Paid install flow completes end-to-end without manual reload","status":"open","priority":2,"issue_type":"task","owner":"me@cooperm.com","created_at":"2026-04-24T03:45:13Z","created_by":"Cooper Maruyama","updated_at":"2026-04-24T03:45:13Z","dependencies":[{"issue_id":"stackpanel-i5r","depends_on_id":"stackpanel-63e","type":"blocks","created_at":"2026-04-23T20:46:09Z","created_by":"Cooper Maruyama","metadata":"{}"},{"issue_id":"stackpanel-i5r","depends_on_id":"stackpanel-w3r","type":"blocks","created_at":"2026-04-23T20:46:10Z","created_by":"Cooper Maruyama","metadata":"{}"}],"dependency_count":2,"dependent_count":0,"comment_count":0} -{"id":"stackpanel-24e","title":"Revenue accounting: gross / fee / developer net ledger","description":"Ledger that tracks every transaction with platform fee + developer share; source of truth for payouts and future reporting.\n\n## Scope\n\n- Drizzle table: revenue_event(id, license_id FK, event_type enum(purchase|renewal|refund|chargeback), gross_cents, fee_cents, developer_net_cents, currency, polar_event_id unique, occurred_at)\n- Derived view: developer_balance(user_id, module_slug, balance_cents, last_updated) — sum of developer_net_cents minus already-paid-out\n- Populated by the same Polar webhook handler as the license writes\n- Dashboard API: /api/me/revenue — current balance, monthly breakdown, per-module totals\n\n## Rules\n\n- Platform fee: 15% flat at MVP, calculated at write time (can change later without rewriting history)\n- Processing fee (~3%) absorbed from the 15% — developer always gets 85% of gross minus refunds\n- Refund: negative revenue_event, reduces balance\n- Chargeback: negative + lock payout temporarily (resolution flow is Phase 2)","acceptance_criteria":"- Every Polar webhook produces exactly one revenue_event (idempotent)\n- developer_balance view reconciles to sum of events\n- Refunds correctly decrement balance\n- Dashboard API returns accurate per-developer totals","status":"open","priority":2,"issue_type":"task","owner":"me@cooperm.com","created_at":"2026-04-24T03:45:08Z","created_by":"Cooper Maruyama","updated_at":"2026-04-24T03:45:08Z","dependencies":[{"issue_id":"stackpanel-24e","depends_on_id":"stackpanel-p4y","type":"blocks","created_at":"2026-04-23T20:46:08Z","created_by":"Cooper Maruyama","metadata":"{}"}],"dependency_count":1,"dependent_count":3,"comment_count":0} -{"id":"stackpanel-w3r","title":"CLI: stackpanel install + Nix module resolver integration","description":"End-user install flow that adds a module to .stack/config.nix and wires it through the existing module system.\n\n## Scope\n\n### CLI (apps/stack-go)\n- stackpanel install \u003cslug\u003e[@version] — looks up in catalog, checks license (for paid), fetches signed tarball, unpacks to .stack/modules/\u003cslug\u003e/\u003cversion\u003e/, edits .stack/config.nix to add the module reference + version pin\n- stackpanel uninstall \u003cslug\u003e — removes pin; leaves unpacked tarball for potential rollback\n- stackpanel update \u003cslug\u003e — checks catalog for new versions, prompts to upgrade\n- Handles paid flow: if no license, opens Polar checkout URL in browser, waits for webhook to create license, then continues install\n\n### Nix integration\n- New option: stack.modules.install = [ { slug = '...'; version = '...'; source = './path or fetchTarball'; } ];\n- Resolver: if source points to .stack/modules/\u003cslug\u003e/\u003cversion\u003e/, import the module's module.nix and merge into config\n- Auto-detection from .stack/modules/ dir as fallback\n\n## Why CLI-first\n\nStudio panel can come later — CLI covers CI + power users today.","acceptance_criteria":"- stackpanel install \u003cfree-slug\u003e works end-to-end\n- stackpanel install \u003cpaid-slug\u003e launches checkout, completes install post-purchase\n- Installed module's options appear under stack.modules.\u003cslug\u003e in the config\n- stackpanel uninstall cleanly reverts config.nix","status":"open","priority":2,"issue_type":"task","owner":"me@cooperm.com","created_at":"2026-04-24T03:45:00Z","created_by":"Cooper Maruyama","updated_at":"2026-04-24T03:45:00Z","dependencies":[{"issue_id":"stackpanel-w3r","depends_on_id":"stackpanel-89x","type":"blocks","created_at":"2026-04-23T20:46:07Z","created_by":"Cooper Maruyama","metadata":"{}"},{"issue_id":"stackpanel-w3r","depends_on_id":"stackpanel-qij","type":"blocks","created_at":"2026-04-23T20:46:08Z","created_by":"Cooper Maruyama","metadata":"{}"}],"dependency_count":2,"dependent_count":2,"comment_count":0} -{"id":"stackpanel-qij","title":"Module tarball signing + verification","description":"Ed25519 signatures on module tarballs so installers can verify authorship + integrity without trusting the distribution CDN.\n\n## Scope\n\n### Publishing side (dev portal)\n- Module author uploads tarball; cloud API signs it with platform key (MVP) OR author brings own signing key (Phase 2 with KYC)\n- Store signature + public key id in module_version.signature\n- Tarball hosted on R2/S3 with signed URLs\n\n### Verification side\n- Agent/CLI: on stackpanel install, fetch manifest + tarball, verify signature against catalog's published public key\n- Refuse install on signature mismatch with clear error\n- Nix side: evaluate tarball as a flake input with 'narHash' pin (Nix's own integrity check) once extracted — belt + suspenders\n\n## Why signing even with HTTPS\n\n- Defends against CDN compromise or MITM on corporate proxies\n- Enables offline-verifiable attestation of who published what\n- Precondition for any automated-update flow","acceptance_criteria":"- All published versions have a valid signature\n- CLI install refuses on signature mismatch (tested with a tampered tarball)\n- Signature check adds \u003c 500ms to install flow","status":"open","priority":2,"issue_type":"task","owner":"me@cooperm.com","created_at":"2026-04-24T03:44:51Z","created_by":"Cooper Maruyama","updated_at":"2026-04-24T03:44:51Z","dependencies":[{"issue_id":"stackpanel-qij","depends_on_id":"stackpanel-63e","type":"blocks","created_at":"2026-04-23T20:46:06Z","created_by":"Cooper Maruyama","metadata":"{}"}],"dependency_count":1,"dependent_count":2,"comment_count":0} -{"id":"stackpanel-89x","title":"Module license + workspace entitlements","description":"License record granting a workspace the right to install a specific module (+ seats).\n\n## Scope\n\n- Drizzle tables:\n - module_license(id, listing_id FK, workspace_id FK, polar_subscription_id | polar_order_id, status enum(active|canceled|expired|refunded), seats int default 1, activated_at, expires_at nullable)\n - module_license_event(id, license_id FK, event_type, polar_event_id unique, payload_json, created_at) — audit log\n- tRPC:\n - modules.listLicenses(workspace_id) — what this workspace owns\n - modules.verifyLicense(slug, workspace_id) → { valid, reason?, expires_at? } (called by CLI + agent)\n- Capability JWT emission (reuses stackpanel-0bt infra): when agent requests a capability token, include 'modules': [{slug, version_pin}] in claims for all active licenses\n- License inheritance across workspace forks / preview stages — TBD, default to no inheritance\n\n## Enforcement touchpoints\n\n- stackpanel install \u003cslug\u003e — CLI checks license before adding pin\n- Shell entry — agent re-verifies licenses on devshell entry for installed paid modules; warns on lapsed\n- Module-provided cloud endpoints — receive capability JWT, check 'modules' claim","acceptance_criteria":"- License auto-created on Polar webhook\n- verifyLicense correctly reflects active/expired/refunded states\n- Capability tokens include per-license claims\n- stackpanel install refuses without a valid license","status":"open","priority":2,"issue_type":"task","owner":"me@cooperm.com","created_at":"2026-04-24T03:44:43Z","created_by":"Cooper Maruyama","updated_at":"2026-04-24T03:44:43Z","dependencies":[{"issue_id":"stackpanel-89x","depends_on_id":"stackpanel-0bt","type":"blocks","created_at":"2026-04-23T20:46:05Z","created_by":"Cooper Maruyama","metadata":"{}"},{"issue_id":"stackpanel-89x","depends_on_id":"stackpanel-63e","type":"blocks","created_at":"2026-04-23T20:46:03Z","created_by":"Cooper Maruyama","metadata":"{}"},{"issue_id":"stackpanel-89x","depends_on_id":"stackpanel-9uo","type":"blocks","created_at":"2026-04-23T20:46:05Z","created_by":"Cooper Maruyama","metadata":"{}"},{"issue_id":"stackpanel-89x","depends_on_id":"stackpanel-p4y","type":"blocks","created_at":"2026-04-23T20:46:04Z","created_by":"Cooper Maruyama","metadata":"{}"}],"dependency_count":4,"dependent_count":2,"comment_count":0} -{"id":"stackpanel-p4y","title":"Polar integration: module products + checkout","description":"Wire each paid module's pricing to a Polar product; buy → checkout → webhook creates license.\n\n## Scope\n\n- When a module pricing row is created with model != 'free': auto-create a Polar product via @polar-sh SDK (one_time → product without interval; subscription → product with interval). Store polar_product_id in module_pricing.\n- tRPC procedure: modules.checkout(slug, workspace_id) → returns Polar checkout URL. Called by studio/CLI.\n- Polar webhook handler (packages/api/src/routes/webhooks/polar.ts):\n - subscription.active / order.paid → create module_license row\n - subscription.canceled / order.refunded → mark license inactive\n - Idempotent on Polar event id\n- Success redirect to studio marketplace panel with a 'Installed' flow\n\n## Notes\n\n- Separate Polar products per module so fee accounting is clean and developers can see per-product revenue in Polar dashboard once they're onboarded\n- Use Polar test env for dev; wire real keys via .stack/secrets/vars/common.sops.yaml","acceptance_criteria":"- Free modules skip Polar entirely\n- Paid module checkout returns a working Polar URL\n- Purchase → webhook → license row appears within 10s\n- Refund → license marked inactive; install command refuses","status":"open","priority":2,"issue_type":"task","owner":"me@cooperm.com","created_at":"2026-04-24T03:44:34Z","created_by":"Cooper Maruyama","updated_at":"2026-04-24T03:44:34Z","dependencies":[{"issue_id":"stackpanel-p4y","depends_on_id":"stackpanel-63e","type":"blocks","created_at":"2026-04-23T20:46:03Z","created_by":"Cooper Maruyama","metadata":"{}"}],"dependency_count":1,"dependent_count":2,"comment_count":0} +{"id":"stackpanel-dh5","title":"docs.stackpanel.com production deploy throws CF 1101 at runtime","description":"Background: cloud-gate-foundation upgraded alchemy-effect 0.9.0 → 0.12.0. apps/web deploys cleanly to local.stackpanel.com. apps/docs production deploy succeeds at the CF API level but every request returns CF error 1101 (Worker exception).\n\nWhat was tried (all merged in feat/cloud-gate-foundation, all still 1101):\n1. Added `--yes` to the deploy step so the React-based Plan UI is skipped (this fixed a separate Symbol.toPrimitive bug; deploy now reaches the upload phase)\n2. Pass full assets config object {directory, config:{notFoundHandling, htmlHandling, runWorkerFirst}} to mirror apps/docs/wrangler.jsonc — alchemy default for bare-string `assets:` was wrong\n3. Pass `isExternal: true` to skip alchemy bootstrap that otherwise wraps `main` in `Layer.effect(tag, entry).asEffect()` — OpenNext exports plain `{ fetch }` shape\n\nBundle size from the upload phase: 40.62 MB unminified. CF Workers Standard limit is 10 MB compressed; 40 MB raw is at the edge.\n\nLikely root cause (untested): rolldown bundle of OpenNext.worker.js inlines the dynamic `import(\"./server-functions/default/handler.mjs\")` instead of preserving as a chunk, producing one oversized file that workerd refuses to fully load. wrangler deploy handles this differently (preserves chunks).\n\nPossible directions:\n- Split via rolldown output options on alchemy side (would require alchemy upstream change)\n- Use a Build resource pattern to feed alchemy a pre-built bundle and skip rolldown entirely\n- Leave docs on plain wrangler deploy (revert alchemy.run.ts for docs) until alchemy ships native OpenNext support\n\napps/web deploys via Cloudflare.Vite which uses viteBuild instead of prepareBundle — that path works.","notes":"Update 2026-04-24 (got CF token with workers tail scope from himitsu cloudflare-api-token):\n\nCaptured the original 1101 root cause via tail:\n No such module \"node:perf_hooks\". imported from \"handler-BwC-NBMH.js\"\n\nFixed by bumping compatibility_date to 2026-03-17 (commit on feat/cloud-gate-foundation). That date is when CF promoted node:perf_hooks to a native module — earlier dates make unenv try to polyfill it, but the polyfill itself does `import \"node:perf_hooks\"` so it cant substitute itself in a chunked bundle.\n\nAfter fix: docs.stackpanel.com homepage returns 200, but /docs/* routes return 500 with a different exception:\n Failed to load external module shiki-db8d315635eb368c/core\n\nRoot cause (different bug): rolldown bundles props.main but Cloudflare Worker config has `unresolvedImport: false` (alchemy-effect/src/Cloudflare/Workers/Worker.ts:715). When OpenNext’s middleware/handler imports its own pre-bundled chunks like `shiki-db8d315635eb368c/core`, rolldown silences the unresolved-import warning, leaves the import literal, and the deployed worker fails to dynamic-import that module name at runtime.\n\nWorkaround paths:\n- Drop alchemy for apps/docs and revert to `wrangler deploy` (the previous wrangler.jsonc approach). Trade-off: lose declarative cert/DNS, but unblocks docs.\n- Patch alchemy/distilled.cloud rolldown plugin to NOT silence unresolved imports (would surface the issue at build time) and bundle shiki inline.\n- Pre-process .open-next/ output before passing to alchemy so all chunks are inlined into a single file.\n\nRecommend option 1 for now and revisit when alchemy adds a no-bundle / pass-through mode for `main`.","status":"open","priority":2,"issue_type":"bug","owner":"me@cooperm.com","created_at":"2026-04-25T03:20:41Z","created_by":"Cooper Maruyama","updated_at":"2026-04-25T04:08:54Z","dependency_count":0,"dependent_count":0,"comment_count":0} +{"id":"stackpanel-i5r","title":"Studio: Marketplace browse + install panel","description":"In-studio UI for discovering, purchasing, and installing modules.\n\n## Scope\n\n- apps/web/src/components/studio/panels/marketplace-panel.tsx\n- Sections: Featured, Official (stackpanel's own paid modules), Community, Installed\n- Listing detail view: README, versions, pricing, screenshots (MDX + images from the listing)\n- Install button: free → instant; paid → Polar checkout in a popup, webhook-driven refresh on return\n- Installed view: updates available, usage (if module reports it), remove\n- Calls into the CLI via agent-local endpoints for the actual install/uninstall (so studio doesn't need Nix directly)","acceptance_criteria":"- Browse renders paginated list with search\n- Listing detail shows full MDX description + pricing\n- Free install works without leaving the studio\n- Paid install flow completes end-to-end without manual reload","status":"open","priority":2,"issue_type":"task","owner":"me@cooperm.com","created_at":"2026-04-24T03:45:13Z","created_by":"Cooper Maruyama","updated_at":"2026-04-24T03:45:13Z","dependencies":[{"issue_id":"stackpanel-i5r","depends_on_id":"stackpanel-63e","type":"blocks","created_at":"2026-04-23T20:46:09Z","created_by":"Cooper Maruyama","metadata":"{}"},{"issue_id":"stackpanel-i5r","depends_on_id":"stackpanel-w3r","type":"blocks","created_at":"2026-04-23T20:46:10Z","created_by":"Cooper Maruyama","metadata":"{}"}],"dependency_count":2,"dependent_count":1,"comment_count":0} +{"id":"stackpanel-24e","title":"Revenue accounting: gross / fee / developer net ledger","description":"Ledger that tracks every transaction with platform fee + developer share; source of truth for payouts and future reporting.\n\n## Scope\n\n- Drizzle table: revenue_event(id, license_id FK, event_type enum(purchase|renewal|refund|chargeback), gross_cents, fee_cents, developer_net_cents, currency, polar_event_id unique, occurred_at)\n- Derived view: developer_balance(user_id, module_slug, balance_cents, last_updated) — sum of developer_net_cents minus already-paid-out\n- Populated by the same Polar webhook handler as the license writes\n- Dashboard API: /api/me/revenue — current balance, monthly breakdown, per-module totals\n\n## Rules\n\n- Platform fee: 15% flat at MVP, calculated at write time (can change later without rewriting history)\n- Processing fee (~3%) absorbed from the 15% — developer always gets 85% of gross minus refunds\n- Refund: negative revenue_event, reduces balance\n- Chargeback: negative + lock payout temporarily (resolution flow is Phase 2)","acceptance_criteria":"- Every Polar webhook produces exactly one revenue_event (idempotent)\n- developer_balance view reconciles to sum of events\n- Refunds correctly decrement balance\n- Dashboard API returns accurate per-developer totals","status":"open","priority":2,"issue_type":"task","owner":"me@cooperm.com","created_at":"2026-04-24T03:45:08Z","created_by":"Cooper Maruyama","updated_at":"2026-04-24T03:45:08Z","dependencies":[{"issue_id":"stackpanel-24e","depends_on_id":"stackpanel-p4y","type":"blocks","created_at":"2026-04-23T20:46:08Z","created_by":"Cooper Maruyama","metadata":"{}"}],"dependency_count":1,"dependent_count":4,"comment_count":0} +{"id":"stackpanel-w3r","title":"CLI: stackpanel install + Nix module resolver integration","description":"End-user install flow that adds a module to .stack/config.nix and wires it through the existing module system.\n\n## Scope\n\n### CLI (apps/stack-go)\n- stackpanel install \u003cslug\u003e[@version] — looks up in catalog, checks license (for paid), fetches signed tarball, unpacks to .stack/modules/\u003cslug\u003e/\u003cversion\u003e/, edits .stack/config.nix to add the module reference + version pin\n- stackpanel uninstall \u003cslug\u003e — removes pin; leaves unpacked tarball for potential rollback\n- stackpanel update \u003cslug\u003e — checks catalog for new versions, prompts to upgrade\n- Handles paid flow: if no license, opens Polar checkout URL in browser, waits for webhook to create license, then continues install\n\n### Nix integration\n- New option: stack.modules.install = [ { slug = '...'; version = '...'; source = './path or fetchTarball'; } ];\n- Resolver: if source points to .stack/modules/\u003cslug\u003e/\u003cversion\u003e/, import the module's module.nix and merge into config\n- Auto-detection from .stack/modules/ dir as fallback\n\n## Why CLI-first\n\nStudio panel can come later — CLI covers CI + power users today.","acceptance_criteria":"- stackpanel install \u003cfree-slug\u003e works end-to-end\n- stackpanel install \u003cpaid-slug\u003e launches checkout, completes install post-purchase\n- Installed module's options appear under stack.modules.\u003cslug\u003e in the config\n- stackpanel uninstall cleanly reverts config.nix","status":"open","priority":2,"issue_type":"task","owner":"me@cooperm.com","created_at":"2026-04-24T03:45:00Z","created_by":"Cooper Maruyama","updated_at":"2026-04-24T03:45:00Z","dependencies":[{"issue_id":"stackpanel-w3r","depends_on_id":"stackpanel-89x","type":"blocks","created_at":"2026-04-23T20:46:07Z","created_by":"Cooper Maruyama","metadata":"{}"},{"issue_id":"stackpanel-w3r","depends_on_id":"stackpanel-qij","type":"blocks","created_at":"2026-04-23T20:46:08Z","created_by":"Cooper Maruyama","metadata":"{}"}],"dependency_count":2,"dependent_count":3,"comment_count":0} +{"id":"stackpanel-qij","title":"Module tarball signing + verification","description":"Ed25519 signatures on module tarballs so installers can verify authorship + integrity without trusting the distribution CDN.\n\n## Scope\n\n### Publishing side (dev portal)\n- Module author uploads tarball; cloud API signs it with platform key (MVP) OR author brings own signing key (Phase 2 with KYC)\n- Store signature + public key id in module_version.signature\n- Tarball hosted on R2/S3 with signed URLs\n\n### Verification side\n- Agent/CLI: on stackpanel install, fetch manifest + tarball, verify signature against catalog's published public key\n- Refuse install on signature mismatch with clear error\n- Nix side: evaluate tarball as a flake input with 'narHash' pin (Nix's own integrity check) once extracted — belt + suspenders\n\n## Why signing even with HTTPS\n\n- Defends against CDN compromise or MITM on corporate proxies\n- Enables offline-verifiable attestation of who published what\n- Precondition for any automated-update flow","acceptance_criteria":"- All published versions have a valid signature\n- CLI install refuses on signature mismatch (tested with a tampered tarball)\n- Signature check adds \u003c 500ms to install flow","status":"open","priority":2,"issue_type":"task","owner":"me@cooperm.com","created_at":"2026-04-24T03:44:51Z","created_by":"Cooper Maruyama","updated_at":"2026-04-24T03:44:51Z","dependencies":[{"issue_id":"stackpanel-qij","depends_on_id":"stackpanel-63e","type":"blocks","created_at":"2026-04-23T20:46:06Z","created_by":"Cooper Maruyama","metadata":"{}"}],"dependency_count":1,"dependent_count":3,"comment_count":0} +{"id":"stackpanel-89x","title":"Module license + workspace entitlements","description":"License record granting a workspace the right to install a specific module (+ seats).\n\n## Scope\n\n- Drizzle tables:\n - module_license(id, listing_id FK, workspace_id FK, polar_subscription_id | polar_order_id, status enum(active|canceled|expired|refunded), seats int default 1, activated_at, expires_at nullable)\n - module_license_event(id, license_id FK, event_type, polar_event_id unique, payload_json, created_at) — audit log\n- tRPC:\n - modules.listLicenses(workspace_id) — what this workspace owns\n - modules.verifyLicense(slug, workspace_id) → { valid, reason?, expires_at? } (called by CLI + agent)\n- Capability JWT emission (reuses stackpanel-0bt infra): when agent requests a capability token, include 'modules': [{slug, version_pin}] in claims for all active licenses\n- License inheritance across workspace forks / preview stages — TBD, default to no inheritance\n\n## Enforcement touchpoints\n\n- stackpanel install \u003cslug\u003e — CLI checks license before adding pin\n- Shell entry — agent re-verifies licenses on devshell entry for installed paid modules; warns on lapsed\n- Module-provided cloud endpoints — receive capability JWT, check 'modules' claim","acceptance_criteria":"- License auto-created on Polar webhook\n- verifyLicense correctly reflects active/expired/refunded states\n- Capability tokens include per-license claims\n- stackpanel install refuses without a valid license","status":"open","priority":2,"issue_type":"task","owner":"me@cooperm.com","created_at":"2026-04-24T03:44:43Z","created_by":"Cooper Maruyama","updated_at":"2026-04-24T03:44:43Z","dependencies":[{"issue_id":"stackpanel-89x","depends_on_id":"stackpanel-0bt","type":"blocks","created_at":"2026-04-23T20:46:05Z","created_by":"Cooper Maruyama","metadata":"{}"},{"issue_id":"stackpanel-89x","depends_on_id":"stackpanel-63e","type":"blocks","created_at":"2026-04-23T20:46:03Z","created_by":"Cooper Maruyama","metadata":"{}"},{"issue_id":"stackpanel-89x","depends_on_id":"stackpanel-9uo","type":"blocks","created_at":"2026-04-23T20:46:05Z","created_by":"Cooper Maruyama","metadata":"{}"},{"issue_id":"stackpanel-89x","depends_on_id":"stackpanel-p4y","type":"blocks","created_at":"2026-04-23T20:46:04Z","created_by":"Cooper Maruyama","metadata":"{}"}],"dependency_count":4,"dependent_count":3,"comment_count":0} +{"id":"stackpanel-p4y","title":"Polar integration: module products + checkout","description":"Wire each paid module's pricing to a Polar product; buy → checkout → webhook creates license.\n\n## Scope\n\n- When a module pricing row is created with model != 'free': auto-create a Polar product via @polar-sh SDK (one_time → product without interval; subscription → product with interval). Store polar_product_id in module_pricing.\n- tRPC procedure: modules.checkout(slug, workspace_id) → returns Polar checkout URL. Called by studio/CLI.\n- Polar webhook handler (packages/api/src/routes/webhooks/polar.ts):\n - subscription.active / order.paid → create module_license row\n - subscription.canceled / order.refunded → mark license inactive\n - Idempotent on Polar event id\n- Success redirect to studio marketplace panel with a 'Installed' flow\n\n## Notes\n\n- Separate Polar products per module so fee accounting is clean and developers can see per-product revenue in Polar dashboard once they're onboarded\n- Use Polar test env for dev; wire real keys via .stack/secrets/vars/common.sops.yaml","acceptance_criteria":"- Free modules skip Polar entirely\n- Paid module checkout returns a working Polar URL\n- Purchase → webhook → license row appears within 10s\n- Refund → license marked inactive; install command refuses","status":"open","priority":2,"issue_type":"task","owner":"me@cooperm.com","created_at":"2026-04-24T03:44:34Z","created_by":"Cooper Maruyama","updated_at":"2026-04-24T03:44:34Z","dependencies":[{"issue_id":"stackpanel-p4y","depends_on_id":"stackpanel-63e","type":"blocks","created_at":"2026-04-23T20:46:03Z","created_by":"Cooper Maruyama","metadata":"{}"}],"dependency_count":1,"dependent_count":3,"comment_count":0} {"id":"stackpanel-63e","title":"Module catalog: Drizzle schema + tRPC search/list","description":"Foundation data model for the marketplace.\n\n## Scope\n\n- Drizzle tables:\n - module_listing(id, slug, name, summary, description_md, author_user_id, status enum(draft|pending|approved|rejected|deprecated), category, created_at)\n - module_version(id, listing_id FK, version semver, tarball_url, signature, manifest_json, released_at)\n - module_pricing(listing_id FK, model enum(free|one_time|subscription), price_cents, interval enum(month|year|null), polar_product_id, active bool)\n- tRPC router packages/api/src/routers/modules.ts\n - modules.list({ category?, q?, cursor? }) — public, paginated\n - modules.get(slug) — public, returns listing + latest version + pricing\n - modules.listVersions(slug) — public\n- Slug validation + reserved-word list (prevent 'stack', 'stackpanel', 'official', etc. without admin flag)\n- Seed script with 3 stub listings for local dev\n\n## Why first\n\nEvery other marketplace task depends on this catalog existing.","acceptance_criteria":"- Drizzle migration applied cleanly\n- modules.list returns paginated results with search\n- modules.get returns a full listing with pricing + latest version\n- Reserved slugs cannot be used outside admin","status":"open","priority":2,"issue_type":"task","owner":"me@cooperm.com","created_at":"2026-04-24T03:44:27Z","created_by":"Cooper Maruyama","updated_at":"2026-04-24T03:44:27Z","dependency_count":0,"dependent_count":6,"comment_count":0} -{"id":"stackpanel-b86","title":"Premium modules marketplace","description":"Epic: enable third-party developers to publish paid modules to the extension registry; users install/purchase from the studio or CLI; revenue flows to the developer minus a platform fee.\n\n## Scope\n\n- Catalog API, listings, search (free + paid in one lane)\n- Polar-backed checkout for one-time + subscription pricing\n- Per-workspace licensing with team seats\n- Signed module tarballs, verification at install/shell-entry\n- Developer portal for submitting and managing listings\n- Revenue accounting + payout rails (Polar Connect / Stripe Connect)\n- Manual review workflow at MVP; automated Nix static analysis later\n- Studio 'Marketplace' browse panel; 'stackpanel install \u003cmodule\u003e' CLI\n- Integrates with existing stack.modules option + nix/stack/modules/ auto-discovery\n\n## Economics\n\n- 15% platform fee (developer receives 85% net of processing)\n- Processing (~3%) absorbed from the 15%, not the developer's share\n- USD first; multi-currency later via Polar\n- Monthly payout minimum (TBD — $50? $100?)\n\n## Trust \u0026 safety\n\n- Nix modules can execute arbitrary build code. MVP = curated launch partners + manual review + signed tarballs required.\n- Phase 2 = automated static analysis for IFD, impure builtins, network calls outside known registries\n- Phase 3 = restricted 'pure' DSL for modules that want a verification badge\n\n## Relationship to other work\n\n- Depends on stackpanel-9uo (protectedPaidProcedure middleware) — reuses the same gate pattern\n- Benefits from stackpanel-0bt (capability JWT) for shell-entry license checks\n- Module catalog shares DB with @stack/db; uses same Polar + Better-Auth plumbing\n\n## Open questions (resolve before Phase 1 ships)\n\n1. Platform fee: 15% sticker, or tiered (higher cut for top sellers / lower for new devs)?\n2. Price floor/ceiling on one-time purchases?\n3. Annual subscription discount rate?\n4. Refund policy — 14-day hard return, or author discretion?\n5. Licensing across workspace forks + preview deploys — each needs its own seat or shared?\n6. Stackpanel's own paid modules (Hosted State, AI) — same catalog or separate 'Official' tier?\n\n## Phasing\n\n- **Phase 1 (MVP)**: curated launch, ~5 partner modules, manual payouts, honor-system + signature verification\n- **Phase 2**: self-serve dev portal, Polar Connect payouts, usage-metered pricing, automated static analysis\n- **Phase 3**: restricted DSL, plugin quality badges, bundled 'collections' pricing","acceptance_criteria":"- Developers can publish a paid module via dev portal and receive payouts\n- Users can discover, purchase, and install paid modules from studio + CLI\n- Per-workspace licensing enforced; non-licensed workspaces cannot install paid modules\n- Revenue accounting tracks gross, platform fee, and developer net\n- Signed tarballs verified at install; unsigned/invalid → refuse\n- Curated launch with ≥3 partner modules shipped","status":"open","priority":2,"issue_type":"feature","owner":"me@cooperm.com","created_at":"2026-04-24T03:44:16Z","created_by":"Cooper Maruyama","updated_at":"2026-04-24T03:44:16Z","dependencies":[{"issue_id":"stackpanel-b86","depends_on_id":"stackpanel-63e","type":"blocks","created_at":"2026-04-23T20:46:24Z","created_by":"Cooper Maruyama","metadata":"{}"}],"dependency_count":1,"dependent_count":0,"comment_count":0} +{"id":"stackpanel-b86","title":"Premium modules marketplace","description":"Epic: enable third-party developers to publish paid modules to the extension registry; users install/purchase from the studio or CLI; revenue flows to the developer minus a platform fee.\n\n## Scope\n\n- Catalog API, listings, search (free + paid in one lane)\n- Polar-backed checkout for one-time + subscription pricing\n- Per-workspace licensing with team seats\n- Signed module tarballs, verification at install/shell-entry\n- Developer portal for submitting and managing listings\n- Revenue accounting + payout rails (Polar Connect / Stripe Connect)\n- Manual review workflow at MVP; automated Nix static analysis later\n- Studio 'Marketplace' browse panel; 'stackpanel install \u003cmodule\u003e' CLI\n- Integrates with existing stack.modules option + nix/stack/modules/ auto-discovery\n\n## Economics\n\n- 15% platform fee (developer receives 85% net of processing)\n- Processing (~3%) absorbed from the 15%, not the developer's share\n- USD first; multi-currency later via Polar\n- Monthly payout minimum (TBD — $50? $100?)\n\n## Trust \u0026 safety\n\n- Nix modules can execute arbitrary build code. MVP = curated launch partners + manual review + signed tarballs required.\n- Phase 2 = automated static analysis for IFD, impure builtins, network calls outside known registries\n- Phase 3 = restricted 'pure' DSL for modules that want a verification badge\n\n## Relationship to other work\n\n- Depends on stackpanel-9uo (protectedPaidProcedure middleware) — reuses the same gate pattern\n- Benefits from stackpanel-0bt (capability JWT) for shell-entry license checks\n- Module catalog shares DB with @stack/db; uses same Polar + Better-Auth plumbing\n\n## Open questions (resolve before Phase 1 ships)\n\n1. Platform fee: 15% sticker, or tiered (higher cut for top sellers / lower for new devs)?\n2. Price floor/ceiling on one-time purchases?\n3. Annual subscription discount rate?\n4. Refund policy — 14-day hard return, or author discretion?\n5. Licensing across workspace forks + preview deploys — each needs its own seat or shared?\n6. Stackpanel's own paid modules (Hosted State, AI) — same catalog or separate 'Official' tier?\n\n## Phasing\n\n- **Phase 1 (MVP)**: curated launch, ~5 partner modules, manual payouts, honor-system + signature verification\n- **Phase 2**: self-serve dev portal, Polar Connect payouts, usage-metered pricing, automated static analysis\n- **Phase 3**: restricted DSL, plugin quality badges, bundled 'collections' pricing","acceptance_criteria":"- Developers can publish a paid module via dev portal and receive payouts\n- Users can discover, purchase, and install paid modules from studio + CLI\n- Per-workspace licensing enforced; non-licensed workspaces cannot install paid modules\n- Revenue accounting tracks gross, platform fee, and developer net\n- Signed tarballs verified at install; unsigned/invalid → refuse\n- Curated launch with ≥3 partner modules shipped","status":"open","priority":2,"issue_type":"feature","owner":"me@cooperm.com","created_at":"2026-04-24T03:44:16Z","created_by":"Cooper Maruyama","updated_at":"2026-04-24T03:44:16Z","dependencies":[{"issue_id":"stackpanel-b86","depends_on_id":"stackpanel-02c","type":"blocks","created_at":"2026-04-23T20:46:30Z","created_by":"Cooper Maruyama","metadata":"{}"},{"issue_id":"stackpanel-b86","depends_on_id":"stackpanel-24e","type":"blocks","created_at":"2026-04-23T20:46:28Z","created_by":"Cooper Maruyama","metadata":"{}"},{"issue_id":"stackpanel-b86","depends_on_id":"stackpanel-3vi","type":"blocks","created_at":"2026-04-23T20:46:31Z","created_by":"Cooper Maruyama","metadata":"{}"},{"issue_id":"stackpanel-b86","depends_on_id":"stackpanel-63e","type":"blocks","created_at":"2026-04-23T20:46:24Z","created_by":"Cooper Maruyama","metadata":"{}"},{"issue_id":"stackpanel-b86","depends_on_id":"stackpanel-89x","type":"blocks","created_at":"2026-04-23T20:46:26Z","created_by":"Cooper Maruyama","metadata":"{}"},{"issue_id":"stackpanel-b86","depends_on_id":"stackpanel-c7t","type":"blocks","created_at":"2026-04-23T20:46:29Z","created_by":"Cooper Maruyama","metadata":"{}"},{"issue_id":"stackpanel-b86","depends_on_id":"stackpanel-i5r","type":"blocks","created_at":"2026-04-23T20:46:29Z","created_by":"Cooper Maruyama","metadata":"{}"},{"issue_id":"stackpanel-b86","depends_on_id":"stackpanel-l1q","type":"blocks","created_at":"2026-04-23T20:46:31Z","created_by":"Cooper Maruyama","metadata":"{}"},{"issue_id":"stackpanel-b86","depends_on_id":"stackpanel-p4y","type":"blocks","created_at":"2026-04-23T20:46:25Z","created_by":"Cooper Maruyama","metadata":"{}"},{"issue_id":"stackpanel-b86","depends_on_id":"stackpanel-qij","type":"blocks","created_at":"2026-04-23T20:46:26Z","created_by":"Cooper Maruyama","metadata":"{}"},{"issue_id":"stackpanel-b86","depends_on_id":"stackpanel-tvv","type":"blocks","created_at":"2026-04-23T20:46:32Z","created_by":"Cooper Maruyama","metadata":"{}"},{"issue_id":"stackpanel-b86","depends_on_id":"stackpanel-w3r","type":"blocks","created_at":"2026-04-23T20:46:27Z","created_by":"Cooper Maruyama","metadata":"{}"}],"dependency_count":12,"dependent_count":0,"comment_count":0} {"id":"stackpanel-0bt","title":"Capability JWT: issuance endpoint + agent-side verifier","description":"Short-lived signed JWT from cloud API embedding plan claims, verified by Go agent before unlocking gated commands.\n\n## Scope\n\n### Cloud side (packages/api)\n- POST /api/capability/issue → returns { token, expiresAt }\n- Payload: { sub: userId, workspace_id, plan, seats, exp: now+1h, iss: api.stackpanel.com }\n- Signed with Ed25519; public key checked into repo + embedded in agent binary\n- Private key in AWS SSM (follows existing secret pattern)\n\n### Agent side (apps/stack-go)\n- New middleware capability.Verify that reads X-Stackpanel-Capability header\n- Verifies Ed25519 signature against embedded public key\n- On success: injects plan claims into request context\n- On expiry/invalid: 401 with clear error\n\n### Studio side (apps/web)\n- AgentProvider fetches capability token on login, refreshes 5min before expiry\n- Passes as header on every agent request\n\n## Scope note\n\nThis is defense in depth. Primary gating is server-side (the tRPC middleware). Capability JWT matters only for local-only features we'll gate later (e.g., Pro UI panels).","acceptance_criteria":"- Pro-tier user successfully obtains and refreshes capability tokens\n- Agent rejects requests without valid capability on gated endpoints\n- Token expiry triggers clean refresh (no visible interruption in studio)\n- Agent's embedded public key loaded at build time, verified with a unit test","status":"open","priority":2,"issue_type":"task","owner":"me@cooperm.com","created_at":"2026-04-24T03:41:23Z","created_by":"Cooper Maruyama","updated_at":"2026-04-24T03:41:23Z","dependencies":[{"issue_id":"stackpanel-0bt","depends_on_id":"stackpanel-9uo","type":"blocks","created_at":"2026-04-23T20:42:09Z","created_by":"Cooper Maruyama","metadata":"{}"}],"dependency_count":1,"dependent_count":2,"comment_count":0} -{"id":"stackpanel-9dq","title":"HostedStateStore adapter for alchemy-effect","description":"Implement alchemy-effect's StateStore interface backed by the cloud tRPC router.\n\n## Scope\n\n- packages/infra/src/state/HostedStateStore.ts\n- Implements the StateStore contract from alchemy-effect/State (get/put/list/delete)\n- Uses @trpc/client with httpBatchLink pointed at api.stackpanel.com, Bearer auth via ALCHEMY_STATE_TOKEN\n- Retries (Effect.retry with exponential backoff) for transient 5xx, not for 4xx\n- Structured error mapping so alchemy's UX shows 'Subscription required' on 402 instead of a cryptic stack trace\n- Feature flag via env: STACKPANEL_STATE_BACKEND=hosted|local; default local\n\n## Integration\n\n- apps/web/alchemy.run.ts and apps/docs/alchemy.run.ts: conditionally construct HostedStateStore when STACKPANEL_STATE_BACKEND=hosted","acceptance_criteria":"- Adapter passes alchemy-effect's StateStore contract tests\n- 402 from API surfaces as actionable 'upgrade' error in alchemy CLI output\n- Can deploy apps/web end-to-end with STACKPANEL_STATE_BACKEND=hosted","status":"open","priority":2,"issue_type":"task","owner":"me@cooperm.com","created_at":"2026-04-24T03:41:14Z","created_by":"Cooper Maruyama","updated_at":"2026-04-24T03:41:14Z","dependencies":[{"issue_id":"stackpanel-9dq","depends_on_id":"stackpanel-ehz","type":"blocks","created_at":"2026-04-23T20:42:08Z","created_by":"Cooper Maruyama","metadata":"{}"}],"dependency_count":1,"dependent_count":4,"comment_count":0} -{"id":"stackpanel-ehz","title":"tRPC router for alchemy state CRUD","description":"New tRPC router exposing state operations to the HostedStateStore adapter.\n\n## Scope\n\n- packages/api/src/routers/alchemyState.ts\n- Procedures (all wrapped with protectedPaidProcedure):\n - get(stack, stage, fqn) → decrypted JSON | null\n - put(stack, stage, fqn, payload, expectedVersion?) → new version (optimistic concurrency)\n - list(stack, stage?) → [{fqn, version, updated_at}]\n - delete(stack, stage, fqn) → void\n - listStages(stack) → [{stage, resource_count, updated_at}]\n- Input validation with Zod (stack/stage/fqn slug regex to prevent injection)\n- Error mapping: 409 on version mismatch, 404 on missing, 402 on unsubscribed (from middleware)\n- Wire router into packages/api/src/routers/_app.ts","acceptance_criteria":"- All procedures callable from a tRPC client with a Pro subscription\n- Version conflict returns 409 (not 500)\n- Unsubscribed caller gets 402\n- Integration test exercises put→get→list→delete round-trip","status":"open","priority":2,"issue_type":"task","owner":"me@cooperm.com","created_at":"2026-04-24T03:41:09Z","created_by":"Cooper Maruyama","updated_at":"2026-04-24T03:41:09Z","dependencies":[{"issue_id":"stackpanel-ehz","depends_on_id":"stackpanel-9uo","type":"blocks","created_at":"2026-04-23T20:42:06Z","created_by":"Cooper Maruyama","metadata":"{}"},{"issue_id":"stackpanel-ehz","depends_on_id":"stackpanel-9zb","type":"blocks","created_at":"2026-04-23T20:42:07Z","created_by":"Cooper Maruyama","metadata":"{}"}],"dependency_count":2,"dependent_count":3,"comment_count":0} -{"id":"stackpanel-9zb","title":"Drizzle schema for workspace_state with envelope encryption","description":"DB schema + KMS integration for storing alchemy state entries, encrypted per-workspace.\n\n## Scope\n\n- Drizzle migration: workspace_state(id uuid, workspace_id uuid FK, stack text, stage text, fqn text, key_id text, encrypted_blob bytea, version int, updated_at timestamptz). Unique on (workspace_id, stack, stage, fqn).\n- New table workspace_dek(workspace_id FK PK, encrypted_dek bytea, kms_key_id text, created_at) — per-workspace DEK wrapped by master KMS key\n- Helpers in @stack/db: encryptStateBlob(workspaceId, plaintext) / decryptStateBlob(workspaceId, ciphertext) using @aws-sdk/client-kms\n- Lazy DEK creation on first write; DEK rotation hook (not wired to a schedule yet)\n\n## Notes\n\n- Plaintext state MUST NOT be persisted anywhere. Decrypt on read, discard after response.\n- Master KMS key already provisioned via @stackpanel/infra — reuse that key ID.","acceptance_criteria":"- Migration applied cleanly to a fresh dev DB\n- encryptStateBlob/decryptStateBlob round-trip correctly\n- workspace_dek row auto-created on first encryptStateBlob call\n- KMS decrypt calls only happen on read; no plaintext persisted","status":"open","priority":2,"issue_type":"task","owner":"me@cooperm.com","created_at":"2026-04-24T03:41:02Z","created_by":"Cooper Maruyama","updated_at":"2026-04-24T03:41:02Z","dependency_count":0,"dependent_count":2,"comment_count":0} -{"id":"stackpanel-9uo","title":"Add protectedPaidProcedure middleware in @stack/api","description":"Middleware that wraps protectedProcedure and additionally verifies the user has an active Polar subscription at the 'pro' tier or higher. Rejects with TRPCError code FORBIDDEN (HTTP 402) + upgrade_url when the check fails.\n\n## Scope\n\n- packages/api/src/lib/middleware/paid.ts: new Effect-backed middleware\n- Reads subscription state via the existing @polar-sh/better-auth plugin\n- Passes {workspace_id, plan} into ctx for downstream procedures\n- Unit tests covering: active pro, active free, trial, expired, no session\n\n## Why this first\n\nUnblocks every cloud-gated feature we'll ever build — Hosted state is just the first caller.","acceptance_criteria":"- protectedPaidProcedure exported from @stack/api\n- Free/expired users get 402 with { code, upgrade_url }\n- Pro users pass through with plan claims in ctx\n- Tests cover all subscription states","status":"open","priority":2,"issue_type":"task","owner":"me@cooperm.com","created_at":"2026-04-24T03:40:55Z","created_by":"Cooper Maruyama","updated_at":"2026-04-24T03:40:55Z","dependency_count":0,"dependent_count":4,"comment_count":0} +{"id":"stackpanel-9dq","title":"HostedStateStore adapter for alchemy-effect","description":"Implement alchemy-effect's StateStore interface backed by the cloud tRPC router.\n\n## Scope\n\n- packages/infra/src/state/HostedStateStore.ts\n- Implements the StateStore contract from alchemy-effect/State (get/put/list/delete)\n- Uses @trpc/client with httpBatchLink pointed at api.stackpanel.com, Bearer auth via ALCHEMY_STATE_TOKEN\n- Retries (Effect.retry with exponential backoff) for transient 5xx, not for 4xx\n- Structured error mapping so alchemy's UX shows 'Subscription required' on 402 instead of a cryptic stack trace\n- Feature flag via env: STACKPANEL_STATE_BACKEND=hosted|local; default local\n\n## Integration\n\n- apps/web/alchemy.run.ts and apps/docs/alchemy.run.ts: conditionally construct HostedStateStore when STACKPANEL_STATE_BACKEND=hosted","acceptance_criteria":"- Adapter passes alchemy-effect's StateStore contract tests\n- 402 from API surfaces as actionable 'upgrade' error in alchemy CLI output\n- Can deploy apps/web end-to-end with STACKPANEL_STATE_BACKEND=hosted","status":"closed","priority":2,"issue_type":"task","owner":"me@cooperm.com","created_at":"2026-04-24T03:41:14Z","created_by":"Cooper Maruyama","updated_at":"2026-04-24T07:34:27Z","closed_at":"2026-04-24T07:34:27Z","close_reason":"Closed","dependencies":[{"issue_id":"stackpanel-9dq","depends_on_id":"stackpanel-ehz","type":"blocks","created_at":"2026-04-23T20:42:08Z","created_by":"Cooper Maruyama","metadata":"{}"}],"dependency_count":1,"dependent_count":4,"comment_count":0} +{"id":"stackpanel-ehz","title":"tRPC router for alchemy state CRUD","description":"New tRPC router exposing state operations to the HostedStateStore adapter.\n\n## Scope\n\n- packages/api/src/routers/alchemyState.ts\n- Procedures (all wrapped with protectedPaidProcedure):\n - get(stack, stage, fqn) → decrypted JSON | null\n - put(stack, stage, fqn, payload, expectedVersion?) → new version (optimistic concurrency)\n - list(stack, stage?) → [{fqn, version, updated_at}]\n - delete(stack, stage, fqn) → void\n - listStages(stack) → [{stage, resource_count, updated_at}]\n- Input validation with Zod (stack/stage/fqn slug regex to prevent injection)\n- Error mapping: 409 on version mismatch, 404 on missing, 402 on unsubscribed (from middleware)\n- Wire router into packages/api/src/routers/_app.ts","acceptance_criteria":"- All procedures callable from a tRPC client with a Pro subscription\n- Version conflict returns 409 (not 500)\n- Unsubscribed caller gets 402\n- Integration test exercises put→get→list→delete round-trip","status":"closed","priority":2,"issue_type":"task","owner":"me@cooperm.com","created_at":"2026-04-24T03:41:09Z","created_by":"Cooper Maruyama","updated_at":"2026-04-24T07:34:27Z","closed_at":"2026-04-24T07:34:27Z","close_reason":"Closed","dependencies":[{"issue_id":"stackpanel-ehz","depends_on_id":"stackpanel-9uo","type":"blocks","created_at":"2026-04-23T20:42:06Z","created_by":"Cooper Maruyama","metadata":"{}"},{"issue_id":"stackpanel-ehz","depends_on_id":"stackpanel-9zb","type":"blocks","created_at":"2026-04-23T20:42:07Z","created_by":"Cooper Maruyama","metadata":"{}"}],"dependency_count":2,"dependent_count":3,"comment_count":0} +{"id":"stackpanel-9zb","title":"Drizzle schema for workspace_state with envelope encryption","description":"DB schema + KMS integration for storing alchemy state entries, encrypted per-workspace.\n\n## Scope\n\n- Drizzle migration: workspace_state(id uuid, workspace_id uuid FK, stack text, stage text, fqn text, key_id text, encrypted_blob bytea, version int, updated_at timestamptz). Unique on (workspace_id, stack, stage, fqn).\n- New table workspace_dek(workspace_id FK PK, encrypted_dek bytea, kms_key_id text, created_at) — per-workspace DEK wrapped by master KMS key\n- Helpers in @stack/db: encryptStateBlob(workspaceId, plaintext) / decryptStateBlob(workspaceId, ciphertext) using @aws-sdk/client-kms\n- Lazy DEK creation on first write; DEK rotation hook (not wired to a schedule yet)\n\n## Notes\n\n- Plaintext state MUST NOT be persisted anywhere. Decrypt on read, discard after response.\n- Master KMS key already provisioned via @stackpanel/infra — reuse that key ID.","acceptance_criteria":"- Migration applied cleanly to a fresh dev DB\n- encryptStateBlob/decryptStateBlob round-trip correctly\n- workspace_dek row auto-created on first encryptStateBlob call\n- KMS decrypt calls only happen on read; no plaintext persisted","status":"closed","priority":2,"issue_type":"task","owner":"me@cooperm.com","created_at":"2026-04-24T03:41:02Z","created_by":"Cooper Maruyama","updated_at":"2026-04-24T07:34:27Z","closed_at":"2026-04-24T07:34:27Z","close_reason":"Closed","dependency_count":0,"dependent_count":2,"comment_count":0} +{"id":"stackpanel-9uo","title":"Add protectedPaidProcedure middleware in @stack/api","description":"Middleware that wraps protectedProcedure and additionally verifies the user has an active Polar subscription at the 'pro' tier or higher. Rejects with TRPCError code FORBIDDEN (HTTP 402) + upgrade_url when the check fails.\n\n## Scope\n\n- packages/api/src/lib/middleware/paid.ts: new Effect-backed middleware\n- Reads subscription state via the existing @polar-sh/better-auth plugin\n- Passes {workspace_id, plan} into ctx for downstream procedures\n- Unit tests covering: active pro, active free, trial, expired, no session\n\n## Why this first\n\nUnblocks every cloud-gated feature we'll ever build — Hosted state is just the first caller.","acceptance_criteria":"- protectedPaidProcedure exported from @stack/api\n- Free/expired users get 402 with { code, upgrade_url }\n- Pro users pass through with plan claims in ctx\n- Tests cover all subscription states","status":"closed","priority":2,"issue_type":"task","owner":"me@cooperm.com","created_at":"2026-04-24T03:40:55Z","created_by":"Cooper Maruyama","updated_at":"2026-04-24T07:34:27Z","closed_at":"2026-04-24T07:34:27Z","close_reason":"Closed","dependency_count":0,"dependent_count":4,"comment_count":0} {"id":"stackpanel-e7v","title":"Hosted alchemy state backend (Pro tier)","description":"Epic: Build a hosted state backend for alchemy-effect (replacing the filesystem-only backend), gated behind a Pro subscription via Polar. Unlocks team deploys and fixes CI orphaning.\n\n## Motivation\n\nToday alchemy-effect ships only a filesystem state backend (noted in .github/workflows/deploy-{web,docs}.yaml). Our CI has to cache the .alchemy/state/ directory across runners so destroy jobs can find the resources to tear down — otherwise 'every runner starts blind and orphans Workers + Neon projects' (comment from deploy-web.yaml).\n\nThis is:\n- A real pain felt today (CI workaround in place)\n- Gate-able by architecture (requires our cloud API; cannot be reproduced locally)\n- Natural fit with existing Polar + Better-Auth + Neon + KMS plumbing\n- Already half-scaffolded: ALCHEMY_STATE_TOKEN is in packages/gen/env/src/effect/scope/deploy.ts\n\n## Strategy\n\n- Cloud API (api.stackpanel.com) exposes tRPC procedures for state CRUD, gated by a Polar subscription check\n- Per-workspace envelope encryption (KMS master key wraps a per-workspace DEK)\n- Custom StateStore adapter on the alchemy-effect side that talks to the cloud API\n- .stack/config.nix option to toggle hosted vs local backend\n- Studio UI panel showing state entries per stage\n\n## Free vs Pro\n\n- **Free**: local filesystem backend (status quo). Works fine for solo use.\n- **Pro**: hosted backend. Survives CI runner churn. Enables true team deploys. Audit log of who deployed what.","design":"## Architecture\n\n- Alchemy-effect StateStore is an interface (see alchemy-effect/State module). We implement HostedStateStore that calls tRPC procedures via HTTPS.\n- tRPC routers live in packages/api/src/routers/alchemyState.ts with a protectedPaidProcedure middleware gate.\n- DB schema (Drizzle): workspace_state(id, workspace_id, stack, stage, fqn, encrypted_blob, key_id, version, updated_at).\n- Encryption: per-workspace DEK stored encrypted by KMS master key. On read, API decrypts DEK via KMS, decrypts state blob, returns plaintext. Plaintext never persisted.\n- Auth: ALCHEMY_STATE_TOKEN is a signed capability JWT issued by cloud API after Better-Auth login, embedding { workspace_id, plan, exp }. Agent-side verifier checks plan claim before allowing mutations (defense in depth).\n- CI: workflows pass ALCHEMY_STATE_TOKEN (from GitHub secret) → adapter uses it as Bearer → API verifies signature + plan.\n\n## Subscription gate\n\nprotectedPaidProcedure middleware:\n1. Resolve session via Better-Auth\n2. Look up Polar subscription via @polar-sh/better-auth plugin\n3. If plan.tier \u003c 'pro' or status !== 'active' → TRPCError 402 FORBIDDEN with upgrade_url\n4. Otherwise pass {workspace_id, plan} in ctx to procedure","acceptance_criteria":"- Pro users on Polar-active subscriptions can deploy using hosted state backend\n- Non-subscribers get 402 from cloud API with clear upgrade link\n- CI deploys no longer need the filesystem cache workaround in deploy-{web,docs}.yaml\n- State is encrypted at rest (no plaintext blobs in Postgres)\n- Studio has a 'State' panel showing deployed stages + resources per workspace\n- Docs guide covers migration from local → hosted","status":"open","priority":2,"issue_type":"feature","owner":"me@cooperm.com","created_at":"2026-04-24T03:40:31Z","created_by":"Cooper Maruyama","updated_at":"2026-04-24T03:40:31Z","dependencies":[{"issue_id":"stackpanel-e7v","depends_on_id":"stackpanel-0bt","type":"blocks","created_at":"2026-04-23T20:42:24Z","created_by":"Cooper Maruyama","metadata":"{}"},{"issue_id":"stackpanel-e7v","depends_on_id":"stackpanel-3p8","type":"blocks","created_at":"2026-04-23T20:42:25Z","created_by":"Cooper Maruyama","metadata":"{}"},{"issue_id":"stackpanel-e7v","depends_on_id":"stackpanel-3qt","type":"blocks","created_at":"2026-04-23T20:42:26Z","created_by":"Cooper Maruyama","metadata":"{}"},{"issue_id":"stackpanel-e7v","depends_on_id":"stackpanel-6a3","type":"blocks","created_at":"2026-04-23T20:42:26Z","created_by":"Cooper Maruyama","metadata":"{}"},{"issue_id":"stackpanel-e7v","depends_on_id":"stackpanel-9dq","type":"blocks","created_at":"2026-04-23T20:42:23Z","created_by":"Cooper Maruyama","metadata":"{}"},{"issue_id":"stackpanel-e7v","depends_on_id":"stackpanel-9uo","type":"blocks","created_at":"2026-04-23T20:42:21Z","created_by":"Cooper Maruyama","metadata":"{}"},{"issue_id":"stackpanel-e7v","depends_on_id":"stackpanel-9zb","type":"blocks","created_at":"2026-04-23T20:42:21Z","created_by":"Cooper Maruyama","metadata":"{}"},{"issue_id":"stackpanel-e7v","depends_on_id":"stackpanel-bni","type":"blocks","created_at":"2026-04-23T20:42:24Z","created_by":"Cooper Maruyama","metadata":"{}"},{"issue_id":"stackpanel-e7v","depends_on_id":"stackpanel-ehz","type":"blocks","created_at":"2026-04-23T20:42:22Z","created_by":"Cooper Maruyama","metadata":"{}"}],"dependency_count":9,"dependent_count":0,"comment_count":0} {"id":"stackpanel-7zb","title":"Move .envrc generation into lib.initFiles (drop Go constant)","description":"stackpanel init currently hardcodes .envrc contents in a Go const (envrcContent in apps/stackpanel-go/cmd/cli/init.go). This duplicates nix/flake/templates/default/.envrc and means the template can drift from what ship-to-user init writes.\n\nAction: add .envrc to lib.initFiles in nix/flake/exports.nix so it flows through the existing stepWriteInitFiles pipeline as a single source of truth; remove envrcContent + stepGenerateEnvrc from init.go; keep the idempotency semantics (don't clobber existing .envrc unless --force).\n\nContext: during stackpanel-y53/0qu work (2026-04-23), the agent chose to keep .envrc on the Go side because lib.initFiles appeared broken. It was not — lib.initFiles works and already ships .stackpanel/{config,_internal,data,.gitignore}. It just doesn't include .envrc today, and its templateDir path (../flake/templates/default/.stackpanel) points at a nonexistent dir while the real templates live under nix/flake/templates/default/.stack and the repo-root .envrc. Fix that path drift too while you're in there.","status":"open","priority":2,"issue_type":"task","owner":"me@cooperm.com","created_at":"2026-04-23T16:08:51Z","created_by":"Cooper Maruyama","updated_at":"2026-04-23T16:08:51Z","dependency_count":0,"dependent_count":0,"comment_count":0} {"id":"stackpanel-y53","title":"add envrc generation to 'stackpanel init'","status":"closed","priority":2,"issue_type":"task","assignee":"Cooper Maruyama","owner":"me@cooperm.com","created_at":"2026-04-23T15:35:17Z","created_by":"Cooper Maruyama","updated_at":"2026-04-23T15:45:55Z","started_at":"2026-04-23T15:37:04Z","closed_at":"2026-04-23T15:45:55Z","close_reason":"Closed","dependency_count":0,"dependent_count":0,"comment_count":0} @@ -40,12 +41,12 @@ {"id":"stackpanel-os2.7","title":"Unify deployment docs and migration guides around the current model","description":"The repo currently mixes older provider-centric deployment docs (for example nix/stackpanel/deployment/README.md and apps/docs/content/docs/internal/deployment.mdx) with newer Nix-first deploy/provision design docs. Once the remaining CLI/backend work lands, refresh the public/internal docs so users see one coherent deployment story instead of parallel models.","design":"Document the shipped behavior after the CLI/backend/UI work settles; keep examples aligned with docs/design/deploy-command.md and docs/design/provisioning.md.","acceptance_criteria":"- Public docs cover app deploy, machine-target deploy, provisioning, and hosted-backend flows with current commands/options\n- Internal/module docs stop teaching superseded provider/defaultProvider shapes where they no longer match shipped behavior\n- Migration guidance explains old vs new config shapes and current container/deploy commands\n- Examples and validation steps match actual commands in the repo","status":"closed","priority":2,"issue_type":"task","owner":"me@cooperm.com","created_at":"2026-03-28T15:02:38Z","created_by":"Cooper Maruyama","updated_at":"2026-03-28T20:19:30Z","closed_at":"2026-03-28T20:19:30Z","close_reason":"Superseded by pluggable-deploy-backends restructure. Work absorbed into new phase-based tasks. See openspec/changes/pluggable-deploy-backends/","external_ref":"https://linear.app/darkmatterlabs/issue/ENG-381","labels":["deployment"],"dependencies":[{"issue_id":"stackpanel-os2.7","depends_on_id":"stackpanel-os2","type":"parent-child","created_at":"2026-03-28T08:02:38Z","created_by":"Cooper Maruyama","metadata":"{}"},{"issue_id":"stackpanel-os2.7","depends_on_id":"stackpanel-os2.3","type":"blocks","created_at":"2026-03-28T08:02:42Z","created_by":"Cooper Maruyama","metadata":"{}"},{"issue_id":"stackpanel-os2.7","depends_on_id":"stackpanel-os2.4","type":"blocks","created_at":"2026-03-28T08:02:42Z","created_by":"Cooper Maruyama","metadata":"{}"},{"issue_id":"stackpanel-os2.7","depends_on_id":"stackpanel-os2.5","type":"blocks","created_at":"2026-03-28T08:02:43Z","created_by":"Cooper Maruyama","metadata":"{}"},{"issue_id":"stackpanel-os2.7","depends_on_id":"stackpanel-os2.6","type":"blocks","created_at":"2026-03-28T08:02:43Z","created_by":"Cooper Maruyama","metadata":"{}"}],"dependency_count":4,"dependent_count":0,"comment_count":0} {"id":"stackpanel-os2.5","title":"Add stackpanel provision --new and config round-trip machine authoring","description":"apps/stackpanel-go/cmd/cli/provision.go handles provisioning for machines that already exist in config, but the provisioning design also calls for a --new workflow that can author a minimal machine entry and preserve Nix path literals for hardwareConfig/diskLayout updates. Add that machine-authoring path so new-machine setup is not a manual edit-before-provision step.","design":"Reuse the repo's existing config-writing/serialization patterns instead of inventing a new config mutator; add tagged path handling if necessary to preserve Nix path types.","acceptance_criteria":"- stackpanel provision --new \u003cname\u003e --host \u003ctarget\u003e creates a minimal machine entry in the canonical Stackpanel config\n- hardwareConfig and diskLayout paths round-trip as Nix path literals instead of quoted absolute strings\n- The provision flow can update the new machine entry after generating hardware config\n- Add tests for config edit / serialization behavior","status":"closed","priority":2,"issue_type":"task","owner":"me@cooperm.com","created_at":"2026-03-28T15:02:37Z","created_by":"Cooper Maruyama","updated_at":"2026-03-28T20:19:21Z","closed_at":"2026-03-28T20:19:21Z","close_reason":"Dropped: manual config editing is acceptable, provision --new deferred indefinitely","external_ref":"https://linear.app/darkmatterlabs/issue/ENG-382","labels":["deployment"],"dependencies":[{"issue_id":"stackpanel-os2.5","depends_on_id":"stackpanel-os2","type":"parent-child","created_at":"2026-03-28T08:02:36Z","created_by":"Cooper Maruyama","metadata":"{}"},{"issue_id":"stackpanel-os2.5","depends_on_id":"stackpanel-os2.1","type":"blocks","created_at":"2026-03-28T08:02:40Z","created_by":"Cooper Maruyama","metadata":"{}"}],"dependency_count":1,"dependent_count":2,"comment_count":0} {"id":"stackpanel-os2.6","title":"Wire deploy/provision state into the Studio Deploy panel","description":"apps/web/src/components/studio/panels/deploy/deploy-panel.tsx is still Colmena-centric and does not appear to consume the CLI state tracked in .stack/state/deployments.json and .stack/state/machines.json. Update the Studio deploy experience so it reflects the same deploy/provision model and status that the CLI writes.","design":"Expose deploy/provision state through the agent/web API rather than teaching the browser to read local state files directly.","acceptance_criteria":"- The Deploy panel shows machine provisioning state and last deploy state from the supported agent/CLI APIs\n- Users can trigger deploy/provision actions from the panel with clear loading, success, and error states\n- Unsupported or partially configured backends degrade gracefully in the UI\n- Add frontend or integration coverage for the key panel states","status":"closed","priority":2,"issue_type":"task","owner":"me@cooperm.com","created_at":"2026-03-28T15:02:37Z","created_by":"Cooper Maruyama","updated_at":"2026-03-28T20:19:29Z","closed_at":"2026-03-28T20:19:29Z","close_reason":"Superseded by pluggable-deploy-backends restructure. Work absorbed into new phase-based tasks. See openspec/changes/pluggable-deploy-backends/","external_ref":"https://linear.app/darkmatterlabs/issue/ENG-383","labels":["deployment"],"dependencies":[{"issue_id":"stackpanel-os2.6","depends_on_id":"stackpanel-os2","type":"parent-child","created_at":"2026-03-28T08:02:37Z","created_by":"Cooper Maruyama","metadata":"{}"},{"issue_id":"stackpanel-os2.6","depends_on_id":"stackpanel-os2.3","type":"blocks","created_at":"2026-03-28T08:02:40Z","created_by":"Cooper Maruyama","metadata":"{}"},{"issue_id":"stackpanel-os2.6","depends_on_id":"stackpanel-os2.4","type":"blocks","created_at":"2026-03-28T08:02:41Z","created_by":"Cooper Maruyama","metadata":"{}"},{"issue_id":"stackpanel-os2.6","depends_on_id":"stackpanel-os2.5","type":"blocks","created_at":"2026-03-28T08:02:41Z","created_by":"Cooper Maruyama","metadata":"{}"}],"dependency_count":3,"dependent_count":1,"comment_count":0} -{"id":"stackpanel-3vi","title":"Docs: module author guide + marketplace policies","description":"Docs that make it obvious how to build, test, price, and publish a module — plus the policies that keep the marketplace trustworthy.\n\n## Scope\n\n### Author guide (apps/docs/content/docs/modules/)\n- 'Build your first module' — scaffolding, module.nix structure, meta.nix fields, ui.nix if applicable\n- 'Test a module locally' — stackpanel link (local dev), running against sample .stack/config.nix\n- 'Package for publication' — tarball layout, signing, manifest requirements\n- 'Price and publish' — free vs paid tradeoffs, pricing UX tips\n- 'Get paid' — Polar Connect onboarding, tax docs, payout schedule\n- 'Versioning + updates' — semver discipline, deprecation policy\n\n### Policies\n- Acceptable use: no crypto miners, no telemetry without disclosure, no license keys hardcoded\n- Refund policy: 14-day no-questions-asked (author can opt into stricter)\n- Takedown policy: security issues → emergency delist within 24h\n- Revenue share + fee structure (the 15% sticker, transparent)\n- Intellectual property: developer retains ownership, grants distribution license","acceptance_criteria":"- Author guide builds with apps/docs\n- Policies are linked from dev portal's publish flow\n- Sample module repo referenced from the 'first module' page","status":"open","priority":3,"issue_type":"task","owner":"me@cooperm.com","created_at":"2026-04-24T03:45:46Z","created_by":"Cooper Maruyama","updated_at":"2026-04-24T03:45:46Z","dependencies":[{"issue_id":"stackpanel-3vi","depends_on_id":"stackpanel-02c","type":"blocks","created_at":"2026-04-23T20:46:16Z","created_by":"Cooper Maruyama","metadata":"{}"},{"issue_id":"stackpanel-3vi","depends_on_id":"stackpanel-c7t","type":"blocks","created_at":"2026-04-23T20:46:15Z","created_by":"Cooper Maruyama","metadata":"{}"},{"issue_id":"stackpanel-3vi","depends_on_id":"stackpanel-w3r","type":"blocks","created_at":"2026-04-23T20:46:17Z","created_by":"Cooper Maruyama","metadata":"{}"}],"dependency_count":3,"dependent_count":0,"comment_count":0} -{"id":"stackpanel-l1q","title":"Module review workflow + automated Nix static analysis","description":"Prevent malicious or broken modules from reaching users. MVP manual, Phase 2 automated.\n\n## Scope\n\n### MVP: manual review\n- Admin tool (packages/api route + studio admin panel) showing pending listings\n- Reviewer sees: uploaded tarball contents, diff from previous version (if any), links to GitHub repo, automated scan results\n- Approve → listing goes live; Reject → listing status updated with reason visible to author\n- SLA target: 3 business days for initial review\n\n### Phase 2: automated scans\n- Static-analysis pass over module.nix + meta.nix:\n - Flag: import-from-derivation without explicit opt-in\n - Flag: builtins.fetchurl with non-allowlisted host\n - Flag: arbitrary path reads outside module dir\n - Flag: network calls during eval\n- Feed findings into review UI; author sees them pre-submit\n- Optionally: automatic 'verified pure' badge for modules with zero findings\n\n## Why not AI review\n\nPattern-match is more reliable for this than an LLM for the boring 'did they try to phone home during eval' checks. LLM review can come later for README/security claims.","acceptance_criteria":"- Reviewer can approve/reject pending listings\n- Rejected listings show reason to author with re-submit path\n- Static analysis surfaces known-bad patterns in a handful of test cases","status":"open","priority":3,"issue_type":"task","owner":"me@cooperm.com","created_at":"2026-04-24T03:45:37Z","created_by":"Cooper Maruyama","updated_at":"2026-04-24T03:45:37Z","dependencies":[{"issue_id":"stackpanel-l1q","depends_on_id":"stackpanel-c7t","type":"blocks","created_at":"2026-04-23T20:46:15Z","created_by":"Cooper Maruyama","metadata":"{}"}],"dependency_count":1,"dependent_count":0,"comment_count":0} -{"id":"stackpanel-02c","title":"Developer payout: Polar Connect + KYC onboarding","description":"Pay developers their accrued balance via Polar Connect (Stripe Connect underneath), with KYC + tax form collection at onboarding.\n\n## Scope\n\n- Onboarding flow: first time creating a paid listing → prompt to connect Polar Connect account (redirect OAuth flow)\n- Collect tax info (W-9 US / W-8BEN international) via Polar's Connect UI\n- Payout job (scheduled): once per month, for each developer with balance \u003e= $50, trigger Polar payout; record payout_event(developer_id, amount_cents, polar_transfer_id, status)\n- Emails: onboarding done, first sale, monthly statement\n- Admin tool for manual payout holds (fraud, chargeback disputes)\n\n## Phase 1 fallback\n\nIf Polar Connect isn't ready: accumulate balances, issue manual Wise transfers quarterly while we collect via email. Works for ~20 developers, not for scale.","acceptance_criteria":"- Developer can connect payout account end-to-end\n- Monthly payout runs successfully against test Polar env\n- Balance decrements match transferred amount\n- Tax forms captured before first payout","status":"open","priority":3,"issue_type":"task","owner":"me@cooperm.com","created_at":"2026-04-24T03:45:28Z","created_by":"Cooper Maruyama","updated_at":"2026-04-24T03:45:28Z","dependencies":[{"issue_id":"stackpanel-02c","depends_on_id":"stackpanel-24e","type":"blocks","created_at":"2026-04-23T20:46:13Z","created_by":"Cooper Maruyama","metadata":"{}"},{"issue_id":"stackpanel-02c","depends_on_id":"stackpanel-c7t","type":"blocks","created_at":"2026-04-23T20:46:14Z","created_by":"Cooper Maruyama","metadata":"{}"}],"dependency_count":2,"dependent_count":1,"comment_count":0} -{"id":"stackpanel-c7t","title":"Developer portal: submit + manage listings","description":"Dashboard where authors create listings, upload versions, set pricing, view revenue.\n\n## Scope\n\n- apps/web/src/routes/studio/developer/* (new section gated to users with \u003e0 listings or who opt in)\n- Create listing wizard: slug, name, summary, category, repo link (optional)\n- Upload version: drag-drop tarball, auto-extract manifest, show diff from previous version\n- Pricing editor: choose free / one-time / subscription; set price; connect Polar product (auto-created by backend)\n- Revenue dashboard: balance, recent transactions, export CSV\n- Listing status flow: draft → pending review → approved/rejected → published\n- Reject reasons visible to author with actionable next steps\n\n## MVP alternative\n\nIf this feels too big for Phase 1: start with a Google Form + manual backend entry. Still gets to ~5 launch partners. Ship dev portal when we have \u003e10 authors waiting.","acceptance_criteria":"- Author can create a listing, upload a version, set pricing, and submit for review\n- Rejected listings show the reason; author can fix and resubmit\n- Revenue dashboard reconciles with backend ledger","status":"open","priority":3,"issue_type":"task","owner":"me@cooperm.com","created_at":"2026-04-24T03:45:20Z","created_by":"Cooper Maruyama","updated_at":"2026-04-24T03:45:20Z","dependencies":[{"issue_id":"stackpanel-c7t","depends_on_id":"stackpanel-24e","type":"blocks","created_at":"2026-04-23T20:46:12Z","created_by":"Cooper Maruyama","metadata":"{}"},{"issue_id":"stackpanel-c7t","depends_on_id":"stackpanel-63e","type":"blocks","created_at":"2026-04-23T20:46:11Z","created_by":"Cooper Maruyama","metadata":"{}"},{"issue_id":"stackpanel-c7t","depends_on_id":"stackpanel-qij","type":"blocks","created_at":"2026-04-23T20:46:12Z","created_by":"Cooper Maruyama","metadata":"{}"}],"dependency_count":3,"dependent_count":3,"comment_count":0} +{"id":"stackpanel-3vi","title":"Docs: module author guide + marketplace policies","description":"Docs that make it obvious how to build, test, price, and publish a module — plus the policies that keep the marketplace trustworthy.\n\n## Scope\n\n### Author guide (apps/docs/content/docs/modules/)\n- 'Build your first module' — scaffolding, module.nix structure, meta.nix fields, ui.nix if applicable\n- 'Test a module locally' — stackpanel link (local dev), running against sample .stack/config.nix\n- 'Package for publication' — tarball layout, signing, manifest requirements\n- 'Price and publish' — free vs paid tradeoffs, pricing UX tips\n- 'Get paid' — Polar Connect onboarding, tax docs, payout schedule\n- 'Versioning + updates' — semver discipline, deprecation policy\n\n### Policies\n- Acceptable use: no crypto miners, no telemetry without disclosure, no license keys hardcoded\n- Refund policy: 14-day no-questions-asked (author can opt into stricter)\n- Takedown policy: security issues → emergency delist within 24h\n- Revenue share + fee structure (the 15% sticker, transparent)\n- Intellectual property: developer retains ownership, grants distribution license","acceptance_criteria":"- Author guide builds with apps/docs\n- Policies are linked from dev portal's publish flow\n- Sample module repo referenced from the 'first module' page","status":"open","priority":3,"issue_type":"task","owner":"me@cooperm.com","created_at":"2026-04-24T03:45:46Z","created_by":"Cooper Maruyama","updated_at":"2026-04-24T03:45:46Z","dependencies":[{"issue_id":"stackpanel-3vi","depends_on_id":"stackpanel-02c","type":"blocks","created_at":"2026-04-23T20:46:16Z","created_by":"Cooper Maruyama","metadata":"{}"},{"issue_id":"stackpanel-3vi","depends_on_id":"stackpanel-c7t","type":"blocks","created_at":"2026-04-23T20:46:15Z","created_by":"Cooper Maruyama","metadata":"{}"},{"issue_id":"stackpanel-3vi","depends_on_id":"stackpanel-w3r","type":"blocks","created_at":"2026-04-23T20:46:17Z","created_by":"Cooper Maruyama","metadata":"{}"}],"dependency_count":3,"dependent_count":1,"comment_count":0} +{"id":"stackpanel-l1q","title":"Module review workflow + automated Nix static analysis","description":"Prevent malicious or broken modules from reaching users. MVP manual, Phase 2 automated.\n\n## Scope\n\n### MVP: manual review\n- Admin tool (packages/api route + studio admin panel) showing pending listings\n- Reviewer sees: uploaded tarball contents, diff from previous version (if any), links to GitHub repo, automated scan results\n- Approve → listing goes live; Reject → listing status updated with reason visible to author\n- SLA target: 3 business days for initial review\n\n### Phase 2: automated scans\n- Static-analysis pass over module.nix + meta.nix:\n - Flag: import-from-derivation without explicit opt-in\n - Flag: builtins.fetchurl with non-allowlisted host\n - Flag: arbitrary path reads outside module dir\n - Flag: network calls during eval\n- Feed findings into review UI; author sees them pre-submit\n- Optionally: automatic 'verified pure' badge for modules with zero findings\n\n## Why not AI review\n\nPattern-match is more reliable for this than an LLM for the boring 'did they try to phone home during eval' checks. LLM review can come later for README/security claims.","acceptance_criteria":"- Reviewer can approve/reject pending listings\n- Rejected listings show reason to author with re-submit path\n- Static analysis surfaces known-bad patterns in a handful of test cases","status":"open","priority":3,"issue_type":"task","owner":"me@cooperm.com","created_at":"2026-04-24T03:45:37Z","created_by":"Cooper Maruyama","updated_at":"2026-04-24T03:45:37Z","dependencies":[{"issue_id":"stackpanel-l1q","depends_on_id":"stackpanel-c7t","type":"blocks","created_at":"2026-04-23T20:46:15Z","created_by":"Cooper Maruyama","metadata":"{}"}],"dependency_count":1,"dependent_count":1,"comment_count":0} +{"id":"stackpanel-02c","title":"Developer payout: Polar Connect + KYC onboarding","description":"Pay developers their accrued balance via Polar Connect (Stripe Connect underneath), with KYC + tax form collection at onboarding.\n\n## Scope\n\n- Onboarding flow: first time creating a paid listing → prompt to connect Polar Connect account (redirect OAuth flow)\n- Collect tax info (W-9 US / W-8BEN international) via Polar's Connect UI\n- Payout job (scheduled): once per month, for each developer with balance \u003e= $50, trigger Polar payout; record payout_event(developer_id, amount_cents, polar_transfer_id, status)\n- Emails: onboarding done, first sale, monthly statement\n- Admin tool for manual payout holds (fraud, chargeback disputes)\n\n## Phase 1 fallback\n\nIf Polar Connect isn't ready: accumulate balances, issue manual Wise transfers quarterly while we collect via email. Works for ~20 developers, not for scale.","acceptance_criteria":"- Developer can connect payout account end-to-end\n- Monthly payout runs successfully against test Polar env\n- Balance decrements match transferred amount\n- Tax forms captured before first payout","status":"open","priority":3,"issue_type":"task","owner":"me@cooperm.com","created_at":"2026-04-24T03:45:28Z","created_by":"Cooper Maruyama","updated_at":"2026-04-24T03:45:28Z","dependencies":[{"issue_id":"stackpanel-02c","depends_on_id":"stackpanel-24e","type":"blocks","created_at":"2026-04-23T20:46:13Z","created_by":"Cooper Maruyama","metadata":"{}"},{"issue_id":"stackpanel-02c","depends_on_id":"stackpanel-c7t","type":"blocks","created_at":"2026-04-23T20:46:14Z","created_by":"Cooper Maruyama","metadata":"{}"}],"dependency_count":2,"dependent_count":2,"comment_count":0} +{"id":"stackpanel-c7t","title":"Developer portal: submit + manage listings","description":"Dashboard where authors create listings, upload versions, set pricing, view revenue.\n\n## Scope\n\n- apps/web/src/routes/studio/developer/* (new section gated to users with \u003e0 listings or who opt in)\n- Create listing wizard: slug, name, summary, category, repo link (optional)\n- Upload version: drag-drop tarball, auto-extract manifest, show diff from previous version\n- Pricing editor: choose free / one-time / subscription; set price; connect Polar product (auto-created by backend)\n- Revenue dashboard: balance, recent transactions, export CSV\n- Listing status flow: draft → pending review → approved/rejected → published\n- Reject reasons visible to author with actionable next steps\n\n## MVP alternative\n\nIf this feels too big for Phase 1: start with a Google Form + manual backend entry. Still gets to ~5 launch partners. Ship dev portal when we have \u003e10 authors waiting.","acceptance_criteria":"- Author can create a listing, upload a version, set pricing, and submit for review\n- Rejected listings show the reason; author can fix and resubmit\n- Revenue dashboard reconciles with backend ledger","status":"open","priority":3,"issue_type":"task","owner":"me@cooperm.com","created_at":"2026-04-24T03:45:20Z","created_by":"Cooper Maruyama","updated_at":"2026-04-24T03:45:20Z","dependencies":[{"issue_id":"stackpanel-c7t","depends_on_id":"stackpanel-24e","type":"blocks","created_at":"2026-04-23T20:46:12Z","created_by":"Cooper Maruyama","metadata":"{}"},{"issue_id":"stackpanel-c7t","depends_on_id":"stackpanel-63e","type":"blocks","created_at":"2026-04-23T20:46:11Z","created_by":"Cooper Maruyama","metadata":"{}"},{"issue_id":"stackpanel-c7t","depends_on_id":"stackpanel-qij","type":"blocks","created_at":"2026-04-23T20:46:12Z","created_by":"Cooper Maruyama","metadata":"{}"}],"dependency_count":3,"dependent_count":4,"comment_count":0} {"id":"stackpanel-3qt","title":"Docs: hosted alchemy state backend — guide + migration","description":"Documentation for the hosted backend: tradeoffs, setup, and migration from local.\n\n## Scope\n\n- apps/docs/content/docs/guides/hosted-state.mdx: new guide\n- Cover: why hosted (team deploys, CI resilience, audit trail), why not (solo use, offline), subscription requirement, encryption model, incident/outage behavior\n- Migration steps: toggle Nix option → obtain ALCHEMY_STATE_TOKEN → first deploy uploads existing local state\n- Screenshots of the Studio State panel\n- Link from deploy-*.yaml workflow comments to this doc\n\n## Why bundle this with the feature\n\nUsers won't discover hosted state unless it's in the docs; Pro conversion depends on understanding the value.","acceptance_criteria":"- Guide builds with apps/docs\n- Links from workflow comments + from the Studio empty state\n- Covers pricing + how the capability token works","status":"open","priority":3,"issue_type":"task","owner":"me@cooperm.com","created_at":"2026-04-24T03:41:46Z","created_by":"Cooper Maruyama","updated_at":"2026-04-24T03:41:46Z","dependencies":[{"issue_id":"stackpanel-3qt","depends_on_id":"stackpanel-3p8","type":"blocks","created_at":"2026-04-23T20:42:15Z","created_by":"Cooper Maruyama","metadata":"{}"},{"issue_id":"stackpanel-3qt","depends_on_id":"stackpanel-9dq","type":"blocks","created_at":"2026-04-23T20:42:15Z","created_by":"Cooper Maruyama","metadata":"{}"},{"issue_id":"stackpanel-3qt","depends_on_id":"stackpanel-bni","type":"blocks","created_at":"2026-04-23T20:42:16Z","created_by":"Cooper Maruyama","metadata":"{}"}],"dependency_count":3,"dependent_count":1,"comment_count":0} {"id":"stackpanel-6a3","title":"Migrate CI deploys off filesystem cache once hosted backend ships","description":"Kill the actions/cache hack in deploy-{web,docs}.yaml.\n\n## Scope\n\n- .github/workflows/deploy-web.yaml: remove 'Restore alchemy state' step (lines ~82-93)\n- .github/workflows/deploy-docs.yaml: remove analogous step (lines ~80-88)\n- Same for the destroy jobs\n- Add ALCHEMY_STATE_TOKEN to the job env (pulled from GitHub secrets)\n- Update comments: explain why state persistence is no longer our problem\n- Verify preview deploys still work (state now survives runner churn because it's in our DB)\n\n## Blocker\n\nOnly run this after HostedStateStore is shipped and proven on dev-CI for a week.","acceptance_criteria":"- CI workflows have no filesystem state caching\n- Preview deploys + destroys work across runner churn\n- bun.lock / workflow diff shows net-negative lines","status":"open","priority":3,"issue_type":"task","owner":"me@cooperm.com","created_at":"2026-04-24T03:41:41Z","created_by":"Cooper Maruyama","updated_at":"2026-04-24T03:41:41Z","dependencies":[{"issue_id":"stackpanel-6a3","depends_on_id":"stackpanel-9dq","type":"blocks","created_at":"2026-04-23T20:42:11Z","created_by":"Cooper Maruyama","metadata":"{}"},{"issue_id":"stackpanel-6a3","depends_on_id":"stackpanel-bni","type":"blocks","created_at":"2026-04-23T20:42:14Z","created_by":"Cooper Maruyama","metadata":"{}"}],"dependency_count":2,"dependent_count":1,"comment_count":0} {"id":"stackpanel-3p8","title":"Studio: State panel showing hosted state per workspace/stack/stage","description":"New studio UI panel for browsing hosted state — who deployed what, when, which resources exist per stage.\n\n## Scope\n\n- apps/web/src/components/studio/panels/state-panel.tsx\n- Wires to alchemyState.list + listStages tRPC procedures (not the agent)\n- Tree view: workspace → stack → stage → resources\n- Row actions: 'View JSON' (decrypt on-demand via get), 'Rollback to previous version' (requires versioning; keep as a follow-up)\n- Empty state for local-backend users pointing to the Pro upgrade\n\n## Why this matters\n\nGives Pro users immediate visible value — audit log-like view of their cloud state they couldn't have on local filesystem.","acceptance_criteria":"- Panel lists all stages for the active workspace\n- JSON view decrypts on demand and renders in a modal\n- Local-backend users see upgrade prompt\n- Panel doesn't break for free users (no 500s)","status":"open","priority":3,"issue_type":"task","owner":"me@cooperm.com","created_at":"2026-04-24T03:41:35Z","created_by":"Cooper Maruyama","updated_at":"2026-04-24T03:41:35Z","dependencies":[{"issue_id":"stackpanel-3p8","depends_on_id":"stackpanel-ehz","type":"blocks","created_at":"2026-04-23T20:42:10Z","created_by":"Cooper Maruyama","metadata":"{}"}],"dependency_count":1,"dependent_count":2,"comment_count":0} -{"id":"stackpanel-bni","title":"Nix option: stack.deploy.stateBackend = 'hosted' | 'local'","description":"Expose the state backend choice in .stack/config.nix so users can opt into the Pro backend declaratively.\n\n## Scope\n\n- nix/stackpanel/core/options/deploy.nix: new option stack.deploy.stateBackend (default 'local')\n- When 'hosted': emit STACKPANEL_STATE_BACKEND=hosted into _envs/deploy, and mark ALCHEMY_STATE_TOKEN as required\n- When 'local': leave current behavior untouched\n- Preflight warning if 'hosted' but no ALCHEMY_STATE_TOKEN in scope\n- JSON schema update for IDE intellisense (.stack/gen/schemas/)","acceptance_criteria":"- Config validates with stateBackend = 'hosted' + ALCHEMY_STATE_TOKEN present\n- Missing token emits a clear preflight warning\n- Default-unchanged behavior: existing users see no difference","status":"open","priority":3,"issue_type":"task","owner":"me@cooperm.com","created_at":"2026-04-24T03:41:28Z","created_by":"Cooper Maruyama","updated_at":"2026-04-24T03:41:28Z","dependencies":[{"issue_id":"stackpanel-bni","depends_on_id":"stackpanel-9dq","type":"blocks","created_at":"2026-04-23T20:42:09Z","created_by":"Cooper Maruyama","metadata":"{}"}],"dependency_count":1,"dependent_count":3,"comment_count":0} -{"id":"stackpanel-tvv","title":"Phase 2: usage-metered pricing via Polar Meters","description":"Enable modules to charge by usage (e.g., 'N deploys/month', 'M agents run') in addition to flat pricing.\n\n## Scope\n\n- Polar Meter integration via @polar-sh SDK\n- Module manifest gains optional 'meters' field declaring the usage dimensions\n- Runtime: module reports usage via tRPC modules.reportUsage(slug, meter, delta) — gated by license\n- Billing: Polar rolls up usage monthly; webhook events translate into revenue_event rows\n- Author-side: dashboard shows usage graphs + projected revenue\n- User-side: studio shows current usage with soft caps before hard billing\n\n## Why Phase 2\n\nOne-time + subscription covers 95% of modules. Metered billing adds real complexity (rate limits, reconciliation, disputes). Ship it when we have a use case demanding it.","acceptance_criteria":"- At least one test module uses metered pricing end-to-end\n- Usage events reconcile between our ledger and Polar\n- User can see current usage in studio","status":"open","priority":4,"issue_type":"task","owner":"me@cooperm.com","created_at":"2026-04-24T03:45:52Z","created_by":"Cooper Maruyama","updated_at":"2026-04-24T03:45:52Z","dependencies":[{"issue_id":"stackpanel-tvv","depends_on_id":"stackpanel-24e","type":"blocks","created_at":"2026-04-23T20:46:18Z","created_by":"Cooper Maruyama","metadata":"{}"},{"issue_id":"stackpanel-tvv","depends_on_id":"stackpanel-89x","type":"blocks","created_at":"2026-04-23T20:46:18Z","created_by":"Cooper Maruyama","metadata":"{}"}],"dependency_count":2,"dependent_count":0,"comment_count":0} +{"id":"stackpanel-bni","title":"Nix option: stack.deploy.stateBackend = 'hosted' | 'local'","description":"Expose the state backend choice in .stack/config.nix so users can opt into the Pro backend declaratively.\n\n## Scope\n\n- nix/stackpanel/core/options/deploy.nix: new option stack.deploy.stateBackend (default 'local')\n- When 'hosted': emit STACKPANEL_STATE_BACKEND=hosted into _envs/deploy, and mark ALCHEMY_STATE_TOKEN as required\n- When 'local': leave current behavior untouched\n- Preflight warning if 'hosted' but no ALCHEMY_STATE_TOKEN in scope\n- JSON schema update for IDE intellisense (.stack/gen/schemas/)","acceptance_criteria":"- Config validates with stateBackend = 'hosted' + ALCHEMY_STATE_TOKEN present\n- Missing token emits a clear preflight warning\n- Default-unchanged behavior: existing users see no difference","status":"closed","priority":3,"issue_type":"task","assignee":"Cooper Maruyama","owner":"me@cooperm.com","created_at":"2026-04-24T03:41:28Z","created_by":"Cooper Maruyama","updated_at":"2026-04-24T07:36:45Z","started_at":"2026-04-24T07:34:36Z","closed_at":"2026-04-24T07:36:45Z","close_reason":"Closed","dependencies":[{"issue_id":"stackpanel-bni","depends_on_id":"stackpanel-9dq","type":"blocks","created_at":"2026-04-23T20:42:09Z","created_by":"Cooper Maruyama","metadata":"{}"}],"dependency_count":1,"dependent_count":3,"comment_count":0} +{"id":"stackpanel-tvv","title":"Phase 2: usage-metered pricing via Polar Meters","description":"Enable modules to charge by usage (e.g., 'N deploys/month', 'M agents run') in addition to flat pricing.\n\n## Scope\n\n- Polar Meter integration via @polar-sh SDK\n- Module manifest gains optional 'meters' field declaring the usage dimensions\n- Runtime: module reports usage via tRPC modules.reportUsage(slug, meter, delta) — gated by license\n- Billing: Polar rolls up usage monthly; webhook events translate into revenue_event rows\n- Author-side: dashboard shows usage graphs + projected revenue\n- User-side: studio shows current usage with soft caps before hard billing\n\n## Why Phase 2\n\nOne-time + subscription covers 95% of modules. Metered billing adds real complexity (rate limits, reconciliation, disputes). Ship it when we have a use case demanding it.","acceptance_criteria":"- At least one test module uses metered pricing end-to-end\n- Usage events reconcile between our ledger and Polar\n- User can see current usage in studio","status":"open","priority":4,"issue_type":"task","owner":"me@cooperm.com","created_at":"2026-04-24T03:45:52Z","created_by":"Cooper Maruyama","updated_at":"2026-04-24T03:45:52Z","dependencies":[{"issue_id":"stackpanel-tvv","depends_on_id":"stackpanel-24e","type":"blocks","created_at":"2026-04-23T20:46:18Z","created_by":"Cooper Maruyama","metadata":"{}"},{"issue_id":"stackpanel-tvv","depends_on_id":"stackpanel-89x","type":"blocks","created_at":"2026-04-23T20:46:18Z","created_by":"Cooper Maruyama","metadata":"{}"}],"dependency_count":2,"dependent_count":1,"comment_count":0} diff --git a/.github/workflows/deploy-api.yaml b/.github/workflows/deploy-api.yaml new file mode 100644 index 00000000..0a13a53e --- /dev/null +++ b/.github/workflows/deploy-api.yaml @@ -0,0 +1,123 @@ +name: deploy-api + +# Deploys apps/api to Fly. +# +# Linux runners because skopeo-nix2container doesn't build on darwin (upstream +# bug in the vendored go.podman.io path). The whole container pipeline — build, +# push, deploy — runs on ubuntu-latest where the standard nix2container +# toolchain works without patches. +# +# Triggers: +# - push to main touching apps/api/** or packages/api/** +# - manual via workflow_dispatch + +on: + push: + # Include feature branches matching feat/cloud-gate-* so we can verify + # the deploy pipeline before merging to main. Production deploys still + # gate on main. + branches: [main, "feat/cloud-gate-**"] + paths: + - "apps/api/**" + - "packages/api/**" + - "packages/auth/**" + - "packages/db/**" + - "packages/gen/env/**" + - ".sops.yaml" + - ".stack/config.nix" + - ".stack/config.apps.nix" + - "nix/**" + - ".github/workflows/deploy-api.yaml" + workflow_dispatch: + inputs: + skip_build: + description: "Skip container build/push (deploy last-pushed image)" + type: boolean + default: false + +concurrency: + group: deploy-api-${{ github.ref }} + cancel-in-progress: false + +jobs: + deploy: + runs-on: ubuntu-latest + permissions: + contents: read + id-token: write + env: + FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN }} + SOPS_AGE_KEY: ${{ secrets.SECRETS_AGE_KEY_DEV }} + # mkAppDir reads .output via this absolute path so freshly-built + # untracked output (apps/api/.output/server/index.mjs) lands in the + # nix store. Must be visible to BOTH `nix build` and the push step + # (each re-evaluates the flake separately) — set at job scope so + # the derivation hash matches across steps. + STACKPANEL_ROOT_ABSOLUTE: ${{ github.workspace }} + steps: + - uses: actions/checkout@v4 + + - uses: DeterminateSystems/nix-installer-action@main + with: + extra-conf: | + accept-flake-config = true + + - uses: DeterminateSystems/magic-nix-cache-action@main + + - uses: superfly/flyctl-actions/setup-flyctl@master + + - name: Install sops + run: | + curl -LO https://github.com/getsops/sops/releases/download/v3.11.0/sops-v3.11.0.linux.amd64 + chmod +x sops-v3.11.0.linux.amd64 + sudo mv sops-v3.11.0.linux.amd64 /usr/local/bin/sops + + - name: Stage Fly secrets from SOPS + run: bash apps/api/scripts/push-secrets.sh + + - uses: oven-sh/setup-bun@v2 + with: + bun-version: 1.3.11 + + - name: Build api bundle (produces apps/api/.output for the container) + if: inputs.skip_build != true + run: | + bun install --frozen-lockfile + cd apps/api && bun run build + + - name: Build container image + if: inputs.skip_build != true + run: nix build --impure .#packages.x86_64-linux.container-api + + - name: Push container image to Fly registry + if: inputs.skip_build != true + run: | + nix run --impure .#copy-container-api -- \ + docker://registry.fly.io/ \ + --dest-creds "x:${FLY_API_TOKEN}" + + - name: Deploy + run: | + flyctl deploy \ + --config apps/api/fly.toml \ + --app stackpanel-api \ + --image registry.fly.io/stackpanel-api:latest \ + --wait-timeout 300 + + - name: Verify health + run: | + curl -fsS --retry 5 --retry-delay 5 \ + https://stackpanel-api.fly.dev/health + + # Idempotent ACME cert + Cloudflare A/AAAA records pointing at the + # Fly app's IPs. Skipped on dev (the .fly.dev hostname is enough) + # and only kicks in for production / staging / pr-N stages — see + # apps/api/alchemy.run.ts. + - name: Bind public hostname + if: github.ref_name == 'main' || github.ref_name == 'develop' || github.event_name == 'pull_request' + env: + FLY_IO_API_KEY: ${{ secrets.FLY_API_TOKEN }} + STAGE: ${{ github.ref_name == 'main' && 'production' || github.ref_name == 'develop' && 'staging' || format('pr-{0}', github.event.pull_request.number) }} + working-directory: apps/api + run: bunx alchemy-effect deploy --stage "$STAGE" + diff --git a/.github/workflows/deploy-docs.yaml b/.github/workflows/deploy-docs.yaml index 4deb9390..bf91db08 100644 --- a/.github/workflows/deploy-docs.yaml +++ b/.github/workflows/deploy-docs.yaml @@ -99,10 +99,14 @@ jobs: # SOPS AGE key used by `loadAppEnv` (sops-age) to decrypt the # generated per-app payloads. `production` uses the prod key; # everything else (staging, pr-*, dev) uses the dev key. - SOPS_AGE_KEY: ${{ needs.stage.outputs.stage == 'production' && secrets.SECRETS_AGE_KEY_PROD || secrets.SECRETS_AGE_KEY_DEV }} + # All stages encrypt with the github_actions age recipient + # (SECRETS_AGE_KEY_DEV's pubkey). The previous prod/dev split + # was stale — SECRETS_AGE_KEY_PROD's pubkey isn't on the prod + # payloads, so production deploys failed at decrypt time. + SOPS_AGE_KEY: ${{ secrets.SECRETS_AGE_KEY_DEV }} run: | set -euo pipefail - bunx alchemy-effect deploy --stage ${{ needs.stage.outputs.stage }} + bunx alchemy-effect deploy --stage ${{ needs.stage.outputs.stage }} --yes - name: Comment preview URL on PR if: github.event_name == 'pull_request' uses: marocchino/sticky-pull-request-comment@v2 @@ -139,7 +143,7 @@ jobs: SOPS_AGE_KEY: ${{ secrets.SECRETS_AGE_KEY_DEV }} run: | set -euo pipefail - bunx alchemy-effect destroy --stage ${{ needs.stage.outputs.stage }} + bunx alchemy-effect destroy --stage ${{ needs.stage.outputs.stage }} --yes - name: Delete cached alchemy state if: always() env: diff --git a/.github/workflows/deploy-web.yaml b/.github/workflows/deploy-web.yaml index b0b29bee..efbc8ebb 100644 --- a/.github/workflows/deploy-web.yaml +++ b/.github/workflows/deploy-web.yaml @@ -96,7 +96,11 @@ jobs: # SOPS AGE key used by `loadAppEnv` (sops-age) to decrypt the # generated per-app payloads. `production` uses the prod key; # everything else (staging, pr-*, dev) uses the dev key. - SOPS_AGE_KEY: ${{ needs.stage.outputs.stage == 'production' && secrets.SECRETS_AGE_KEY_PROD || secrets.SECRETS_AGE_KEY_DEV }} + # All stages encrypt with the github_actions age recipient + # (SECRETS_AGE_KEY_DEV's pubkey). The previous prod/dev split + # was stale — SECRETS_AGE_KEY_PROD's pubkey isn't on the prod + # payloads, so production deploys failed at decrypt time. + SOPS_AGE_KEY: ${{ secrets.SECRETS_AGE_KEY_DEV }} run: | set -euo pipefail bunx alchemy-effect deploy --stage ${{ needs.stage.outputs.stage }} --yes diff --git a/.gitignore b/.gitignore index 28f9deb4..244b3897 100644 --- a/.gitignore +++ b/.gitignore @@ -1,8 +1,11 @@ .artifacts release.tar.gz -.stackpanel-root +# `.stackpanel-root` holds a single "." so the stackpanel-root flake +# input resolves to the flake source dir across machines. Must be +# tracked so the flake input can read it during pure evaluation. *.local.json .worktrees/ +.patch-work/ # Dependencies node_modules .pnp diff --git a/.sops.yaml b/.sops.yaml index 270e5a6e..19d4f4d7 100644 --- a/.sops.yaml +++ b/.sops.yaml @@ -13,7 +13,7 @@ keys: - &coopmoney_3 age1dwqnyurvm7vasf9n7alduzmg79nczkuafknr8x3l4jnzwnuzzydqj0y92p - &coopmoney_4 age1dx6u86w8d242tvjesz362caf4lcatw24ldd0hj9qn7xhqw0s0c5qus8wxt - &fkb032 age1h0nv9lwkkhd9y0rlf832g3lualvjafqpyvlkgf8d0cn6c4zg959qkrfzt3 - - &github_actions age1eqcj2g0fdekj2wpqp4y0fg9c5myydjdt9zlr5scr0grk6fxszymqkpw5jf + - &github_actions age1d9h9mm3u5qalmpl2pf62pyzqj8t654n435emn93rutv0cg9sr32sg64fdj - &jjkoh95 age1fm7zr0ea3d589tkgcz2klqgnajduzkr25e8tnhh7qxzuleqxq3yq3c0s3t - &keyservice age16wuzuxnkcgfuxzvzgk5e5a5f6hhs386adjewyv54m9esr4yj6uuslpn6tp - &local age16rkvks3tljju3y6xu0l7luhjzx634et97g3xe58xf2dgfn2865rqkq6t8f diff --git a/.stack/config.apps.nix b/.stack/config.apps.nix index 60054235..734b8c9f 100644 --- a/.stack/config.apps.nix +++ b/.stack/config.apps.nix @@ -47,6 +47,43 @@ let }; in { + # apps/api + api = { + name = "api"; + description = "Cloud API (Better-Auth, Polar webhooks, hosted alchemy state)."; + path = "apps/api"; + type = "bun"; + + bun.generateFiles = false; + + env = { + PORT = { + value = "3000"; + }; + } + // envs.shared; + + container = { + enable = true; + type = "bun"; + port = 3000; + }; + + deployment = { + enable = true; + host = "fly"; + fly = { + appName = "stackpanel-api"; + region = "iad"; + memory = "512mb"; + cpus = 1; + autoStop = "stop"; + minMachines = 1; + forceHttps = true; + }; + }; + }; + # apps/docs docs = { name = "docs"; diff --git a/.stack/config.nix b/.stack/config.nix index 982e2983..00ec17e4 100644 --- a/.stack/config.nix +++ b/.stack/config.nix @@ -144,7 +144,13 @@ warnIfMissing = true; }; settings = { - backend = "nix2container"; + # dockerTools emits a standard docker-archive tarball that the + # system skopeo can push without patches. nix2container's + # skopeo-nix2container currently fails to build against skopeo 1.20 + # (upstream patches `vendor/go.podman.io/image/v5` but that path no + # longer exists in the vendored tree). Switch back once the + # upstream bug is fixed. + backend = "dockerTools"; }; }; @@ -304,6 +310,31 @@ secret = true; sops = "/dev/postgres-url"; }; + + # Fly api deploy secrets — routed through the CI-accessible deploy + # scope so the deploy workflow can decrypt them. push-secrets.sh + # reads from the rendered deploy payload, not shared.sops.yaml + # directly (which is encrypted only for human users' AGE keys). + BETTER_AUTH_SECRET = { + secret = true; + sops = "/shared/better-auth-secret"; + }; + POLAR_ACCESS_TOKEN = { + secret = true; + sops = "/shared/polar-access-token"; + }; + POLAR_WEBHOOK_SECRET = { + secret = true; + sops = "/shared/polar-webhook-secret"; + }; + POLAR_PRO_PRODUCT_ID_PRODUCTION = { + secret = true; + sops = "/shared/polar-pro-product-id-production"; + }; + POLAR_FREE_PRODUCT_ID_PRODUCTION = { + secret = true; + sops = "/shared/polar-free-product-id-production"; + }; }; }; @@ -749,10 +780,13 @@ }; local = { public-key = "age16rkvks3tljju3y6xu0l7luhjzx634et97g3xe58xf2dgfn2865rqkq6t8f"; - tags = [ "dev" ]; + tags = [ + "dev" + "deploy" + ]; }; github-actions = { - public-key = "age1eqcj2g0fdekj2wpqp4y0fg9c5myydjdt9zlr5scr0grk6fxszymqkpw5jf"; + public-key = "age1d9h9mm3u5qalmpl2pf62pyzqj8t654n435emn93rutv0cg9sr32sg64fdj"; tags = [ "dev" "staging" diff --git a/.stack/secrets/vars/common.sops.yaml b/.stack/secrets/vars/common.sops.yaml new file mode 100644 index 00000000..5f779659 --- /dev/null +++ b/.stack/secrets/vars/common.sops.yaml @@ -0,0 +1,161 @@ +alchemy_state_token: ENC[AES256_GCM,data:qj9L0l9/Gag4HEZrHe1i4xy9GlzinW104yc=,iv:jHITR1HIzlMEGzP6F/c6VBKPhB2a/6ekk/p7Al99nqg=,tag:uTSDHFYygmuDebYRxI8Zlg==,type:str] +alchemy-state-token: ENC[AES256_GCM,data:s0ogaZrbQ9/eeBMHSn2C0tDc7mssSF3+IqXuXkLNOWBb6yGLVbS/gP4RAZg=,iv:xx/a20zxsQ2vgBozh0rwuZRv0learBObR8Sdf69dU1Q=,tag:wjVymVZHToR2lpOQy818lQ==,type:str] +sops: + age: + - recipient: age14vpdar7vzznyxgskp9772zjar95n8l2f36w6tzk980889t7kjdqsc5a50q + enc: | + -----BEGIN AGE ENCRYPTED FILE----- + YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBzNnJRU2VmKzUxRjJyS0l0 + K3U3VUlNVDRxSTY0dE9pcHAvNUNzVmFEUlJ3ClBna05pS01qSVRXMTRHMHM1R1lX + aFBpWDFxQll1QUNhK1BhN2pQQmJmdVEKLS0tIElBQXBDSXdlc0IyYXA4QTVyNXpn + ZzNyMm9KS3VEQ0lXZnVwdFFPL0tOcVUK0O9bIq4xNmMSJZP133J6bKJwAeFe/qu/ + ixX8mbQ5s6SJb2JkWdRLQPH5r36cqy8WcohI7cg47++NEo1UjZ2Mkg== + -----END AGE ENCRYPTED FILE----- + - recipient: age1ugmyh9qcz05ehtkgnt2nn3jfz2rf2umnmqx69pgp2ue82dn7vpuqlc3g7v + enc: | + -----BEGIN AGE ENCRYPTED FILE----- + YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSB5WmNZN2F5N2dNeGwzVksr + dG16b3Nld3U4dWJCWUFtNm9RZzJPTFRsRkNvClgzWW5aT2xrLzNBcURLZmNnOHdI + bHdRQzI3ekF3bGNsM0w1WlBQMjMwa0kKLS0tIER5bFI2dkNxSFFyUjA4NWNsMlcy + OUNLcHZ3aEdoMnlPZ2h3b3RXQU9JMEUKtUc0UxB/yfEhYncQmpzgqOlFFf20gwep + 47k27glisau3Wfs9EohuTgCPnqaGCbfQvdSMvCkQrEKXrp6UV+IQSQ== + -----END AGE ENCRYPTED FILE----- + - recipient: age16ymszt6hmv7p3w596w5wlzng7wgk6mwcchr8s2nvwutnx2nrzyqsvn678s + enc: | + -----BEGIN AGE ENCRYPTED FILE----- + YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBkSG4zMzAveXhFeWlBSlE5 + dWhYZlBMRGZiTVNrWCtUVzZKQUpxejJyNm1jCmdGdG9BTEtOend6UFc3WTJ1RzR4 + YnMrOU5vNjduMElVSEwxOWhnK1RCeUUKLS0tIDNCTXQ5WC94bkNIVDZhMWF4ZGs0 + U2dCSmF0cHd0VHl3Yi9sbFYzVHZYRDQKlNCaVjmRYRaZHihgWcfA4CwoN1xlAiNE + x/zYI3Ep6/vLKGkQpXVa2AbCEf7CiGeHKfoqo5cYeWzzPYZNtlDvCA== + -----END AGE ENCRYPTED FILE----- + - recipient: age12vnpyjwhnnm85vktfmg6jwzn55fcg0lmgn6q0wx2z4wawnwgm5cqt6yf2f + enc: | + -----BEGIN AGE ENCRYPTED FILE----- + YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBQTzRoZ1NjY1o4bHhobWVI + d1QxMXY1aW5jSGJ2L1JWcmpZUzNYQ2dFZGhVCkQxWGNXWTZZRFJtQXVFQnoybHVX + c1JWaW15eVJoZnJyOFdYaXlaV2dyMDQKLS0tIEV2eGZYaUpTc3lYb1RaamVoR2h3 + SnF0ckFGZmhIQk9rUXNON1BGbzJ2MmcKwqVShGTzXtz4gR9bwKhOWj3ZDyn6yB8T + fXXSKSKAyaYbOlmLgfGwhAMehd/uLmZQuZ9E3/TVIOX+nctx1pn0HA== + -----END AGE ENCRYPTED FILE----- + - recipient: age1ruav09tz635tt3sgq55pgn83qnte0j2nx0jd6gvjxelkqqdtygcqgcam2t + enc: | + -----BEGIN AGE ENCRYPTED FILE----- + YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBBQm95a3YvQ3V4KzlGaGVi + clFoY0tpRVFyb0F4RUVUR212Vm5aODZ5REc4CjN1TDArajEvWXBETUF1S0N5VFNU + L3RiTHNEQVF3SkMwL3BLbVdRZW5KV3cKLS0tIC9MK0hsenhjVFYyc0J0R2dheXFa + MVdiNTc1M3BGVi96ZFRyVDk2clJZSGsKG+jPH1qSr5Q3NpTJJk9cY+K74ZNjS16G + cSuyOCDpX9xjC5eW/nYjhvihndFEsrNbr2FTGGfIKxFhDTiCn17qpw== + -----END AGE ENCRYPTED FILE----- + - recipient: age1ph0gtrpvus5y2kl5t5wnmlcjpevavxf4l2aagrqyp7nng7jvluus959fvq + enc: | + -----BEGIN AGE ENCRYPTED FILE----- + YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSB1VFVZQXhGN203UTAvQU9l + SllwUitOUmZoS1RsRlQ5bjh6RXgvQlpiTHdnCi92RWptYm9UYk9qYkdTYjdoL0hl + TmU2VzNJTXZWclNXUk41bnFDRUhDWlEKLS0tIFZhRGVPQmNTQnJ3aCt4NHY1Ly9h + bWl6SlFwa2g3WlR1WThCVDlsSUNzT2sKBc00gZQMA0X5hoiXwR5FvgR824tip1iT + w0mIId0681l3iSl69eMZBQIffHnQ8nS0bEhYblf5Fw8ku3TTFcM8JQ== + -----END AGE ENCRYPTED FILE----- + - recipient: age1sy6t7kdeyf63mjnrdnqm08rjv2s5ddexgncuq4ps6z4c5hgg4dzqp6pznq + enc: | + -----BEGIN AGE ENCRYPTED FILE----- + YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBjOEtaNnhpWGVoNXFNa3Yz + NUhNa09UREFPM25neElhaUw4YzQyUXlwcnhRCjhKMVRBYVRaYXlueUg0b3ZpbDha + UERDU3crQ0IxSFd2THh0Q3B3cmtyVGMKLS0tIDVRa3lHc292Z0VDU1NwdVd0RDZV + Ky9uOVo0V1NnR0NFeHBiUit6TVZ5ejgKDXnZRZQJsDCvNI5zUmAqLXsFpfPg0dmt + tNg7T895K4KB0pfrCXxYwav9Y+lJMQRbtLUq1cU9W3fRPHRQyeuDfw== + -----END AGE ENCRYPTED FILE----- + - recipient: age1ruav09tz635tt3sgq55pgn83qnte0j2nx0jd6gvjxelkqqdtygcqgcam2t + enc: | + -----BEGIN AGE ENCRYPTED FILE----- + YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBKajNkZGtHUCtUaXRuYkhu + c1gxUmxCZ0VwUzhlWkV2YlhRZmI5NWN0SlNZCkFsRHdrT056TjQxYUJEeWlZNzZB + d1BoSVJhd1dRNEZFMElaY0VZenU5bGsKLS0tIFpOb201Z1Z6N2s0YktXNHF2TEZo + WHBBWUFFZitjSk1nMUd4Znl5QldaSVEKxUzQxRabwSsWF4NbL4KklM78nODR6pZh + 4/EcLSJccTX+g7qq7zlKtE2Kk5txMZdcVzCWb7I6ALPSGHzLqtQH2A== + -----END AGE ENCRYPTED FILE----- + - recipient: age1dwqnyurvm7vasf9n7alduzmg79nczkuafknr8x3l4jnzwnuzzydqj0y92p + enc: | + -----BEGIN AGE ENCRYPTED FILE----- + YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBrTWxNNE96M3NBeEhYNy9t + TGc5MmF6NTd5VXdCZzc3Q0dRT0FuNi9wV1ZBCmsreDM2VmJMYVQrMmludzNhdWQ1 + cTQydDNwSVBJdmh3emd0QURBcEpKZ0kKLS0tIHAvbE5MMnJpVWFkQ2tCNnlocGZw + SG9TYUlIYTY4VUpkeUovOWxoZ2pkUDQK0yRnv+Z721B51dP0D498YeA0IqRsdOYL + DtbbAGZWtskvFfRK7pE23p32vkbsM9LUo+5KP5NdLkVR8KupayRR3g== + -----END AGE ENCRYPTED FILE----- + - recipient: age1dx6u86w8d242tvjesz362caf4lcatw24ldd0hj9qn7xhqw0s0c5qus8wxt + enc: | + -----BEGIN AGE ENCRYPTED FILE----- + YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBSY3FsZnJvbEQ3K1REd2VY + VXAxOVdtZE94SU5zNlJ2SzN1ZzB1ZG1TOTFVCnlQa2pjNGdDeDcwMHNPSTk1czU1 + RWswaWlGSWlqR3h2Q1hyaDJOdks2N1UKLS0tIE1qTUE1SnhkSXdObTk0dTZiZWxI + Z1U3SXdEY1dURktHV1VtaW1QY21ONUkKrLHd7ZPB8wODa2tLvz7RPUqBd3Q1H02h + n8X2TZD5mZYZPZkxI+2mtHCHEn3LaUHaUZi8afJnhQnfsvTnIgZhHw== + -----END AGE ENCRYPTED FILE----- + - recipient: age1h0nv9lwkkhd9y0rlf832g3lualvjafqpyvlkgf8d0cn6c4zg959qkrfzt3 + enc: | + -----BEGIN AGE ENCRYPTED FILE----- + YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBzaE1scWZLYVJMTjVjUkJW + NWY0QjhZaWl0RWJidkJkc01HWG5FaVpuczBrCjhvRXVlSHVCT0p6Ukl1Tm04VERI + Z0d4TGtab0h3Q1l1MzVYSWpVOUtjUE0KLS0tIHl0d0pqamZoOERoa1FqR1I1VitC + aWZsa0NvZWorSmg5aDAwVkUvVVZGY0kKq2jIONa2nhOwAjMb8nxVVvEb20Sa9+ga + oJvP2ywIvV0fXp7bPvi3Gh52tItnFOcA5fklQVMSyN2i4S8HS8diUQ== + -----END AGE ENCRYPTED FILE----- + - recipient: age1d9h9mm3u5qalmpl2pf62pyzqj8t654n435emn93rutv0cg9sr32sg64fdj + enc: | + -----BEGIN AGE ENCRYPTED FILE----- + YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBGZEtNbXJHRTFudEZFSW9s + dm5CVFJTQU40TzNscHJRRElvblRzelcwYVQ0CjlYaDZIMitRSGw2SWpaM0V1Yk5n + ZktKY1JqUVUyM3lLNUZSUkZ6bTZCU0kKLS0tIGhkOEVxN3NQYk1ZZjJ1MGF3anlD + QTkzVkJ0ZE0wTFBrekpDRkJXQi8rSncKOpTuTnHKI8sXHyOpwZSov1X1EdI4HIIt + BGSxBv/ztZR9RaHFAWIwJQXE0q5rKe2CE2PjFLrA5k0aen/yPcxbeg== + -----END AGE ENCRYPTED FILE----- + - recipient: age1fm7zr0ea3d589tkgcz2klqgnajduzkr25e8tnhh7qxzuleqxq3yq3c0s3t + enc: | + -----BEGIN AGE ENCRYPTED FILE----- + YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSA1U0pPYm1YM0hteld0NGRn + SStzVTI5Wm9JSGhtWVlyUHRwek9jYmpmc2lFCllzMUpEUGhHTGZZT21wc2dBRTNR + c1R6SW1aenNuL0ZlL0NMSHE2aFFGZ28KLS0tIHVudWhuWXk2SWh0MFVOSDJaMjR2 + K1pKNkhjdjZlL1cwZ1p2bExsODBtRUUKdFmHherzXR6290QJmg8FXnRiKYWdIqyN + 3/hvSub1tiQpSggCaLm7GXiiKXMSjM1M0GZg6d5865ILDhhpd1/qVg== + -----END AGE ENCRYPTED FILE----- + - recipient: age16wuzuxnkcgfuxzvzgk5e5a5f6hhs386adjewyv54m9esr4yj6uuslpn6tp + enc: | + -----BEGIN AGE ENCRYPTED FILE----- + YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBtcW5XN2VQZEJxOWk2bDM5 + d1V1cjRJRVlkTXNPZmllV2pjOTNjU0JBZlc4CmVtTDZFV2pOWXpzMHM0aHJrMXdB + dm5yNEcyOGZZMCs5WUsrcjJnODNNSE0KLS0tIERtN3dSeUl5RXErcG1VZEdtWkNR + MGxSRE5Ycm1LTjMvaXllWUNUUTlXczAKa1GM4IbmT4YS0U/XR+FnDMVomMOnP4+n + b6YCuzWK+DmI86EuRLjq/p5gvizDbNSLShs7VM9PocB+GFKyFrE7oQ== + -----END AGE ENCRYPTED FILE----- + - recipient: age16rkvks3tljju3y6xu0l7luhjzx634et97g3xe58xf2dgfn2865rqkq6t8f + enc: | + -----BEGIN AGE ENCRYPTED FILE----- + YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBtZWdsUHBGUFZlRjcwTkR4 + Tkp1enZyb0V4VXpqS3RhMy94bDRXRTZMdFVJCk5RVXpHYmtmTDBiM2lkdEFaejNP + Vk5zUkRPN3F1QkNMOUtVVXNzd0JaNVEKLS0tIFovMmJZcllOK25LWDRWVUlCWXpC + TmphVEVvRW40REpoWW5XenY2dGo5YkEKDR6KQ5zG+Lp8deF9IK2oLI50oHPYB6YR + Xjenadu2RseZrRL57MUCIZjheqTm0E0WUB+fCX2sJ4aA51e3jb4lfA== + -----END AGE ENCRYPTED FILE----- + - recipient: age1yuh8vhakrwn2nm4dzxmdp99cmvl3cd4af36p5w2v559263a4uy4sulpn60 + enc: | + -----BEGIN AGE ENCRYPTED FILE----- + YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBZZkdiNnkrVXh5dnJKWHh4 + QlVrQ0RiSVlTanBoUjMyTXdwL0ZweFRMOXdvCmVvSnhxdTBSSndmSEMxa3pTcVZl + VEdGT09zYWQ3clk2dXVyY1J3Zk81SU0KLS0tIFQycUdMOURBZm5ZNGtweFJubk54 + dWtkdWNEbisvUlRLMWhRVlJEV2VYVHMKssbK4/2B5XehwiYNCRTtaby8cOsIOsqp + yFaipRCQ+NPy5gPbQxNJK3nfS4ZJU5GsMn/mFH2R9voJ99BiFyKDqw== + -----END AGE ENCRYPTED FILE----- + - recipient: age17dh936q0l622ez7m0zfak46awqdx35hldqzsfnh72cgtcthlhg4qdl74fh + enc: | + -----BEGIN AGE ENCRYPTED FILE----- + YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBLUU9udGpWd0wwWHRKZFlU + V3V2aEJRV29FbmNTY2FRSm9uU1RaZElnUmswClRIODBsd0pLRGZ5S0h1U3l6NU05 + MGJmOGlKT1o2Y0NZZUFCTzR4MHR4TjAKLS0tIDExWDFUZTNDQ285cUVaRnROb2Ew + ckg0eWF3ZU1Cd2lpd3FIcnFabFdjeDQKEK6+RfVfGhAblCR8clwF0nfbeywB5Vxy + BpbomVDix8FiJvGNX5PJDmeaPk3D86lqQBkBiN9WxPXdLrZl7Mw1XQ== + -----END AGE ENCRYPTED FILE----- + lastmodified: "2026-04-24T14:26:55Z" + mac: ENC[AES256_GCM,data:EyTu6ARdqi+Uz8PYtffa6KJyL6U5WodlGYuGXdoSCkakD0QmJ2wtqv8D0QKKdTGybcD2piwVMPsFcibZpA9wBlpgbTX2C4N8Qk0s4GjwHXDWyMPpBhY0u5qngBtzGUpOZglXkKO0BXkdzHN1oKI74cNPnQsjdkSIgdl5xoOppHs=,iv:I2jFgPWfw3vTTYwJtiln/iaZZpihFl2acxI8ftz6wC0=,tag:iYPDJx4s06/JNRXUZewyLQ==,type:str] + unencrypted_comment_regex: .* + version: 3.11.0 diff --git a/.stack/secrets/vars/dev.sops.yaml b/.stack/secrets/vars/dev.sops.yaml index 18c5a5dc..3f346445 100644 --- a/.stack/secrets/vars/dev.sops.yaml +++ b/.stack/secrets/vars/dev.sops.yaml @@ -7,155 +7,155 @@ sops: - recipient: age14vpdar7vzznyxgskp9772zjar95n8l2f36w6tzk980889t7kjdqsc5a50q enc: | -----BEGIN AGE ENCRYPTED FILE----- - YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBjOHYxWVk4U3c4dEtuZ0E4 - SW4rRzg5OEVUUkc3Qjh3OVNXbUtJV0Y3b0U4CkhBbWJXNFJZRGJKTWplcnlKUnBa - QlVGaVlndjdMR0gxSE1Vek0rNVJFQzgKLS0tIDYwWjU4NW9tZTcxM1RvVU5MRE1t - NDlUWkNKOG9ZUzE5SXc4LytuUUt2L2cKiQXHpeSX3xxP2ZgOfndyopfiGeEzrBYz - u86HaQmAZqOUd98lKJcmTWkKleje4CkRpvHRyK8msD2NKDcbyX+BMQ== + YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBDb2dxNWRPSVNKcE02ZUhx + QlFCck1nOFBUTzFxcGprUHY4K01MZ0pxbDM4Cnh5dmhucmFTN3hpUEZnVXkxUFNY + YXo4bmg4RFhxU1YxUTBvWGVDcHNiUlkKLS0tIE13WGtiV3kydUN6dHNPbEhmTk1K + MFNHVm1Yc3AyNS9MeWJqZTV0cmRRQUUKl3hTHZcIbMhm43OAguk57k9lVsSUxYcZ + GLkjmDamV+opgOWbyP53JCezdEPGMQ6f4G8eCfWKECsw6lfmrl+qNg== -----END AGE ENCRYPTED FILE----- - recipient: age1ugmyh9qcz05ehtkgnt2nn3jfz2rf2umnmqx69pgp2ue82dn7vpuqlc3g7v enc: | -----BEGIN AGE ENCRYPTED FILE----- - YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSA3ZWhkUVlIbTdWYVREV0hR - Ri9PSmtRSXkyMFNjdEluVkxnb3Rybk93WWdZCkVvSTVNOWp5cFM0dUpmZ2FWQmxY - blJKVVhNcXNzU24vL0d4VnlqeDJHRDAKLS0tIHFYdURBQ281Nm1rZGZuTFJJV1JZ - SjFob0llZEpPREh1TE90eC9ma25ud0UKbJgAXail/AdEbVsTWbllZ1kbI/bebvnA - bZWiWyBP29tuvY+G9kZCAwvHndrBBcYpmBqb9OuKCSTzZs7NaeqoPw== + YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSArOFZieEtiNkVQdjc3UGlK + SkcrWWRsWitTdHN0alhRRisrbGtyRmozQ1FRCjQyMzFMZFVLYnNTUER2Zy80ai90 + RitiRDZHRVJFMlQwWnYvR01RcG5STHMKLS0tIFpIWUROVHcyZ0dvTTBXUWtvZEg0 + Zmxja0phTlIrV3pybUxqTmdUUDNtM2cKd3sW1gOkNtNqCpb4PwGh0Tbm7wJT2o0x + 6ZbTy+INI8GZWqH7G0xlDMPQvLUhmmVOTBXGH6iB4A53j8V5OilByQ== -----END AGE ENCRYPTED FILE----- - recipient: age16ymszt6hmv7p3w596w5wlzng7wgk6mwcchr8s2nvwutnx2nrzyqsvn678s enc: | -----BEGIN AGE ENCRYPTED FILE----- - YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBnUnFMNEdHeStlMFliMEp5 - WndOZElUSG1qQ2VQUlFMdEdzVHpiYklNcTJNCjltclJIWDNheURYNkdvRE9yeENN - RW1Nd25LMlNnSHFIN0ZSOEVhNHlWZ3cKLS0tIERQMXU4bmowZU1JQjdWZWZLTmFr - aGwxbytzODEvcU5RZkJnR0ZwK2NpakkK3QeHPfx/Pm65oHuy/161QZ3X+q54dQ48 - 9Mlkm1YkS7kHVC1lhqfJLvM7nimEx0LmQH7+uwUbnSUg2FMe+KMjpA== + YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSAyTzNlUFlIemZ5NmphSU5s + MHBrMzdKYTlhc25HWnRJb1d4L0V1bjZXTkg0CmJ6R3FIVDBLQ2U3alk1dUdpUXhn + ZERSVUpvcWdodEJxYWNxcTdlVDdlRHMKLS0tIHpJaU9MalNKKy85Y216VUlQL1pO + Zkd3dENoZTI4d3lGWWlvOTVsa1pTR0kKPm8ta/6TEM7tkhWwiUDQGtq8ghjoVZRW + BmROQCIOiSB+qTMWshkZAc0pUq5JhZC36kdoWx13OxsoRTQYzD60xA== -----END AGE ENCRYPTED FILE----- - recipient: age12vnpyjwhnnm85vktfmg6jwzn55fcg0lmgn6q0wx2z4wawnwgm5cqt6yf2f enc: | -----BEGIN AGE ENCRYPTED FILE----- - YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBsZHpvOEh0RnFGYlpUc1hh - T21abVl2Vm1OcTRQYzAyZnpldjkrYW9qUFZrCnhKU1l5T1BTcUZvRDBsQ0hiUFBT - N3Qxc1lKdWc4Wm1TY3lHV2R4OXgrSk0KLS0tIHZuYlFwTjU0Z1Z2Z0s3VmdyMm4r - QUdQaFMzM2xxeXlaVjMvcWNpVnBMRlUKyThZPvmrGE8MoyD/ADbXmi3zyKE6+nrP - JLmtsTH12lDnKPKrmblAXHsggTCQvB7E3qlv1b++d/yEaztcMakVXg== + YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBHb1lpVlZMQ3pmV0tEeTBs + NkdKeVpBU1l1OWlndjRlV2MrdkcvN01ERVFrClE2dUM0NmlNSllyalp4b0V1NVIw + VXQ0ejhGUVRqY1ZKRXhVNlVmTmo5bUUKLS0tIHJYaFZxbWw3cEFMb3I3cU1Za2Qv + ak9NL1NLOStmQXdKM3ZCQU9sR1BBVGMKxPFNdvKAqcB9gPxtk2SuIW8wYDqNLnmh + 0XYjk5NdvgSgvsXYPvEq3an9GOgq/nnUq5wh9tCxEVkWRGcaXesTAw== -----END AGE ENCRYPTED FILE----- - recipient: age1ruav09tz635tt3sgq55pgn83qnte0j2nx0jd6gvjxelkqqdtygcqgcam2t enc: | -----BEGIN AGE ENCRYPTED FILE----- - YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSAxTzZPOHdKN2RaTnduK1M0 - bjB1VHU3Sm9EZWdPUS9jVFp1TVBXU3VlSVNjCjk1ZWRMVXF2b0pQaHErMWpBbTly - ZE95Z2pkNGhoQWpWR2V1c0RjMTl6blkKLS0tICtRUlhlelpvd1hBM1Z4WWRjOFBh - Rm51ZDNZa25xR1hGWlZBenVQNTZBdU0KRbFgOND373NBHWvnOMiNPvCsN0qeQVyz - Fb1/O2ynZuK27qEBOoLqH0uhc/3E2f7NJ7SYfpHcPYrUy5PPzahlKA== + YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBzOWhKRWNNbjFsd0tYeXV4 + TUdJNTBUa3pycTFsZFpjblprTjVqaFhBTmtvCmRjNHZ1M0htV1BZYlhBUE4rZGRB + TFN5T1lzOGpsRkE3bmw1Sm43Z2RJMUEKLS0tIG4vOEFNUjFDcjBNeUYvREtyMEc3 + L3VLek5QNTYyWGxla1ZmeXNLQWFMeFUKjhGE4fj1S10eUm+R/kTY50cYK7C4B070 + PLjTS33/illFuj6PeMf6nIiyUG/0RIw+AEJdheqCYNE8C/4ycJ1K6w== -----END AGE ENCRYPTED FILE----- - recipient: age1ph0gtrpvus5y2kl5t5wnmlcjpevavxf4l2aagrqyp7nng7jvluus959fvq enc: | -----BEGIN AGE ENCRYPTED FILE----- - YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSB0cW1GOW9iV2NiQkJGSW1s - Um5BSTlTSWd6SDhCWkhkZXZ5akJIME5NQlQ4CmZCVldSTG16WHBEZEs4Sm51eUk2 - ZWdObHFNNUxFekx3VW9TTjRoL21ld00KLS0tIE5tZ3hjVFVwZkI2cmNpa0lZZTZQ - Qm1mdHN4dGJ4bGFZa3RKT3I2ZWZJcGMKhSia8Td1ypbJiZlyi0Cz/tmC7XDTcOgB - r7wcSagCcRX0iXM+G5FdmbL/UDyTNmP3oIz1Rp9V/w//9Ex5lTcaEg== + YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBwbEJ4ZjF4Tis1T2ZsNGNS + SHMwY1laT3I1b1RtdlM1Y1p5d1FXYjhjMkFvCisyUmVGUVJqWjNhQjJDcDV6VnJO + RERLbkhBUE02ZjJtWlBHcGxzMEhQTW8KLS0tIDU2UTNoTmh2Y05Hbk5NSy81emM0 + bWhzbmwwQjYzeFllNCs2Y0UzZTdzMncKL+HZNrO7enr9xYQRfFl2L5VjEZirsRFg + GCj2goRj4yoUOPrE+Xb/Ad3IcZpxSfT1+Uvw2L1SgLHw62RnrualUw== -----END AGE ENCRYPTED FILE----- - recipient: age1sy6t7kdeyf63mjnrdnqm08rjv2s5ddexgncuq4ps6z4c5hgg4dzqp6pznq enc: | -----BEGIN AGE ENCRYPTED FILE----- - YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBMaXZSOFhWOUFCUCt1MTVK - MlRBSDdZVVQ4akpwdHp1UU1LS3l6VjRWM0hVCkVwTFRiazBmNEp0MEdhUDF3UXNC - dXVTWkY0NTh1ZmNsRzIxSVdzV2E4bEUKLS0tIGE5NlZBMG1JaGpKMThpUGRqWVRa - TDJTeVcxN3YzZHNWcU83em9mVWFPU1EKKBvr+Rfgbd3Wd5paMqzu6YnN6gqeEpxt - y1qPGaQMwrG4nGKWCUZtUGLE8oNeags+LNw8V/L36qVwEs3JY0aWTw== + YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBqS2F0akdRRTJWUFh2QzQx + M3QxZ3FvR3hqd292TkUrbFAxUGtrNUk1Q2lnCm16dDJ6dVRXczYzVUR4WS92YzFm + anRUQmJMa1hCZjZKUEFsWVErU0orWjAKLS0tIHVYaFo4RGJSYnY5QThmcWthSGtK + WG9paTI4Y013aWJOUGVHR2YwNmJCR0UKTQEHr9sOXQZD2xQ9VgX4A00xsJjt6qzy + s9000fDZ1snZ03m1/SRWjRdgrUvbtsVtA25RnVtPvRqhUSQSu7uMWg== -----END AGE ENCRYPTED FILE----- - recipient: age1ruav09tz635tt3sgq55pgn83qnte0j2nx0jd6gvjxelkqqdtygcqgcam2t enc: | -----BEGIN AGE ENCRYPTED FILE----- - YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBWYmlNSzF1SjNSUUxUY2Zo - amxyUXFMRFk3K3duWStPLzNyZGt4bFYybDE4CmFpaDlod2had3U0eFM5YXFsbThx - dFBwQWRaM0w2UHRLZFRxU3p1SzRsZFkKLS0tIGdyV1Y4N09xY1ZFeW8yZGlvZ0xm - QkRjNEhYZkptMWN6SWlKMHFZM253encKSA31enORoMOEUbaFVvlzS/C7yfQKvEkk - P1BkUKe2eCaFT7wqbsytwfnxMigcf/2CySd4/qUvr6gVdSd7CyW6Yw== + YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBGajRCZkZ0d0F1clVyUS9z + NmJkeGtVV3ZSb0hHVGtsQkZUT3lXWm1ENTFFCjVRQkd3c0hNU1JSMWc1SVFrS0Rx + K3NBSWEvRkE3cnFYd2xBMTlXc05SWGcKLS0tIDNqMzJGZlgwUzJDdEhPL2FlMnl5 + Sm0yd3NLRy9qMlYxb0lncTREUXNnWW8KaKXGK2nYPkHQT8SFkVlT90mJFbcK0mSy + Co3E3GS/rfqCmnNr7nLlaBx1jXfrTihNUZPpr89/qh9bBPWDaCbhFQ== -----END AGE ENCRYPTED FILE----- - recipient: age1dwqnyurvm7vasf9n7alduzmg79nczkuafknr8x3l4jnzwnuzzydqj0y92p enc: | -----BEGIN AGE ENCRYPTED FILE----- - YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBidm5QSUxQWDk2Y1hsa0hl - R2d1QUpIU01yMWdzWDFyOWlQR3p0bm41aGg0Cnp0U20zbHpabFFlcTdFVzZhdEhG - aUxFSUZxNzloSzdFNjJES2g5M2pUbUEKLS0tIDluL3MzMFJ6c2tnN2tjR2tSR2dx - MlF0aC9lRVIza1FBRW4yU1pYdkJPWm8KzkBtGeu/t1FS75XuA9bSh0gK5OAOcOEU - StIZnsIP6+sV7EA50F+3IDwR4VI47vN3uUy4HD9n4akOOc0GW77taw== + YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBYVDlLZzVQTURwa0wwaWg5 + SlpwTWRxdWxFSWVkenV3aVU0Z1FLZ2ZyS3pJCm81RVVhNE9OM0hUakxubWwrdVUy + WldjdmpQY1lZc2N5OFFNckorWWNtYjAKLS0tIFNyTDRrZC9HTEVGS2g5Z0Z5cmIv + TXY3V0xwRXk1ZUVpeWgrcHBQRTJNUHMKWKJkpRMgmWDjZuaNDhzvlQyegbbm/k/5 + I38M2TI+tGgcSpoKWfPeFTfnxUzT+V0iAP/JGkPDwVlpaSoSXw5E5g== -----END AGE ENCRYPTED FILE----- - recipient: age1dx6u86w8d242tvjesz362caf4lcatw24ldd0hj9qn7xhqw0s0c5qus8wxt enc: | -----BEGIN AGE ENCRYPTED FILE----- - YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBLalhIQzhQSU9FNkFMcGFH - dHhKdEtVbnprVG1yaGJqbHZQQnVuUmREaWxVCm9FODJ0L3lvUTBIczRIM2crWkdk - M1lUV3BLbENXUTVZODVaOGd2Nk8yRnMKLS0tICszM3N4UEk2M3VNTDJxQW13bVJR - SjZzbmZVRjh3cEJYNFFDZWw3OUE3VEUKElT3vyLB6VHsmUrUBlV5lIjVlt7VpDGM - ixYqfjQ6gV8RkSsHo0lq5FaFOJiPfb/P8uxlMU9uVTyfBJsmmU7zeQ== + YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBmTERrQTdoSkwvTmpoU0lq + MWVtZGNFcWlKOEk3TW12Nmh1azA0Z3AxY1c0CitOZWxZT3dPSzErVFNrMEREQWQy + dzZWc0JqaVpqUHZNdmJxS2x5dDlGK1UKLS0tIGtRanA5V2lOMXAvanExU1h1a1cr + RGMvZkRNditCblUwTXN4bjdhaHdhdXMKboLfOsBoyielS/T5uFsrp2+xsxymW2lZ + zIG6DMyE+p5J5wHycmdpSNWqmkw8eaiu129QK0gMmEEr+Zg4RTVK4w== -----END AGE ENCRYPTED FILE----- - recipient: age1h0nv9lwkkhd9y0rlf832g3lualvjafqpyvlkgf8d0cn6c4zg959qkrfzt3 enc: | -----BEGIN AGE ENCRYPTED FILE----- - YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBEN25iVDJqZGx4V0l6aUdM - bVJGMFE1SHBURCtQbGhCVCtTV2Z5b1RqVUg0CnlpVCtRMnA4YktDMzJYRldpWDRZ - K0F4dVVvM0RXWlVCZDNaQzlsNEp4TU0KLS0tIENhc0E2czhrRGs3WlZJT3FrYVBO - SndLM0R3Y3VDZEl2T2VzRjBGd2tHeEUKDAhCGtxL4ptpX/GrnCJSd0FetsytFc0s - mWRl7IVnT9piwuPsaYk8Y883QP6zBFIu6eTUfYHcIfAVKdHVq9T8Rg== + YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBrWWdlcTdpS2xGNWZDZmNB + aldkZ1AwTTNoUXUrSG91akFNM1V5U2k2U1hZCkhGM2ZjaFlzRjd1NmtFbHFSaHZM + eHMvQVZ2enlLU1NyQW1pNmJ0MHZrVmMKLS0tIEMxUWFuZzRaTFlkdkV3YlAvU3JF + SEJPT0Q0VUlJcGZpa0Q3aWVyUEszWE0KUoS1/XPmq+gT0r9eAEvcL0U3hctQQD5N + uTg7FIZvGfSGAJLPc+aoPk8z38aOmoPjxbuPnM3jpJcgJjHYsBAlRQ== -----END AGE ENCRYPTED FILE----- - - recipient: age1eqcj2g0fdekj2wpqp4y0fg9c5myydjdt9zlr5scr0grk6fxszymqkpw5jf + - recipient: age1d9h9mm3u5qalmpl2pf62pyzqj8t654n435emn93rutv0cg9sr32sg64fdj enc: | -----BEGIN AGE ENCRYPTED FILE----- - YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBwaFhRSisxdnBXWGhRVTdt - QUliVWhNWTRUV2ZBOWp2QXpxTGQrRUFIMEVrCnBqeDhTdnVvRWYzOE1wQkF1WXZa - ZVBHSWxzeFZTSTRCVi9mV053VWxlYk0KLS0tICtGOGhMNTNiSHBBNDNZMGRtWHdF - WUtTSmF5OWxDSll1RWJPb3hKTWtOTGMKN4Wq2qdkdoCMB/onycWSUrfjMYOEOv7i - rV+7YbXb0/4f0B09UvhbYwvhJfzv4FUAdZB05qdSoywDhp4/ANFypw== + YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBNelJsck5oekNMK095Nnlk + dE5CenNQQmpNaG1OSWNiWU82NXBTYVUrRm1BCjdUQTZVeUdCT2RyaGFlbUkyWHZw + UUgvNzlEempkeVU0TFYwOGhKb1lkL1kKLS0tIE01ZERHRzhXWElpZ1dyOGZoeGRp + TGdBNERMN0NVRGs5V3ZadzJ6RGpjQm8K9tTYxUtwMAtg+jAmfkpNlxnk4a1o5gIg + MVjNymRwiK45oZ6h0Kgj+BNu0D5xjAxn8oEPXshkkOUqhbol9fB5HQ== -----END AGE ENCRYPTED FILE----- - recipient: age1fm7zr0ea3d589tkgcz2klqgnajduzkr25e8tnhh7qxzuleqxq3yq3c0s3t enc: | -----BEGIN AGE ENCRYPTED FILE----- - YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSAzM1VhZFNZY3dENE42My9p - N1lOMVd2dTdlK0ttdDlyVDdkcTF4VlpOYWprCkNCRUs0YmF5MlM0RXhVN2dYT2lo - aVRoV2RpUkphRkJNNmg2cnRJR09sMFkKLS0tIHdQMTVQY1FHQlV6Ly94NkZ5Qkw3 - eDZSVHU5a21ucVpIVXRaN0wvdXBBVzQK2Ck6IftFau9XKgBLXWDg6cCI4/yz8ak/ - vC/Qpe9cCextW7nK6ZOehJrtJjq6DKwd4X7YPN3f94xKTQkm0ZoLDA== + YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSAyZVhIY2tEdllGamtXOVJ2 + ajFMSnA4cUtDS1pWaytYa3JVbXlYalh5ZjFjCmhZeTFIN0xseVhmbDVDdEJhb1By + QkFpVlJtZDhla2VLU2Fvd3doVjFXYUEKLS0tIFdFY0pWamtSbjQ0aVk2ZGlxczhM + b3BzRG80R3ozRElyZ1JjOVhzTkxoMWMKDzI/fCsIn6VdMqIrH8vS1TMxx7sq0HGv + FTHMExoCkBjZLUmIRJa2F0Bfzf1bCiUOa+FiVQNYvCiwodYZGsiCzg== -----END AGE ENCRYPTED FILE----- - recipient: age16wuzuxnkcgfuxzvzgk5e5a5f6hhs386adjewyv54m9esr4yj6uuslpn6tp enc: | -----BEGIN AGE ENCRYPTED FILE----- - YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBwWlFackFxcG8xVzlqc2tI - d1JESVh1b013RjRQRmJONDZybFRyUExVaVZzCnFrVnZPemJNdGV0cDJCNEwwMVJQ - MTVvRXdsVmpnVmY2d2p3MDBRMlRETzQKLS0tIDhqem9VVXB0cko1OC9BUVZldWhF - cVFTdDVPTVdwYi9HRVgrSzlPbDRtOVkKuwfbZiMuD0Zpi6FCEGZmV86ftS2DZycs - aC/L7fIyqZ/AbM/VNvzq6d6MkSljZoUeoRx8k774iV3ZTLakQnuy8A== + YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBxM2NqVnNwYlI1MnlSbHc3 + TWxGN1dRWHZSb1B3UjFPZS9jbnFLb1FnZG1RCmxmRzFmbGZuaW5HV1dYWE8zY0xH + OXdhblU2bE5vZ2hJMjR3Z0xXRjJiZ1EKLS0tIGJNdlNvNjVKdi9CZGdqWEpBdDRW + N2QyYTBLY3BlRzdVRHE2MDNvcjFCN1UKvMW0Qi5Sm3kUeAvb52MfeTYiPBww8KAQ + IXQFMtyQM7a+2sTg9WOn0ZvwlbPb7fxT0vzX3o3mXb2wZc+fyjMtEQ== -----END AGE ENCRYPTED FILE----- - recipient: age16rkvks3tljju3y6xu0l7luhjzx634et97g3xe58xf2dgfn2865rqkq6t8f enc: | -----BEGIN AGE ENCRYPTED FILE----- - YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBqVzBpa25tRHhpUC9tcm1Z - dHA3TUE2SVNyLzQxUjAvWEZhcjRVaHlMRlI0CnZVRDZhRFVDNXJsdXlzbFdjRDhi - UmJjVjcrR0lqVEhkc3FjR3k1K1hBVmsKLS0tIGZMZlY1K3ZaSyt6MU82bVViOXBG - cmd6SkZjeHN0T2dUQm9mR2JRc04wNmcK1JV1E4Ceh0keFe27jV5ihcZVdaSzYv7Q - k7w0FlIwWM8qInzs8FM+B5qBRcpxhNTmWmAkrxSkCZsI5R0EQ3TopQ== + YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBjUWkvMWs1QytVRGdzcGlZ + RkNFNmtXcndBSHVxSFhFTUdXY1FnMXFra25ZClpyK1o2am9pbG1LN3kvR1FsMmxI + bzQ1MlBYVDZoN25TSjRURzdEeFpreUUKLS0tIDBHZUVSNEdxdzNTNnFzN3V2L2hk + UkVlMEcyam4wazRzTUtRZHFxUjlDOFkK+1ka4vQTrvsvBMWGU2JF9nTlBUHUBC/5 + 7/oR75boG/GbQ3cUxZoHfQehQEvjs7um2KRuBP9fF2QPuWZCWAlpgg== -----END AGE ENCRYPTED FILE----- - recipient: age1yuh8vhakrwn2nm4dzxmdp99cmvl3cd4af36p5w2v559263a4uy4sulpn60 enc: | -----BEGIN AGE ENCRYPTED FILE----- - YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSB4VlJDZStESjM1VVh4RWtM - UHBTKy9ZNFN2S1VLNHRJYW0xUXRIL1hoRm1VCjJEb2M2bE1ZVldHaFA2TTBTU1lH - Z0VwbGhRV21pV0FEU1dDTkQ2YVo3bFUKLS0tIFJLek1QUFJsSmZUYVNlZ2JJQzZE - elZqTFcxSFJmeHJkK1VKb0taNVErK0kKaDDM+tWzx5uLLnWGL+EwIAYTWDEZzDCc - gNO68ireRxrukhnoLhqH+dFiTzk+zhi7RjPqjs4dnCiIgD2hnOTCWw== + YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSA3b1hzVUk5YmNoSHFLeUpv + Qy9GckcydXJNUmUrV3BnaENZWEVHekdrOUR3CksxRFFDd1N5S0dTZ2dhUGphNS93 + THdTOWVZRjUrOU1hTGxmVEpHVFdwN3MKLS0tIEdxek0vbEREb2FEYzkydEpwelhP + QWNkNWJ0UGpCSDJtcE4veHJDdzhUSVEKRscUX/tncYsK0LlWX60/AbctxREbvDq9 + sBB9dKRWOpLp4Taf8jwD2o4A3C1aQnHhkBIvFf8UK46D4nEgRFrPKQ== -----END AGE ENCRYPTED FILE----- - recipient: age17dh936q0l622ez7m0zfak46awqdx35hldqzsfnh72cgtcthlhg4qdl74fh enc: | -----BEGIN AGE ENCRYPTED FILE----- - YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBqYVFxenFsLy82SDJiWkNO - Q09FS3AzdnQ0Y085Y2NxTjJiaGlsTGtHZ0NjCndiaXFGNUtpMDRESVBFeDJicExJ - cnIycStvWDBnTTJ2bTJQb1FtU2tSSjAKLS0tIFNtd3YxWnFHMU9OaU0zNEI4b0k5 - bkJ0bUVmN3o0OXV2NDlHVlBNVHQ3NzAKtbukA5CsIzav1+T9VqAZV2dMMntRiFIb - lqfv7MKRlPcl2uhrSTJKPMX+L9eijxjPJYLqQ8kIplfRCbW1rDULTg== + YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSAvQzVXM0xVTFkvMU9zcE9o + bjMxeEs3RGVwYjhodDZPZzA0T3BIMFVJeGt3Cm1IRDdRUmIzeXNWZFcrSmx6K3dm + b1BoQVpmYkVDblF5RnczU2svMWs3czQKLS0tIElhVEFhWDBVenNvZTM0Ujc0b3Nw + bWZhTjZ4ZVN0QkxMSnQxdkFqMWxBUzAKXQXvCjzWT11LnJjD8ppAp4es0p/zKnDk + BCxv+gDMUKaNqihFs/G2I1oRyG0JygO0aQXjSWZTS6tjUK9AELriLg== -----END AGE ENCRYPTED FILE----- lastmodified: "2026-04-17T13:05:56Z" mac: ENC[AES256_GCM,data:G+W4axy7x2A3zc2Buujy+n3C2Y91d0fIB7KnkOQRioR4tHS1ySyo3wGyDGvUlyp7sgQL0IBmUSfIorBfPmV8UTc0Ynka3KYsQ7rr5vcdTQbSuM8f6y3goGxPmUerwDoiPuztMCAzj8ln8AH5Yof1ByzWtq6BnhSCYVfI40i2tvI=,iv:iQ7/SL1K6aibPScRVIgA3C70SjaMqiy4wWFUXG8uVyA=,tag:xHMn/i6uhrj4EJaWJjmqlg==,type:str] diff --git a/.stack/secrets/vars/shared.sops.yaml b/.stack/secrets/vars/shared.sops.yaml index da686eca..f20035eb 100644 --- a/.stack/secrets/vars/shared.sops.yaml +++ b/.stack/secrets/vars/shared.sops.yaml @@ -1,168 +1,173 @@ aws_sandbox_access_key_id: ENC[AES256_GCM,data:XQ5ah1vlCnyAK//KKetbyIOjVh4=,iv:R2li+GSiupSSx54YhFguOHJ1erudiY8R1apx/zAL8og=,tag:ks73C8BH5kB+zbrIiyW5bg==,type:str] aws_sandbox_secret_access_key: ENC[AES256_GCM,data:1tNl/QjhrrEVMQEoQbppYzx5Pc/QY7Sjq7v/RmWphajz5iCH917vlw==,iv:zHJbjoqEbXyQrTcxRBHFfTdw/AFcMRXtsv72j7yqQao=,tag:kAmZ0W6q+14NrNPEWXxhaA==,type:str] cloudflare_account_id: ENC[AES256_GCM,data:vxEgNOP76f4l7bWsDVAhNJT1McaRqnGhy3wSFBj6+rE=,iv:lISK8zjDiKE9koXB7sRvcfw+GhvHmB3rtIPq7s8dbik=,tag:U/R9sSahq7etfN2lxRgsbg==,type:str] -cloudflare_api_token: ENC[AES256_GCM,data:erNh8YJHuomJGjw/cQPa/qlzM6edU0pVlud+l1OLVhimt0vmAym5FMMk3NBp2vH8gfOTmwA=,iv:k9Bjb2A57tWhY1xK6Kqcm/6BgI1XBmpiJq4UKaZj7jQ=,tag:Jx02CtoZtoXz47qe1gb2Ig==,type:str] +cloudflare_api_token: ENC[AES256_GCM,data:vOJRkSxa0gl/L+rZWZ8rUQK5qGCXJmG5m/49/RkbcYTlkvMZMY/E98VL+tEljBF7QwLL/F4=,iv:MTOSLclyymt7oMPTEg1f85PYzk0Fh2/Kn6/lfzwkRy8=,tag:c5rKylqVmddUbEbhiU0Mww==,type:str] cloudflare_service_account_client_id: ENC[AES256_GCM,data:QSNncrl1/MfzMxScqbPJcT/ZjoW8UIWYFrYRnyZUnbOww1P1f4Hk,iv:QUtnnykjedEOBnTD7EMPs2CDYAMhSFo4Q8Nivxoe0O0=,tag:zHggRUmtTpzQJ8bwc/PU1A==,type:str] cloudflare_service_account_client_secret: ENC[AES256_GCM,data:4xh96UpRRQdx2FKNbK4dDiznLULJaE/K/BleqTRPNqiLLl1r/zaXNaWH+yPSkjwH1kE2nxr5c2TlsUbvFHttwQ==,iv:r9BXpDp8J28QgkTnceCBxmD+bocDdLdmlowbjsJswbc=,tag:/TAr4Ad7rXrBsvjDN0eGYQ==,type:str] hetzner_api_key: ENC[AES256_GCM,data:Pc8M0fTCCpVAkYZF+x6JEAikjwQ1UQeKvMB/8zm2hHy3+GCaoYU6CTJmxFNaGL/eKc0jZ4XquDNJjB3mTO9tWA==,iv:8gxK4vALWK8Ts8CU8mCXVNN/1pJOtOoNfASXRjLlI+o=,tag:KNEuaYIDOCA3q9KcXmfJqA==,type:str] neon_api_token: ENC[AES256_GCM,data:NNSce3MnmZl3TQRLrGoeOSD0D9Mu7DjwAQ1PBxg5Nm3ecwYvL+xnS7chuPIDSDD1+oXtOire/tTP0HiCoE8LiCEtdY2q,iv:cQuA1xFwItbUkIeYRfMqoa2W350oEtvn1w7c9XxcXeY=,tag:YHpzdtGv+ur2xQq6/rNaEw==,type:str] neon_api_key: ENC[AES256_GCM,data:V91MLYMH4JPR10ygNpSC7zBT9tBEhjvHvfg08pE65LXvrHE8j1xfwFqeNCYuMXvB+iXluaa2lSYZF0SJi4GnGfhua8xc,iv:1tfVSnOaEL9RNYs++rlhplOBqQBThIxo1Ufi7wlgDMQ=,tag:qCZQuCHVERkR15L/wRxMzg==,type:str] +better_auth_secret: ENC[AES256_GCM,data:SV4nbYx+jtkn50nOD2yKXFdfdqhRyNGOdsFOIc08N69G95JwAL0R36JwNAE=,iv:brpVlFfi7k79ynkL0DvzrcfEZXD2gjAzWFa19U/41HY=,tag:JVzGA7Y+zyvDhDdvcHuI3g==,type:str] +polar_webhook_secret: ENC[AES256_GCM,data:jD4goZkvipC6e31tlqRHRSVOnZF9NMWfJJx/zjLfV+jUiQ==,iv:m03yhodIzuKQdK8pNYiEULTVkiVYq84trqBPf9tpxBs=,tag:MkjThhFmVg+lygf2SAgmcw==,type:str] +polar_pro_product_id_production: ENC[AES256_GCM,data:V4KLAMQ0CqO9BIGMTH9AzYo9sa0h6Hw=,iv:HgclsKnzDKHVJqskN1VpjDskcAWWZu6gDRLIE8yARNU=,tag:5+dcxON7dofbKL1NX0qwug==,type:str] +polar_free_product_id_production: ENC[AES256_GCM,data:TnUDpM2MNAcUybcjdKU/1SPDRPugh9ue,iv:fwlByRKzh/qJCohH+7cJgQTjP+LuaL2JFK2Pm9emnd0=,tag:kSkgY03C410pNN2FYI0rBg==,type:str] +polar_access_token: ENC[AES256_GCM,data:MY1LQCF8t2DJhSTFSBsNKnG0rsMm6rVDBquAC/W7EzkwCmXZYV4=,iv:i0wE+ZX0LGS0AX2Jy2E7N8KZqKpOe09dgDRCl5xqb3E=,tag:eDrt+poym3MlbSYj4luiEw==,type:str] sops: age: - recipient: age14vpdar7vzznyxgskp9772zjar95n8l2f36w6tzk980889t7kjdqsc5a50q enc: | -----BEGIN AGE ENCRYPTED FILE----- - YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBLUVFUS2FzZDRWNklrd1BI - NWN4bEQ1NVlEcmtJV0p3Tmhsc0lOT0tldnlrCitoenR4a3FtV1hmR0IwMU5PL1lJ - V2FoZEZ3dHlRa3I2QUwzeWRhOU56cU0KLS0tIFovSC92ZlJPbURSNW52VDZoUGJY - Y1UwNjlOUC9Da2xjV0g1ZllLOEIyeHcKeh3TSDX6fVB2Z/1FBdaR10c36YnLtG5O - CX4bOO/VS/2ue0ahQD2dKXAcWXHXjPauV/KOkvs6gHo+5kLA83X3xg== + YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBOaDhnMTEwMHU0ZHVqUGt4 + QzM2a0luWFk0RE9QVnE5L1VqTEFEMVRyNHowCndOa2NsQnFrMkRUc284blRQMXh6 + VnpMbkwyWnA3cXMxdU1mMk9leTZTSDAKLS0tIHRIMUVJOThjRlJlOHVsdzQ3N0hO + Q0RkNGQ2OEFrQ0hkVUtlT0xiWmRxVmcKIY7GQLtue0vQEuCqmrr+8CS4+C8byK/J + exL01lY/mMrTtkg4sWJm/pVVhZbgMlrm2NLICMs1JeTjZCs6KSl1cg== -----END AGE ENCRYPTED FILE----- - recipient: age1ugmyh9qcz05ehtkgnt2nn3jfz2rf2umnmqx69pgp2ue82dn7vpuqlc3g7v enc: | -----BEGIN AGE ENCRYPTED FILE----- - YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSAyaDJnYnVPL1JHU3VmN2kv - MURIY1V5YUx1V1BZWU9FZGhwVnRNWGxHNVdnClFjcGZVNFJIZnBZTE5UMmRKdFNN - Nm9ndmdtRzJhT043QjZtOE9NREpDdVEKLS0tIEJFcDR1Z3ZYZzdwVXdMQ081ZjVW - alB4QWhFSU5lODM3eTIxblE3a0o1UkEKDplnJMpuMJz8HR+SMdXXjZMy7LXBPBay - lLmDzn9t65X3jsC7HuHy7f/TbqszkZZJC7+nkZhUZvu245ALm/Dr6w== + YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBzS05venFwTWxjd3l5Y3Av + WUgvV3RaOEk5aWhBdXQrT2dQMnJCRXdJNXdzCjNnZHdjSmJkWkFsT0poL3l4dnZu + OWZSYW1IZ3AwbW1kVnpOM2tnZG4yeWcKLS0tIEI3SnN4aTc1VWVSbU1nMVppSUUx + WmhLOTVFOXcyUDN2TXpRaWppdVdkcm8KnyeSl+dhr0v6Ecp7i1sJFOvz3lQD/jJd + NsVqSjKumXpHTDL4GzGBJAUHeBkn77c9GlsulAPsZVIsi/8lICP8+w== -----END AGE ENCRYPTED FILE----- - recipient: age16ymszt6hmv7p3w596w5wlzng7wgk6mwcchr8s2nvwutnx2nrzyqsvn678s enc: | -----BEGIN AGE ENCRYPTED FILE----- - YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBLeFBpWm8rTXRJM1JpS0Rw - M3lvcm9uZFhEQ1hSU25hNzFQTW5LazNZMngwClF0R1lYWEtMbHJUMUV1Rk8wUzdS - Z2hkY3FDT3MrM2EzU0FROGdZL0E0WW8KLS0tIE1naUpzUlNveDZ4MXQ5aFpKS1lp - WWFudzk5Z2ZTeUY3b3h2OVlZYXRoNTAKj+a6sZ5bgXGIulz+rSk2t7vy2skDyVqQ - glsP+xQEyfT9fyljTn3V+9LXcPWQvqMb6MrCbIxI+paeXB8Nx9ApMA== + YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBCNG1HZjJjOHMxeWh0bVQ2 + emlBWUVWV3I1QmpVUFFHOEhDNllvQ202RXg4CjFXVStLcS9mK1EzTDd5VTlhUzRP + NkNNQjhWMlZmc3YxT3Y4aS9STVVsU1UKLS0tIGJWSmdYazZFNFRpQ1AxcitPbk14 + dEo0QmRWNXNkNFN3eE81ODk0elViWEUKfwNC4pk26x3YYNvLH3UhkMimA/mz6IaS + MTrdlo8JKv2nhXgWjIYdd/XcNkuWKyYZmyxTXvCmLKPG/VTq6iLWYA== -----END AGE ENCRYPTED FILE----- - recipient: age12vnpyjwhnnm85vktfmg6jwzn55fcg0lmgn6q0wx2z4wawnwgm5cqt6yf2f enc: | -----BEGIN AGE ENCRYPTED FILE----- - YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBaZXo4MGhCeGZaNFBzZ2VV - SWEwNGdIVVcxSG43T2toSXQzV2kxRkZsbzFZCkQwb3hXY1M2N0k4Z0EvUy9wSHZJ - aGxnODdSTzlSdXpZbDMxdFpWb3g3YzQKLS0tIFN2UlVGbzBRRklyVXBsWHB5a1Fj - WjM5UGtGdHl3Vk94YlAydkwwRDlocFUKlbtEScLYymFJjO3WoofZf0d1nILytZix - 2lthZSEWxdJ0NPNo5yo3xGsRCZyAMrXA9Aaj+ANdhPLPTnrRLHrR7g== + YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBPcXF0a3A0alZ6K1VDaWlE + MnkrNjN3UVhiRkkxcVBMdGhyaGFCL3JNcmxZCjZsWnNQZTZ6cE9NdlBXa1VHTW84 + WFk4TVRZcUt5L3gwd0ZUaE9ZSXBtaHMKLS0tIExPRWh5NEJnalhJNWxjeUVtK25p + K1dCd0RqVW4vSDBOaUhvVXZvd1ZQWlUKq/0UzwdTp3egprgwp80uCZJ/GPgCT8qV + GRU74vq504Ay2cbd9kdPvjEgLaTVMmkYNldtleM5ryluqkIDCijmUw== -----END AGE ENCRYPTED FILE----- - recipient: age1ruav09tz635tt3sgq55pgn83qnte0j2nx0jd6gvjxelkqqdtygcqgcam2t enc: | -----BEGIN AGE ENCRYPTED FILE----- - YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBYbDBxQkF4cjlqTTRmdFk5 - S3Rjc0p1Y0krK0JNenQxQ1FiMUV4Y08yV21VClhYYkE0Y2EvTFdlMVEzMkZCVkhp - MUFOWlBlbjBOT2pQS0YrbkFNbTZCdUEKLS0tIEw5cXR5RGZLdmVIVml6dmV3MTFF - RElJRGEyWjdJbkNrSXUyQW1TTHRFRE0KC7zvfrtWBCmnIXhmCcIEI7UGbJtkAFAP - v7gAmkS85x5k6zCadySAxeWruKVWtgxGXtzG/ecRVhuQqsHD7YNYHQ== + YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBxSk45L3lWV0Z5MnVkdkd2 + QVhjNmxTcVZ1NlVlTUpNdDIzemppdUtkeFNjCjdJb3VpaFJCMmdnRFI4T1hJMTFw + dTVkT3pJUkJIdEt6ZThPVVpnVTdtTHMKLS0tIHVqNVhpM0YrRGdZY2hzK1Z6SGZw + ajI2MEh4b3BhOEZac25aZzJhZkRiRFkK8tFrrWu3DnZiApZ8tKPmIKg8zy2IgE+d + iF4rXwCklStqH6kB8kltQgnVH6nqezNkKnLOQAk/IePCPfUPlWEtXA== -----END AGE ENCRYPTED FILE----- - recipient: age1ph0gtrpvus5y2kl5t5wnmlcjpevavxf4l2aagrqyp7nng7jvluus959fvq enc: | -----BEGIN AGE ENCRYPTED FILE----- - YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBNTEdPMURXZi9qSVFSNzYr - UmMwVjNUYy9pdVVXZHlmRGFhQndubVBraWlNCjZkT0RFMDNFUkJVcXVZRFdJazQ2 - ZTd6RkJXRHptekVidlJkWUlzTy9DODQKLS0tIDhuMkxOcXVoS090eVlCLzcycTVp - eThCbjZMRTAxNWpWWnNMSmY2UWpmK0UKTyee/uxejVZdwS9rH2/bDE2h49vjyuBF - n+QuBOCoOZykQLjf+JtnXRO9a0gZMuM8QFqDGAFE5xl6PBYyS8AK1A== + YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBGVCtFbGxFOXBpUHpQZXFI + dmQ1ZDNLbnhZSFBEcjNrYms0Wk5mVzhkaWtBCnBFUG9SeUxnM3Y0Q0FTM2hMQzRJ + blJadHdWQzlSTnF3aURlaHFPZ3JGUW8KLS0tIHp0dkovLzUrV2x6MitnVmlTUlNR + QmJXME95QUFaNkpUQ2pGVzcyZjl4UTgKbNGYjVq8S6WDbfPIypTRxMfCai+IxjiF + 9Ha3JmvS1qdDAASoHqK4ehQ6XlL69PS/ztpLs7yTqfkju46BrMJ8RA== -----END AGE ENCRYPTED FILE----- - recipient: age1sy6t7kdeyf63mjnrdnqm08rjv2s5ddexgncuq4ps6z4c5hgg4dzqp6pznq enc: | -----BEGIN AGE ENCRYPTED FILE----- - YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBSbkk4M1RNb1pZZ2p1SjRQ - SDF0VHVkcXdjdEEyUGh2THkxVXk3QVlvbkFzCkZQOHIvcmZxR0RMRStLb3BuRU00 - NHFLb21EVmo4MUhmeEdlWXpXVGlqNEEKLS0tIDNaVzdpbEhsWlVYQWpIU28vL3RO - NUZ2SlMwZXFzREI5SmFCQ2JjY3RnN28KRoZFbZVNEe1kmgpXsGIE9ow9UdxO2Fhj - IFlstHgIT0NfCoCJlikIchGrLBSUwfB8fP44iOhV96wYCTdjNTfwww== + YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBhYngySERQcENOcTNjNFgy + R0h2VkpsdEIvdXhYSm8vTjc5bzZTalg3OURRClpNWS9SRUw0elFsTEpTL0tIcnhl + VnM4MWJMRWRzc2hVQktKK21oRFVkQVEKLS0tIHZNNUdOc0xTOTR2OFJ4bEJ3WlhZ + SGtnVkpsLzRHQ2ZuM0E0YmdiZEZFTkkKzMFBd1v3YbWOzWyN8lfGjSZK/fxrKIcU + 0rKWUVqaNwXaR3s20Amie5Cf4FEa6srQ2m8ZXOeJkyO1pyBbDbbxLA== -----END AGE ENCRYPTED FILE----- - recipient: age1ruav09tz635tt3sgq55pgn83qnte0j2nx0jd6gvjxelkqqdtygcqgcam2t enc: | -----BEGIN AGE ENCRYPTED FILE----- - YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBCYWxKbU9XKzlWVEZhNGNQ - cVlhVmsvLzR2alRDN3I4VDdBcFBZMWR5QkZVCm9Pc1hmSjBTZ3BTNTRVcUJYRjk2 - WWtZdFI3S1p6cE1YNXNrbVd0UDZtYUEKLS0tIE1HNVprM1E2cUErSFdoNWxSenNy - dTZGUWZWbVhDaEpmeE0zSHBtcVpZanMK4RYNBRWZCRXxWh1Fzd6qKC0YRqavilXt - g6Jr1caudlPCm+Vf289b7YQBdHjonsY/YnoGOJ6erqKaQHvB+dkzcA== + YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSB0S3htbnJXK2xKWUIwV2Ni + RmpnaWdvNmhiM2JvdGF0MWYzTTJiY3BLb1RzClg0MXJta3hjNGhwNU5oM3g1ZG5j + T1dybDI1cnZnRHRxbXYyS2oyKzFsRGsKLS0tIG9wb1kwMzVOQkFqYTc0RURaN1BK + UHo5UGI3WWRBV241eHM0MzZpbE5TMzAKRl3Xly9fN+gNkF9mJzTt6cycywvYoQjD + KB07JHajozUnJ6LA8d97q/XriMTho08LuKhWfKYbWug4VRZTYYtT0g== -----END AGE ENCRYPTED FILE----- - recipient: age1dwqnyurvm7vasf9n7alduzmg79nczkuafknr8x3l4jnzwnuzzydqj0y92p enc: | -----BEGIN AGE ENCRYPTED FILE----- - YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBCcnFna1VHeHl5aGpVa20y - aFo2UC9FTXB1WmhBd1daTFh0TW00L1RERDM0CmpNZVQ0VDl6Q3UxamVtU3RPUjVk - QU04aTM5ckNMUkU0Q1p4KzFqWlY4ZUkKLS0tIHBEVTREZktTZTh2ZDBWanpwVnNH - eGRqMTlDaWRxWk1tbDJRY1I3VXRyYnMK04tnWpZvADUc5AL38EfA5fyiVeK1PqxX - of3cheqfebsH4tDOHdTzDM7+oWcn0Xh3Ty/FAK56V9oXP96ygrhArw== + YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBRS0QweWJRcDI0bi9xVHht + U09DZUdKR3BTWVFQZlBsL2Z1QVdGUTY4VGc4ClZBRjlvTk9ITm5TV2paeWZUeERV + Z01aOWUvM1J6TFFodUhmcENNR2NYVFkKLS0tIHlJN3Y3Ry9RMUVvc1ppM2ZSeG9X + WGxpLzYrdUorRFdWc0FmZVMzSVlNaGMKeSPHIylPZLbJgG+DrxS1Am7ryS3zb7eo + 8bTnOrlN/OIF+RjivMHVD094SL6xyDCS4tFYNWHfNaWxK291L4L/2A== -----END AGE ENCRYPTED FILE----- - recipient: age1dx6u86w8d242tvjesz362caf4lcatw24ldd0hj9qn7xhqw0s0c5qus8wxt enc: | -----BEGIN AGE ENCRYPTED FILE----- - YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSAxR01lQnNCM00rWDM1VzR0 - WjFPaDRnNitUTkZNVDdwbVd4eTJBTlZtMkI4CjUzckpQSlJ4Z1NMSU96V1FaTm10 - NkRDVXpzODk4VGp4TENjdnZhTW94YzAKLS0tIFV1R1RaekFtT3lJbGR5M2FlMFQ3 - TUdwMWFudGZOTXRuaytsclVvNjVzNVEKfy7mS99cp9Od+D4x80WMFOtOjKfXHiRh - ctfItC+r3utJ+TogS+FMap9ZB7Yf72bu5QIsyeqHrnJijo2pz+qfnA== + YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBrMjh3WXhYQ1EyY3o5Ukoz + YWJlVGdKMlMxSElUS2NHbmIydk93dUdtcjJBCnI1U3VtMnZjdCt2VGRMYkl1alVC + THdFbHZueTcwU2NUQzZNNEI0NHNyMmMKLS0tIFoxOGVadmhRSFNlOVJMejJrWGNY + UXRTMUxIY2ROTkR1eFE2bFJCTHIrZ1UK5SoPsxpG4bRxtcmgqAzs15D/ZfRtlNOV + cLvQIatBEybX0sRO0kfzLjgye7shtFiu9Dx+W3oqOmsR1Gjx4aoPCg== -----END AGE ENCRYPTED FILE----- - recipient: age1h0nv9lwkkhd9y0rlf832g3lualvjafqpyvlkgf8d0cn6c4zg959qkrfzt3 enc: | -----BEGIN AGE ENCRYPTED FILE----- - YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBpUWZkSW16Z3RSNGpRWFdl - VEI4cktrL1ZBek0yZUkzaU13bFdoVktMVDBFCm9SQ0xvM0hkTFE4aWpsVy9CVEVi - cGUvZytTYWp3MHRHUnlDTytHZ2JuNHMKLS0tIGkyNmFSdE4wM2EwaHlJNG5qQTRM - SkR3dlRMVW83cFlTRUQrN1BNWE1yanMK06ok+ydLxgkQzvkfpw4gEfh4TpvHiG0u - ZAyP5z2V87wn1CfE5eZKvD0f8HJU57a7cl+G0B9A+LfhjtLOmmX63Q== + YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBFQll5bHRXNmIxVWFOYnRq + bFlvcVc1MXhuZmo5bndsK1Z2N3dSZkx4Nnl3ClpkcFRxTWZKYytPMy95d1VCakRK + WlJXWmowRTRXMTZrc3J2dG5DWFlkRUUKLS0tIG9WUWxGZTlXZHV5THpLWUh4SVk0 + eDQvbVFYdGFrVWJNSlNjU0Ryb2JnUW8KV4u4GjN1dVJ5eZq3DJ3ph8mmRVxo1i5X + xlJqp+aEEuJTQGXUp9rWW8WKm3LGP+UcJYtPBwZKwgBiVN6fpj3Tzw== -----END AGE ENCRYPTED FILE----- - - recipient: age1eqcj2g0fdekj2wpqp4y0fg9c5myydjdt9zlr5scr0grk6fxszymqkpw5jf + - recipient: age1d9h9mm3u5qalmpl2pf62pyzqj8t654n435emn93rutv0cg9sr32sg64fdj enc: | -----BEGIN AGE ENCRYPTED FILE----- - YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBGTWRkUzA1NjNaNFFCRFlQ - MzB6ZkFqeXg4TTU2OEs0Yk9xZFBXMFpDS1c4CjRJaHp1eWVTd3pWNldOTjBPMlJs - Mm1KYlpvMndxcnhXc0JNdnI5S2tQS1UKLS0tIHBZbWM5TDBTWTI0N2ViVW9LNnE5 - NzZmbkFKQVdmN09sWTJoeWRyQ1FzaVkKE46ZAbggYOoQ85OX25zfPS4HtjzqmoKY - WvcWn4wY/aM8westLk1yIDQeCkyTczbKfdSgvT9lkxpZLX483zZfbA== + YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBObE8xcTVqRUMxcHc3aXVY + bnM3WUhVUHZxYmY3Q0E4clFjUU9oVFVQT0E4CjNXNWxFSVkrdzFvdEdKaEcvY2hh + cGEzRVZyK2NJVWpwNlNDWUJESU5OUHMKLS0tIFRCdm1Rc1ZPeVhPWTU4TVMzZmFW + OUdPaDg0UHFKZ2JVYjF3aUR2Y0hMdDgK/pM1Q3/7r/apPaqZkNvAvJMnL/errQrS + 0vfatY/b81TO3ABvnJPZIEO5/ECtLsldI52kJonNGbzJHuWv0wQdbg== -----END AGE ENCRYPTED FILE----- - recipient: age1fm7zr0ea3d589tkgcz2klqgnajduzkr25e8tnhh7qxzuleqxq3yq3c0s3t enc: | -----BEGIN AGE ENCRYPTED FILE----- - YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBpY1hJaTVxbHU4TEM4ZXNF - T0dFbFpKK3JCR3hjRGVqSzlPOWlTRE1NV3lrCmlRZ3o0dlIxODlqaW96dEE5QXg5 - L0JzOTQ4N0FoWkhaRjFBclFFSDAyQk0KLS0tIDVuUnlGMnlLVk1tZlRTYnRUVGlt - bmR6cW1DbmpDUjJNWk9yL3BucU83emcKtRG8xAgMeVwKWsG5T/Ph9lI1Q0l+8Izk - unMt4RpvKWEdzzxrPQTvLTPA4TEEdnH9kryPdZQF1ob+sPg0Zp8aog== + YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBqbmR6ZFhoMTlBYVVPRm1J + RDV6QS8vaFlNRGZ3ek0zRisxblFrbkFwYURBCk5kMDh2ZHl4elFjR2JkU1FqL0Z5 + K2tQMmg3bmpSMnhVTW1VS0hHYXBjNW8KLS0tIHFwbUwvc2pWZU9DZDV1SlJzWUxE + bDEzOWZJaTQybGQ0T1ZycEYwVmd4RXcK8gRveCQP4nnSNNRM7F8zJAKSaf26OcP+ + Somq0brBPK8RiK4yDPoQ6T56y3zADDv1gRqwcEZ3Hupy6F4dhanxzg== -----END AGE ENCRYPTED FILE----- - recipient: age16wuzuxnkcgfuxzvzgk5e5a5f6hhs386adjewyv54m9esr4yj6uuslpn6tp enc: | -----BEGIN AGE ENCRYPTED FILE----- - YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBUV0x3bGxxRFMrVnBURDhS - MEp3d1NMWTVaNndiVUl0cFVaNHB0Y3hncHlrCmZjUi95UGhSUXlocWQ4SCtHYkRV - NGFaRjluMDJLRDVYNGNER0IvZjNzQ1UKLS0tIGduRGh6aEFnOERqS2NRc1FadlhP - YTd4ell0KzE0cWh6V1dxOTNmblQ4YmsKYt/VKILX06SAUmJhmllx4JZIl9anb1ww - 8WaqVfNVCa/9wA/Ah6FPcpC9hvboIgHzAUJ7xy5fvdYdDzKipbroWg== + YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBBR2hzMVY2a3AyZ1BGTjUw + WFJKeFRjNTh5ZVVKT0ttVnNUNjlCbG5zaUdNCjE0QjRGS2lFNUY4eFhULzBwVGJy + aHJVRmlJcVVPUTJ4QnRMY1BtaDFkYmcKLS0tIHZEWGlLWGV4RnBZblhaYkpIYjF4 + NHlheGkxVklGVXo5TDZzUWpBNzJVUU0KRrn1hYM4WgqBsXQIAeGeuqyh47VSpfSA + vIwXvixQkA9M3OL9BFInRnxpfM0yly9FHlvG2Fyx42CL3FqGw6R6uA== -----END AGE ENCRYPTED FILE----- - recipient: age16rkvks3tljju3y6xu0l7luhjzx634et97g3xe58xf2dgfn2865rqkq6t8f enc: | -----BEGIN AGE ENCRYPTED FILE----- - YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSAvV3RrbDZBQUg5TmNWUnpS - U1pJOGtobnhBZWljczh6NHBVcXBuaWZTR21BClhHVk5wTTV3b3YvM0xEVmx1UU9l - bm9VS1VlejBmLzJaMmtDZTZ1TFJ0bncKLS0tIFhvelFER0ZrdENreHFHTHo3Sjlk - UmErMDZpSFROTm1Fem81TVg3WFQrbkkK3hrbxLcrSkTlyxdRZOdtP/HQ7j0SOEuk - edTAP81z+78QGnrfuvZ7PkJ7yDRJ84KCAxWXLSW3ZN8u305SnAE8/Q== + YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBZbXRIVENmSENCMi81N1Z1 + ZWdYWEJlVkUyY3NDekU5ZEZsYWRXb3lvNGpJCkxsY2RtNXhBdDFqMVpiejZxcHVy + TmZiQTE0dzAxTW82Vk5GVUM1c01rYlEKLS0tIE1nOG9aZzgveUtrbWRGRmVJaFFl + RGxBdmkxOFo4a3ZIMFNpSndBTlRBRUkKtDlty0kr4NSQ3Jfa+eXtEBLInCYwn5ic + 9ydnVHuEs5meV0JJFWPaP8z4edqaVAS0iHbu1WcaF0fmb88vw7EenQ== -----END AGE ENCRYPTED FILE----- - recipient: age1yuh8vhakrwn2nm4dzxmdp99cmvl3cd4af36p5w2v559263a4uy4sulpn60 enc: | -----BEGIN AGE ENCRYPTED FILE----- - YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSA2L2VuakE2c01Tam84K1ZE - Q1F4WmtNNXErT3F5NXZ0MWdPYW5nMnFEY3pVCkFyWDhvdGFGMTZJd3hwMDltalZE - U0pkVWE1TEZ0bnpEMGdCU0VmQWZiZzAKLS0tIE9vVEdHdGxXaWhGbnVDcFE2WE5Q - dGJpZXUxZUNDcFhYamdjdXNkeUZXeTAKZNTEjtTbelQ34REWPv/Qh+lhdDTT9htb - Abfn5yYTZcIPML/dU3WrxeFoMfaNEBX4L9qAyqadzWdgqsvIR0NDcg== + YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBoOW1MUFZhYW5Bd2VqODlD + TkNnU0ZCb0s0TVM4UTFRK0JucFltVENFaURBCndLKzBoZmdCYzNnd2RnRzRrdTJ2 + NFQ3OHpDaTN1QWtJb2ZFWTlwR3FVT1UKLS0tIEFiUmdMMHcwQjltakVpWGNQenRY + WEVVNlJjRWFzR1pudVpXZEFwTkVuY1kK2LcDFxcSlZpvBCN4pxKQ3u896OTiXO62 + qcrs+DurYCB+yWhXsjWF4li6ZloqaE2Y94cEMnn/AtXC3vug+cbGRg== -----END AGE ENCRYPTED FILE----- - recipient: age17dh936q0l622ez7m0zfak46awqdx35hldqzsfnh72cgtcthlhg4qdl74fh enc: | -----BEGIN AGE ENCRYPTED FILE----- - YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBVQkMvNEw0Q1RXbkt1bXFj - c2dnOHhDUGR3VWQxZS9iNExzNHRzdjV1Uml3CjJVeVFaQXAxQ3llM055RlJVcnRB - cFBjOHg4Q05lalh3MHFETG8yS3cxYTgKLS0tIDJ2Qi9RWkpZSXNvVzl1anpRaUtq - QVl6UDg4Zm5iRlhZZWc3bGRHRmFmOEUKelNXpOz+BzlKQppqfqMlW+eq6gorQorm - O8gFQqiLEVxpB+bddYSPSVVgo/qfVguygoYSu5SXb4VohxWt5rRRLA== + YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBia2pnNGJZZUhZUHJqMW8x + aGozOFFoYnlYeUw5NC9yZVJwTDJuWGI1aVcwCkZDT2o0K3U3NGphS0JZem5xc0xO + Tk9CRkx2clgvdU83ekwvR2dDenlLeW8KLS0tIHA3MFdSRFZkS1IycGN0OHdJVWN2 + cVhrOXBNVDU0SklqUklqOFlBb05SVFUKobg5pJzfemDUshDwJL3IAd5xVY+aL8ib + 7ty5Fz4oC0LIpAqeNiaRUsVno9gxDNQj+wLyLl61qXrC+mfagkx7yA== -----END AGE ENCRYPTED FILE----- - lastmodified: "2026-04-24T00:45:23Z" - mac: ENC[AES256_GCM,data:9wUqmYdoxdfZktlkRNBL+OAedqcv+AlBFwsLN6u9bIfHcWG7uh1KrEDrLgQsbFK4rsGvV2saTr0KmHrDrO5wfy894c3lGo/TGXZab0vjgChIwOekwHLAg5LdHZFntY4rt1/j3YYgsnw7+SaoR+VfrktAKzMR4vjbSjSEa2pWJ5Y=,iv:3TYCFuyDGoCSchnd3mqMFHAnWiPjdxpxb+RhfIMI3ZA=,tag:8t7UTgyC/ywOIIk6rqmZQg==,type:str] + lastmodified: "2026-04-24T17:24:17Z" + mac: ENC[AES256_GCM,data:MR2mqz+6OMlGwmF3YGd0pfzUxoMk007MxDv3b/McmHPN3pWhc5x1iim7N5C9uigyL3KbhiBkxvtClFjHnC1C2She9wBp0xE9GJYyaU8W8CGM/LD20gV0qgRt6VcVs+MliRW7+Xw+LHGkGa8nLZI91E0CsOruKjDvfHuhaS435Rk=,iv:ELUGkGqP61LY1lgNSrkoNNN6B85kS8gOpr3BhBrfow4=,tag:SAPb19GKM0Buuo5s3Yvz+w==,type:str] unencrypted_comment_regex: .* version: 3.11.0 diff --git a/.stack/secrets/vars/test.sops.yaml b/.stack/secrets/vars/test.sops.yaml index 06b3aa98..01f21305 100644 --- a/.stack/secrets/vars/test.sops.yaml +++ b/.stack/secrets/vars/test.sops.yaml @@ -1,175 +1,158 @@ postgres_url: ENC[AES256_GCM,data:xjnMMCL6L7wETlwXp62P72z4dmqOsniXaKRV2dFvwiGcLeIirs9sq/sl,iv:yRYX5imo5Olrbek95SOlE3kBY0HJXBUoSpTgE9aUV8g=,tag:VY0a8MB5FKk2H61JMx/fRA==,type:str] sops: age: - - recipient: ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDRZy5oeMfqhk91usPMfWM3qZjOu91mhxP5FNISekFeUuHVWciOTjObUquvXcBXPBECsMkkHCuBVW01usaqvMWl0fGGs6oV0oHBjMVNoNTR8PoXklvXQyTVKH4XDHt21guAZcdIyAWrcjGaUbCotN8gbBQ4qJe8EgVrwOHBiIwKzQT8SQKJAkbwLFmQpHfcSpibr/h/UDuEpgKv6dKE5TNiEKdWKYYbCFei98A1Vax56HXVQKVZmzz0WrH3M5uLVi4BG0Ed1o6IjhBl2iJOBNZpuK6N44mc0wUQcqKwshinDPprstfaV5vYsB3U2nDLeNaO1yvOXkOA+PqGeu5Kx5k3 - enc: | - -----BEGIN AGE ENCRYPTED FILE----- - YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IHNzaC1yc2EgdFJPbGx3ClF6dUZ3TG0y - M0wvY3luWXk0VXdWcktDd2tNVWZvRkNweWdjQk92cmVid0RUNGFqQ3I5MmVDY2ds - MXM4Q1FEZUQKcC9MZXNhTDgyaUdON2hUUzhJYTUyWTZ3ZXZ2cnlrZTdsditDMTNW - T0N1bVN0a2RTeFloK2ZoN3Q4VzRYR2NPMApjd01hcGt6STE3OVBseEY0U1BaSzQ3 - OW94K01TTUdBMkV6eVlablcvUUdON0F2OGxQUXd6RmNSL1VxNHJ5L2o1CnFEQVJR - YWhMYWhzR2hkeTl1RStZZDJlWE4vc3F6NTRUTFlQRlp1SEVMbU12UUxxaXFHa3dE - dEt1SWtpeFp4Q1cKeG5QTzNJTXdYQS8wNU4ycGVHaWZIMU8zWVF2L1cwd2ZFWk9V - Zk9idU9aa1ljSU1jcG80WW5MUjdmQ0ZsY0pRMgpxd3ZodEg4dXN6MWRnZmQydGxD - SmdRCi0tLSBDV3JPOVJLeUFnSmNUZEhSZmEwMStiVEdWbGJsV0lRUE1wek80VGNW - b01FCpswW9vmOR0AO6yFNHXFV6H4i4jAvIh00sEk7ABYKNML5LGyS79/WiglfmmQ - bDHr6tiuXsOP8Le3xoD9rKEI5QM= - -----END AGE ENCRYPTED FILE----- - - recipient: ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDJiwo+71EEnDQfxVjnRlB5Ofpg6fPTFB80W5TQh+TgoKpxTsFqjvukltmnOT8kCFv11KE3QU5LWt70WNOseCS5xTY940SWBZA7zeE7ed4/CCyNP/O2DcCsoc6iIkaSegcrIcOe5Ei1nxMr0F/84GiX06tvD6r56jmu2Teer7ty7baKEjplbT+bEnfNvjMRzvcttzI3Cp6OzvZvrnv2yXm42fw380SU1H9y7uVcAYDZNPiBv0sLue5HxqDW3InQYs9fkuH5hJdLHRhBgvcczVW31fc2P9imLA3IPLF4hNNndS32dKZwfhnhwDaIEUWSwhVqZcQNZZg6P0NFU25aSzcB/FDKrTRoJQBaM4YSCsL4K0J2NI3cFDi64sP35XfcWif/DwtcmmCmxlOO7721fhOO8VwYpZXyeoYt62+EHnCqaQp0AK80hGVco0ZUraoqwnZ8YbJmzpVC7so1bJWncTCutJmgeMDdEyLWRoBV6HUx8wJUVqylQjE3OijgBHa1lxE= - enc: | - -----BEGIN AGE ENCRYPTED FILE----- - YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IHNzaC1yc2EgTXExV2JRCkhqWFRJTVQ4 - UnQ0WVZ1ckhDRCt0cGxUQTBJamlwNW1EYU5FV2xvQ29kUTBJT2xXNFlUbXhGYjkv - aUg2SHpOSVYKK1F0elhGUmJhLzZZeDRGMVBFWTg2N2RsNmJTTUZIWEN6UWcxdXVM - c1JGM1FWOFM4S3VFeHEyRm14Z09nMFZXagoxSE94WVMrbVJpRHFSOWt2QUhJcnh4 - TEFRNE1BbGwyVzI2ajgzZ1VoUWNvMlBWOUlzS0FJME1ybTkrMmpqKzRxClVVTFZv - TGFRNzNvWUtwN2FtTXVISVdQTkxzTWhHRXJsbXlGQ043cTd6OTRqbnc1TThzZjVm - WENxdnZEWllaRWMKaEdldk9INlVPTFRTZDZFZFcvZDhmREZIZGl1ZldpUmpHK3k3 - S1RSeDZjdU1JM1pYMVZnUHkrMktPR1lBYUFqVApXZzl2MFNEaDFNVEdZdGRyamY1 - cHdUWmRONzdLU1BlWjQyRDZtMjNEMU5YUEtiNndZdEdDRDgwK1ZvRHJJMTdLCkln - V29OL2FiZ3dwRFJpc0RnSzdXdndINktsYW9iSDVGMXJLTFY1TjNOVEpWWUloZEZ0 - OWtvbm5jdDlqTXU4VGgKY0VpUnlGa2JEeXJVaTN3TUR5ZVJ1ZWRGRGhDNWg4aUY5 - eWh4d0IrZUw3WUpETjV4dk5KRE5hdjV3L20wQlU2NgoKLS0tIDdpUGJGN2Y4MFM0 - ODloVXlySndha2ZuMUNDSHBpRlhvUHErZFdKTDdaVXcKC5xEsnp4xjvLXyiJeLo9 - WlAgN4tMCkHLFi5D+OlTtalyh1x9I5RNFSb9tC7/nz7y777luFqdHgrCvfQuEvHY - Ng== - -----END AGE ENCRYPTED FILE----- - - recipient: ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDBUbmUQhSErispWvXO4iLZBkpw8LpvJwNUxQ2OMDp3XiM3mZpUNYbk43+DPlpC78P6Kg+wHq41PRq9MPKyrSmjVaNeP/BPIEsnxsDRHIVvdDcOb3dC/DX2MU5ROF19b9jUz0YmKLpjmwL9HTwtmrMwyjag609KlSliF8JvlmvVwTYsubyZr+IjsmGT+VjVohHyMWEQpnBZIDY+LuO/CBkBipbGm0V9h0m32utlKWLESAuqsNw1f4CiHam9UgBoVInAdG9avuMIyeF4NcaBgshufL8/RQibk/hPemierC/lGyMygLq7uYJT3PgMkxRmXVmXCoFARg8gzb0VPAygj6wmde70FWFOLSatkiugHKa7Aim6F0JfgpcYN4XRiVz0xDt5OyK9bmjIqW4z1ZKFtZLVs0MeCvVFSGDRB+3I6V59WfMpmTqcGSbuAl0teh+m8wn2LUdTzgDk3x0KrIGRYNn3gT+3wKnrsyUHG+7iWtsl1JdZ4rSsljbQ8+Z6Kt4qX18= - enc: | - -----BEGIN AGE ENCRYPTED FILE----- - YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IHNzaC1yc2EgYXZRczlnCndKclEwQXVJ - RE1VandFZkFCcTF3NlVzb1pEVGlWWklXdm85eHVrcHpzTFZtWXhIYnNtUDRJUHR6 - bUM5OWtpankKbDVBVUFwaXlQWDNzQ0E4MVNkZ1VCY3A4VTZXYUxsWmhHWENST1Vv - SzEwbms0TVgxNElubzluQVJOVXM1VndMVgpQWXRiQzRWZTBFemhvUnVDS1pxUHls - YmdveDNXTVJFQXN3cEhkaGR5MHMrRkdYMm1UM3poNlY5SDhYTWJrYld4CmR2eEV5 - RUQ4NEZsa0xJUnFFbjNGalBVb0xpaDJ4a3FLUE1LbS8vRVpjeUVrSE9VTGpubFc5 - WWQ2dElyZE54MTcKbDJBZjd3Njh5VnJFbFRIU1RiaVhsRmRTck1HY1BEaDlxazRB - eERMVFRadVdVNFVISkoyaXVEbE9MM2Qwbi8vSApWN3lIK0hhbzRjWVlVWUZjWndP - cTNBRWtONnAyN1htMGU4Y3BLN0p1T01OS1E4SVNZcVdwc2dJbXFqS29HbGlVCmJu - S01aanNuTFdGZngxeXBzYzZ1SE9kMUF6RFFQTEpMWVc4enJPemZNRTBZZnRiOEtL - THlaMy9ZNzF5V296QloKZ1k4anV2N3FZbld6VS9tMmFLc2FGTHEyMFJONUI1T2xm - ajNDWXdZU29zWE9ucklLOXhpeWoxWDBSaVd4UHpoNAoKLS0tIE9Ed1lRUzFVajdo - V3doTkZ5Ulp6ZTJ0MUJ1WkxNU0hlTVYrUWJTQVMwK2cKbqdw+8VMWFL5E0hMocSS - mXwqk27MPd461PaGvrzSW+ZqOEYJ+EdQ2Sh/nGWo7RLkZKcygI4yimNgN02t283M - sQ== - -----END AGE ENCRYPTED FILE----- - - recipient: age16ymszt6hmv7p3w596w5wlzng7wgk6mwcchr8s2nvwutnx2nrzyqsvn678s + - recipient: age14vpdar7vzznyxgskp9772zjar95n8l2f36w6tzk980889t7kjdqsc5a50q enc: | -----BEGIN AGE ENCRYPTED FILE----- - YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBKaGs2aWdGWVZTcUxiZEpl - eFUxU3lNbTF5b2FVWWRKSDQzTCtIK0FjZ3hNCnQ2NUt2UVFMazArTjk4NVR2VlE2 - cjNQcGs0cW5kU0ptaWlxOEM5ZE44UUUKLS0tIDZrblpnKzBZcEUvQi9sN1YzQll4 - U0JmcFgvNk0rc2QxS0RER0VGcHVrUWsKvGlFzXx+8548Vx01QQZQh8Abxzfv/bot - H1OuSRcQYsRiSG2YRYQFipRjmxsaTzFnG1fQj04127YV33Lkk5k53A== + YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSB3bHd6ZWdZb3ZTOG1LWGV2 + aU0yVTRqbXpnam5Oa1NKcG5nekFvQkxJcUQ0ClpIUGIycHRFN1A1Tm9PZnhUVW45 + c2FFM3hoa3NWRGpRT3o3RnlmeE5FekEKLS0tIGJWYXB2Y0lJNktWUWZQWCtlb2Rk + TkFMczlVcnBaT2VJTFpPUkFBTWliMjQKgmU7TSLjvRu4B+wzMZEqDdkh+JUY9BHH + /zx9XPvNdERBbDNeIrqGauKUHzuV8gD4fZcy8Tnq8lIH+FlVFhZE9w== -----END AGE ENCRYPTED FILE----- - - recipient: age12vnpyjwhnnm85vktfmg6jwzn55fcg0lmgn6q0wx2z4wawnwgm5cqt6yf2f + - recipient: age1ugmyh9qcz05ehtkgnt2nn3jfz2rf2umnmqx69pgp2ue82dn7vpuqlc3g7v enc: | -----BEGIN AGE ENCRYPTED FILE----- - YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBsdGNiOUxzK1JZc21acTI0 - elFzb2hmRm5PTmFBRVJjQjl2YlVIQUZycDJjCk80OVZUakRQdGNzSHJNYUlXTVpr - RVNDSDFuZTA4aGthbVdIWWtzajgrVzAKLS0tIFNNbDR0TDVaMktFWk1ZZVMvRSt4 - YkdmU1lYaUc4Sm9iWllxQTZROFNQTTgK7l2ni+/At3IefY1XaS0DilQPG0Cu0Fqq - wrDAbFnAAOSckgyuRY3eFTZjV4KEhkeZCQGL1ri3Ewx5uJhFYdl/8A== + YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSA5ZmFLUVZTL2FtZWJ4ZVF0 + M05pMG1WVUR3L0RHRENNblVEM0p3VGRSYVNzClcya0lpZVJyZGlYbHpoU2lOTTlw + NlFWYy9sb2l1VWs3UmdQYytsY0Fja3cKLS0tIFhrak5GS05rN1dWWGp6ZnZuSmRD + UkFna2tVMlYvK2NnWnZjT0w4UElDSFUKw7HvsZa9NNMB+LKNbp7JrkfmFillO91H + UEM+CnnhNmfDTJpW7nskLhd/mY9AS3utp+ke1zJEXdKqN8TwF1qwjA== -----END AGE ENCRYPTED FILE----- - - recipient: age14vpdar7vzznyxgskp9772zjar95n8l2f36w6tzk980889t7kjdqsc5a50q + - recipient: age16ymszt6hmv7p3w596w5wlzng7wgk6mwcchr8s2nvwutnx2nrzyqsvn678s enc: | -----BEGIN AGE ENCRYPTED FILE----- - YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBpa3pFU3JNRTJ4L2o0Vm9X - ZnpOenJqSDhzbkRwQ2lRdFp6K1NzU2tCSW1zCmhYTkVQSXBXVmR0UzFsYkpTeHJ1 - Q1MrbVMwRFcwWlhZL1k5UzNJSGtNb1kKLS0tIHZqTzlYNnRrTjdIeElXeGFVejF3 - OUtYUzIzblZGRWMzckRZV3NyazFla2cKU5AzHJfoFeWsymgu75dQy+QCqKfq0Ynr - 6EHAR8olR1PBLy4eKOZAoyReyEU5wqiWVH0Azsaix4ylmBr2SFM1qg== + YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBianpvQjNDK0M0amVST1ZW + cUhWVGI1VE1uV0hTdjZWL2pBbFA3eC9CL2tvCnNlRS9UTzdjTituVldOSy9nVTM5 + eTZoYmYvOEc0VGpPb1lPajdCaG5KaWcKLS0tIElzVGlVQTZteEdXblpjZlZSRW5p + RFNJajhUMm5TUG1zWkdUenp3SFF5cTgKjbCF/yLqc2CR/dbgKZnMOj7cxxncs1Xr + IyUNGWHlr16H4a1M/3Ol4vGrxxDy2LfiA/TS+BqVT0pKnpxPSSQkEQ== -----END AGE ENCRYPTED FILE----- - - recipient: age1ugmyh9qcz05ehtkgnt2nn3jfz2rf2umnmqx69pgp2ue82dn7vpuqlc3g7v + - recipient: age12vnpyjwhnnm85vktfmg6jwzn55fcg0lmgn6q0wx2z4wawnwgm5cqt6yf2f enc: | -----BEGIN AGE ENCRYPTED FILE----- - YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBKSEt0NzFIYVhPdml0RnI2 - QVhrTGdiMmY1YXhMaTJZamw3a2RLTE5pTnpNClptWXI3MWZ2TDMxVUlSNDMzS0tG - SjkvakJLcXNVaFlocFYyRWRibzROakkKLS0tIDdzcG03cWdIVEkxU0FHdjRvNHNl - WG8wMnJ4YWRUNVpCYlNrS1ozSEdSd1kK0+beNkd2HhjtcFrj0sGuHvw9Rx3dTP0o - RJ1Wz28lrkh4CXPnMoRbPivc6QyGS/IEyn6HMh9T3V4/pCydgyYSUQ== + YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSA4L1NGSzJCQ2pOSFlWNVZi + SmN6ckZhdU1WQ0tEYTVucXdBdmhMdVc1Q3drCnM5RnU2K1hSSlJoTGtxYVR1ay9E + dTQ0eENia1cxSDVBQS9SY1QzZE1vMVkKLS0tIG1VY1pQZWJNYzcrMG1JaytkL2hH + MWVGSnRzMjkyNUJjYXIzaXdQWlQram8KmvKyzaw9sTvJV7Vl8PlmkPqgI/Br4wJ7 + XVZC52agM0MPXmp06xCloXK8WD54pm0/P6YOqtpxw01fnUWLl8yvuQ== -----END AGE ENCRYPTED FILE----- - recipient: age1ruav09tz635tt3sgq55pgn83qnte0j2nx0jd6gvjxelkqqdtygcqgcam2t enc: | -----BEGIN AGE ENCRYPTED FILE----- - YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBCUGN0cHF0NG5nTUZWYkdT - NGVXeHU0S2NoTzRGemNGZGc0QVRReDRTbTNJCmxNZlNsR0cvYVFJWndBd3RrbmFi - MmwwemIrZmkrWnhiMDI0SkdKTGZDd0kKLS0tIGd1ZExSMkh1MGRkMzRtZE1sZHZW - eGMrdno2SEJ3dzhQc2FFamlLMlZyeWsKXzO+MoPhX6LoDA6T7S0snptCCjnY3Dah - F7Eys0YrOdAvBbCY8++YzfIFYuXLtWFPzuMYtOj96Od8PrkMViZ2uA== + YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBwVXdFcHhwNnBadjBGbkUr + U1UwTUdUN3NldXR1T3NLOWFzTkpJVDU0YmhzClpKSWNROXd3SjdQSjlJb2lxbHZh + ZkRPSGlCVFFTVExwRVVDVjMzMDh5ekUKLS0tIGs0TVFZYlAxTm80ZXJielErRmFz + Mzh4UWN5NGxaWEt0NEFOTklCYlUrdGcKkqCIBhtMbgpnImYQP/j0zUn4THBQbbXi + WoZkk7IIAaRl0dN10pZkXZAH7qC9nL06F90I3tlwFXsrCIWLwipSQw== -----END AGE ENCRYPTED FILE----- - recipient: age1ph0gtrpvus5y2kl5t5wnmlcjpevavxf4l2aagrqyp7nng7jvluus959fvq enc: | -----BEGIN AGE ENCRYPTED FILE----- - YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSB4UlFmZE8yNXlHYVFMOG1o - V3FBR1Q4UmlCWTJyVWJKYmdpWVZBWUF5WUV3CjNoOENRUUhMUVc2ZlAzamQ1Y2Yx - QXpPMTJ6SlRsQnBSM0g0ZWxsMVcwTEEKLS0tIFNVRXNZQXpmeE9GaXU2N1BCS0p0 - QWhhWHM3ODQ5ZVdIY0dBSlJXZjVwQVkKxs3OMkEbbHXrcZaX9tmbwKI8F2xG0Nz8 - IFu4YYJHYBMoc1loiX1uCH+rr0D+LnunCfJSW0a7A5F7qozPetaEmQ== + YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSAySkJRM3NGT1ZBOUZXcFNR + UzVKaTFON2hSMnNNSmdNeGR1SE5LUDZtVGpRCnRJY1NLdG1kcVpHeE9kMC82SUtp + QkVLcFR1T2xyQ2lCSytjN08vTmp2UGsKLS0tIG1xcWYxalgrZ1dtSXVKZFFaa2dv + bVU1TzdXdGY4WGFvTC8zQzNHM0g1ZUUK4hDZtJ7jUBbrUsNpHvjiiUD7Yg2RRydy + ZPi2hsFCCkUVtt53Smi0XJwXWE5i9arIdbRoKZCyUc9v/32hCBKpYw== -----END AGE ENCRYPTED FILE----- - recipient: age1sy6t7kdeyf63mjnrdnqm08rjv2s5ddexgncuq4ps6z4c5hgg4dzqp6pznq enc: | -----BEGIN AGE ENCRYPTED FILE----- - YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSAybGgyVFozclFSaGI0V3NW - by9UbnI3WUdJQkxsSWloa0lWQUNkOTlGK0VjCmdXckZVUXl3VUVMaGdPay9WT3Jp - c0JZMzhzYWMxWTdSTzZsSVMvbTUvMEkKLS0tIGF2Y2dLbElGaUJiWGVxZjJaemp2 - LzdDVTlTejc3NlRaVTY4bUJ2a3RLZGMK/gAQ7Zb0LU9jvZTWvJMB+25+C63CF4jv - k6LapodqsWAVWAIYeXFQa7ZdTlFNt1IvClw69ieq1X1gqdpSIpyT5A== + YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBZTnJFN3IvTFVsZVFqM3JZ + a3dLak9nU1BPbEhZN1RjWXlpY0NGTWh6UlVFClBleWk3dWtLaVY1RnNmdkhSL3l4 + bkt6VWkvU3hOVjc1UTVZTnRGS21TMWcKLS0tIGhhdk96cnJpdjZibWRRMnBrVUdI + aWt2U1g4WWl0cVBLRk96WkVqam44WlkKTORg/CwrOi/NuKROH+VHDbnCbpQvVu7Q + gKK4zJ3ilx+uwZ+m8+ohTdurRkbU8KalnXPCEHxEL19Sx0+Zh04rzw== -----END AGE ENCRYPTED FILE----- - recipient: age1ruav09tz635tt3sgq55pgn83qnte0j2nx0jd6gvjxelkqqdtygcqgcam2t enc: | -----BEGIN AGE ENCRYPTED FILE----- - YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSAxUzVlUEk5aTRlQ1RvSHdW - NXhCZnU5TjB0M1lXTXFtMHBBdm5nQXZ0WTNBClEwSUJFaWx1T3o5d0JXaEFwVHpa - cUpzUTlja3ZFMFZhN1E3VjJ0blJtUFkKLS0tIHRmL1pJQnJ1MWlVeC9yUC93RHpZ - UDNoVzZGY0Y2eU9sTFI1RWhxU1lYSlkKNU+NArZ/GL5kJ+Kv5BZBdwxWTPw1OXLc - LpuGata9dhVfOjTNJsCroNAB4nJ5jym01qvXYWoU+M8sJidJRHGNMg== + YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBVUUlzM21Dd3pnREpodyts + UHFnOWwwaXFCU2N1alJvVWd2MFlLKzBGMW44CkpYczNDaVRzVVhDYWp4d01hUk5Z + aTR1T1loem9sdnhiREJmM2lVU3pQejQKLS0tIEZWQnExRVc2ZTF1UU9DSXFWdzNL + dmo3MmcvaWdKWi9OeUVvdGlpVzZLaVUKHMnjTG+yD1liJQdfsQik3nwzNHGe9ipP + YE0bN5gfIOcD+O3mEJ06LTyyPjlilkltBeJAjsXbiS5uYMcikeEMZw== -----END AGE ENCRYPTED FILE----- - recipient: age1dwqnyurvm7vasf9n7alduzmg79nczkuafknr8x3l4jnzwnuzzydqj0y92p enc: | -----BEGIN AGE ENCRYPTED FILE----- - YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSA1TnVqb090bzZMdE5aUGMx - QkJWbVhVNmFsVmxSTzEwRUE0V3UrWVFPdlI0CmZ1akV2dzlPOXhsYlpuMGdSbDZn - cnNmSFZuSXBRSm1lQWNWWjZmN1ViSTQKLS0tIGpBUFN1VmpQYkNOajV4bUFVVkNU - eDFTbW5VaW56ajRyWFM1N0d4eHp4aWsK40Yua9tXvpCPaF6SfI4aTMxWs2J3LRdm - bZh+ut4LuzpNs1Q4TMORmMbW53P2Gl4580kMxBUsryzP4sUMUzU4hA== + YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBYdmxpTThEdXZGdW1pNll6 + SkpoaVorWnZSRUZnWHBra0dwcjJ3TC9VSXpRCklIRkZXaExSU01GYk56aE9DeW8x + ekhFVFhrblVNMm9aQXJ3TlRydmhDMFkKLS0tIFpLTnlrTjBjVHdxRU1sT2Q4ckx1 + bjdXeURmdjFOSW9ySm9MZlV5U0FlWWcK2YBzpSHV3u6TmqKtNBAFi0iQv73q77kS + O7Hlp0WUqhYp3xqgjfR5Twb1ozkeQX0UOtcnlq/A0o9h+iNWi2zwsg== + -----END AGE ENCRYPTED FILE----- + - recipient: age1dx6u86w8d242tvjesz362caf4lcatw24ldd0hj9qn7xhqw0s0c5qus8wxt + enc: | + -----BEGIN AGE ENCRYPTED FILE----- + YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBESUpIcXBwZHA1QzN4V2R3 + ZzZWYmJkd3BBL2pEQWF0MzUvcWtoQ0ZMaHpZClBnYjlDQ3B0dFVyNW1vTlVjZlo5 + L29XYWVQdUp2c2dlcXFkdTlsSG1mdkUKLS0tIEhmWVEwblQ5V3BPVEVRTnY1bnhR + MW91K3dzdnZNZi9WUzZHUEZwbWpZelUK5uq7yxk0ks9FYi29aL/4sAPIgChxzKhB + 2qJWHihqdJKutQEzRVop3pEbyQnVC0c4WfHqLXIe9u4iDRilT1LLkg== -----END AGE ENCRYPTED FILE----- - recipient: age1h0nv9lwkkhd9y0rlf832g3lualvjafqpyvlkgf8d0cn6c4zg959qkrfzt3 enc: | -----BEGIN AGE ENCRYPTED FILE----- - YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBscDZMUGhtUG9oUnpHZmFJ - MmxTYTZxa0k1MWNRcHlzNE1UV0U2TGN2YnhBCjlPY1d6amtwd0FrSG9RYlM0SlZt - eVE5SHc0WFZFT2JMT3pwUElXUjdBRW8KLS0tIFNDV2VaaEloVlF5Tmc2cE5xSC85 - bXRFU2ZxNzNtTllzeFQva2NUN1JuT3MKPVeUrB6VDG/yYkJ8oyqZlFNpaAfx3DH7 - e0YkA4DEZ2PLLVxZAoYcWVeIoatayuSuPXLQjUgwK6uInpgqMO4ABw== + YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBuQXlCR002QlFQOHJiRi85 + akUwM1QzKzRsWktSekk3SGdWMXozWG5BYVRVCnJUeGxlUVhTQy9pL3R1UWV5dHkx + Y0tvV2UxTnJUejd2QStOa0dGeWRkUGMKLS0tIE10VDk1OTR6RGFheDNVRmJLME0w + S2V4UGY0L2Q3QlFLZkFvU1Q4cllISlUK62IjNKXP3JvaRxyuUK2coqW08iEGJpOO + +2dkb/XyH3UfTtVNBj1OQsq/p9WcHdZudhPuDaJfzDr2vZiVz/SKMQ== + -----END AGE ENCRYPTED FILE----- + - recipient: age1d9h9mm3u5qalmpl2pf62pyzqj8t654n435emn93rutv0cg9sr32sg64fdj + enc: | + -----BEGIN AGE ENCRYPTED FILE----- + YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSB6eFZHMHNHNWtDclUwTzBn + SjcxMlFNVDFqZFFKYlN4VnpUb01LUHFyekJFCi9LWGhUV3BRYnprSE5qdDltVStX + cHZTM2FKaUpESEdRY2djVmlnSjh2UmMKLS0tIGpkT1J3ZkxGeGtKV2pHUDRQTGlk + TnJzSlc5Y2k4QTJoQVJWUUowWG5IRTQK1yn0aMjGKLyJII9QY76wAOkfP2CZkIka + 4Nk3nrbbrXDjr+nbKsoAta7o+JDXqU+y0y6lnmRmS0qFUFrBEQ5fog== -----END AGE ENCRYPTED FILE----- - recipient: age1fm7zr0ea3d589tkgcz2klqgnajduzkr25e8tnhh7qxzuleqxq3yq3c0s3t enc: | -----BEGIN AGE ENCRYPTED FILE----- - YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBtbWptZEE5bFZ6WmlZcDBO - U0xwK3hMVjg4RWlNQTI5OWRONnJqWDIrTDBFCmpVYkRjN2dvWWp4VG04SGZrZVFw - QjRyYm9jYkRmQjM5TzQ0NUIreFR1NDQKLS0tIC9aL1R0SGNyL2l0M1BhSDlVOVFU - SzlYSTdDeVRWUzQ3RW8rZnk1cnVNUmcKBtLuzbRRnHdDe9FHFB7vZFPB3J3ExbD2 - MK6NCLT+pKVjcusJe9ROsxaTt14PuLoewdzujgqPfc8dhs+z9zH9tQ== + YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBNd3V6dFhpMGVVaTY0MW80 + bFBkZTA4RHFaZ0sySEQ0NkpLaWVPNDZUMXdFCkhpaGI5d2U5ZHB6ay9iTkdLNDNq + YnRIbnYzaklMVkRXNmt3SzFvZ1ZxTkUKLS0tIEhvZ2YxdkdCSWMzZC9jdkFEeXFX + a2VGVjd6L1VRd0hiRFUwYVpLZzZGdjAKnxO0Hx8TpkJVSAHDJUILGeOPVuwq7E2R + sP7sP9heWeg97MlZLF4VO0/pu7KHP7OqZ+N8frqRgykVHxkT+Y5XAw== + -----END AGE ENCRYPTED FILE----- + - recipient: age16wuzuxnkcgfuxzvzgk5e5a5f6hhs386adjewyv54m9esr4yj6uuslpn6tp + enc: | + -----BEGIN AGE ENCRYPTED FILE----- + YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBBbEl1Mlp6eThVanJWMVRY + akQzMmVUSGY5bGZKTkJFdnRITkRSS3FNS0RFCmZrd0ZwUElkYXNpWHJkbjhoNFI2 + Ni9RNkJSU1BVNTRGWkpEcWhQNGRUb1kKLS0tIDNBMHcxekFpd0NrL0FkZVFZTVhm + b3dILzBmS0ZDWFlYVnNwejRFcElUU0UKWvn7Y/sOvOB1FHhxilyQzAoCT2dAkbmp + Gvdn0pp+rJrfdWbnrvinmkeIMWnE1oqfKSpruzUsKss22g7ZhgkCMA== + -----END AGE ENCRYPTED FILE----- + - recipient: age16rkvks3tljju3y6xu0l7luhjzx634et97g3xe58xf2dgfn2865rqkq6t8f + enc: | + -----BEGIN AGE ENCRYPTED FILE----- + YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBiSjIvZHMyeklVYmpKOVZQ + OHZIVC85WHpEblN1UWtBK1RUVXVFZTJzZEFjCjBZS0dka2U4dFk4dnhHb0JxNTl5 + S2ZqVVV4OVo1YVk0endGRU5pb2ZHUnMKLS0tIDJ6dG5hVFpmamtGc3dSczZXc1Rs + aVZHREJGdmJreVJqUnNXRm5PazIrMVUKbCFEEmMDGbhuUBkil/RotLYs7IdqTvjy + jqXIXYDjQ1SCtCRQLrnMIZbE4PaPYnsKjInT2KchhcpqlAWbfQhZaQ== -----END AGE ENCRYPTED FILE----- - recipient: age1yuh8vhakrwn2nm4dzxmdp99cmvl3cd4af36p5w2v559263a4uy4sulpn60 enc: | -----BEGIN AGE ENCRYPTED FILE----- - YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBIejRlbm1sZlZvY1VocDNZ - M01oQjN4SXNLMU1sWERQVTI5NEJpR2R0MzBnCmxvL1RqU1hxVUJwV0xXVWxqNlJ6 - Rkk0dGhVQXQ4c3cwWG1DbXIzcTNmczgKLS0tIFNITVZEMXZMOHBaT0hyQjVtVWM5 - SW8zQTY2bVhlQklESVB2eDNnQ3Y4bXcKIpECDftb4mLwnObOer4PpI1wCpzWFlS/ - 7V6A4FYiCAtNKrx8skawX7wD13LS5KjrBau2lnXSPdmQNLZo186iiA== + YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBYemFKWG9KLzNyM1BqTjZO + RlZZVlVCcUh2TGE0SFJJVmtnOWRjM2hERkFBCkZzeHNXZzRjeFpRaWRMMWl6SC9y + Z1NhS0kzdDVqOEVLYUN2KzFhSk1jUEEKLS0tIFkwa1JwZ3lwdmVZZndjeEw4a1Vu + N3BiZllYdTI1MkFxclBVY0JVK1VjRmMKv8xN+xibBQac61R9DHg+SmJRncIcJs69 + dKZNK2mw5Rg+HJrfDyk7yy/yy9eN+odJlEtIZzETantSj/Yfz0Q+hg== -----END AGE ENCRYPTED FILE----- - recipient: age17dh936q0l622ez7m0zfak46awqdx35hldqzsfnh72cgtcthlhg4qdl74fh enc: | -----BEGIN AGE ENCRYPTED FILE----- - YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBBSGZhMG5ZZXQ2WXpUSTMz - Z3h5c0wwMENTcWtzWlhOYWZtOHo4SE1vRmpBCkNVNDBiQnpvVUV6eXI3aUVlYmo2 - aWY0TDlTVzAzdTFhVGpBVjRkRzB1U0kKLS0tIFMxTk9QbjU1d1FzbzEwcXhJVkxm - aS8xT2F0b2Z4Ykk1MG1PQ1FLZWJGRVEKkiVYn+clzMUbMJPfskL8C6HIyVsG7E3x - jc2mqzOr8+bqywCLMD19g4P+1R0WP7m26VPEjTAlJnmU4FYNQu5CmQ== + YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBZWmFPeXpIZGtJWXQ0b0JG + d3V1T2lINGwrdkVtZWhWVkQ5TDRkOVd5R1JZCmRvQkNldDBYRkJvbEdWRWtoM0sz + bExEdnlvRDBjbWlQaU9qMUF0aExvSmMKLS0tIEp0cU4wMWV2MFdkYS9vY2liSVZi + S1NxN1NPSUU1M05MTXduU1JsT0R6UWcKQojDFA5Mr5mTmy37MpFmsaOgs1tMqyKx + bchnch0YV2R8QTBZ5LX8MPhfxiLWUoV8IMsDQ+XxNzyRvjdfmZLPVA== -----END AGE ENCRYPTED FILE----- lastmodified: "2026-03-20T15:32:03Z" mac: ENC[AES256_GCM,data:oHquN4QTFcB3mCeP0buNxsh7oPdOB8ccDk4sW8TIa8u5J8EdmeYUqFeZgheTWvVqo499NGUUxWyXVvXsUaH4u+4xjjrJGhSwP6kLguyY0vcPNFrqGmm7/1JFwC89BlQ5RceHWcmaZFVfpFx5LehRcMATg/KonTTkAlxMxJHIVSA=,iv:RmtSxvkHqmfG0FB63QNa0LgISHO88DDT2UJEGD4E6II=,tag:GKjVFf9ye+7Ss44e/nT1DA==,type:str] diff --git a/.stackpanel-root b/.stackpanel-root new file mode 100644 index 00000000..795c24b0 --- /dev/null +++ b/.stackpanel-root @@ -0,0 +1 @@ +/Users/cm/git/darkmatter/stackpanel diff --git a/.superpowers/brainstorm/filetree-1777244419/.events b/.superpowers/brainstorm/filetree-1777244419/.events new file mode 100644 index 00000000..0fdf8e8f --- /dev/null +++ b/.superpowers/brainstorm/filetree-1777244419/.events @@ -0,0 +1,9 @@ +{"type":"click","text":"illustrative setup tax\n Files your team keeps paying to maintain\n \n \n Not product code. Not differentiating. Just the conventional glue every repo needs.\n \n \n\n \n \n File\n Maintenance reason\n Commits\n Time\n \n\n \n process-compose.yml\n services, readiness, scripts\n 14\n 2.8h\n \n \n .sops.yaml\n recipients, keys, environments\n 9\n 1.6h\n \n \n packages/env/\n typed env drift across apps\n 18\n 3.4h\n \n \n Caddyfile\n local domains and reverse proxy\n 7\n 1.2h\n \n \n Dockerfile\n runtime defaults and build context\n 11\n 2.1h\n \n \n docker-compose.yml\n dev databases, queues, object storage\n 13\n 2.5h\n \n \n .vscode/settings.json\n team editor and schema setup\n 8\n 1.1h\n \n \n tsconfig.json\n tooling defaults and path aliases\n 10\n 1.7h\n \n \n .gitignore\n generated files and local state\n 6\n 45m\n \n \n\n \n\n \n \n Stackpanel moves this into conventions.\n You still get the familiar files. You just stop hand-editing the same boilerplate every time the repo grows.\n \n \n 17h\n illustrative maintenance avoided per project\n \n \n\n \n You maintain: app code, app definitions, secret schemas, project choices\n Stackpanel handles: generated env, service glue, local routes, IDE/tooling defaults","choice":"maintenance-receipt","id":null,"timestamp":1777373993859} +{"type":"click","text":"illustrative setup tax\n Files your team keeps paying to maintain\n \n \n Not product code. Not differentiating. Just the conventional glue every repo needs.\n \n \n\n \n \n File\n Maintenance reason\n Commits\n Time\n \n\n \n process-compose.yml\n services, readiness, scripts\n 14\n 2.8h\n \n \n .sops.yaml\n recipients, keys, environments\n 9\n 1.6h\n \n \n packages/env/\n typed env drift across apps\n 18\n 3.4h\n \n \n Caddyfile\n local domains and reverse proxy\n 7\n 1.2h\n \n \n Dockerfile\n runtime defaults and build context\n 11\n 2.1h\n \n \n docker-compose.yml\n dev databases, queues, object storage\n 13\n 2.5h\n \n \n .vscode/settings.json\n team editor and schema setup\n 8\n 1.1h\n \n \n tsconfig.json\n tooling defaults and path aliases\n 10\n 1.7h\n \n \n .gitignore\n generated files and local state\n 6\n 45m\n \n \n\n \n\n \n \n Stackpanel moves this into conventions.\n You still get the familiar files. You just stop hand-editing the same boilerplate every time the repo grows.\n \n \n 17h\n illustrative maintenance avoided per project\n \n \n\n \n You maintain: app code, app definitions, secret schemas, project choices\n Stackpanel handles: generated env, service glue, local routes, IDE/tooling defaults","choice":"maintenance-receipt","id":null,"timestamp":1777374586871} +{"type":"click","text":"illustrative setup tax\n Files your team keeps paying to maintain\n \n \n Not product code. Not differentiating. Just the conventional glue every repo needs.\n \n \n\n \n \n File\n Maintenance reason\n Commits\n Time\n \n\n \n process-compose.yml\n services, readiness, scripts\n 14\n 2.8h\n \n \n .sops.yaml\n recipients, keys, environments\n 9\n 1.6h\n \n \n packages/env/\n typed env drift across apps\n 18\n 3.4h\n \n \n Caddyfile\n local domains and reverse proxy\n 7\n 1.2h\n \n \n Dockerfile\n runtime defaults and build context\n 11\n 2.1h\n \n \n docker-compose.yml\n dev databases, queues, object storage\n 13\n 2.5h\n \n \n .vscode/settings.json\n team editor and schema setup\n 8\n 1.1h\n \n \n tsconfig.json\n tooling defaults and path aliases\n 10\n 1.7h\n \n \n .gitignore\n generated files and local state\n 6\n 45m\n \n \n\n \n\n \n \n Stackpanel moves this into conventions.\n You still get the familiar files. You just stop hand-editing the same boilerplate every time the repo grows.\n \n \n 17h\n illustrative maintenance avoided per project\n \n \n\n \n You maintain: app code, app definitions, secret schemas, project choices\n Stackpanel handles: generated env, service glue, local routes, IDE/tooling defaults","choice":"maintenance-receipt","id":null,"timestamp":1777374587188} +{"type":"click","text":"illustrative setup tax\n Files your team keeps paying to maintain\n \n \n Not product code. Not differentiating. Just the conventional glue every repo needs.\n \n \n\n \n \n File\n Maintenance reason\n Commits\n Time\n \n\n \n process-compose.yml\n services, readiness, scripts\n 14\n 2.8h\n \n \n .sops.yaml\n recipients, keys, environments\n 9\n 1.6h\n \n \n packages/env/\n typed env drift across apps\n 18\n 3.4h\n \n \n Caddyfile\n local domains and reverse proxy\n 7\n 1.2h\n \n \n Dockerfile\n runtime defaults and build context\n 11\n 2.1h\n \n \n docker-compose.yml\n dev databases, queues, object storage\n 13\n 2.5h\n \n \n .vscode/settings.json\n team editor and schema setup\n 8\n 1.1h\n \n \n tsconfig.json\n tooling defaults and path aliases\n 10\n 1.7h\n \n \n .gitignore\n generated files and local state\n 6\n 45m\n \n \n\n \n\n \n \n Stackpanel moves this into conventions.\n You still get the familiar files. You just stop hand-editing the same boilerplate every time the repo grows.\n \n \n 17h\n illustrative maintenance avoided per project\n \n \n\n \n You maintain: app code, app definitions, secret schemas, project choices\n Stackpanel handles: generated env, service glue, local routes, IDE/tooling defaults","choice":"maintenance-receipt","id":null,"timestamp":1777374587337} +{"type":"click","text":"illustrative setup tax\n Files your team keeps paying to maintain\n \n \n Not product code. Not differentiating. Just the conventional glue every repo needs.\n \n \n\n \n \n File\n Maintenance reason\n Commits\n Time\n \n\n \n process-compose.yml\n services, readiness, scripts\n 14\n 2.8h\n \n \n .sops.yaml\n recipients, keys, environments\n 9\n 1.6h\n \n \n packages/env/\n typed env drift across apps\n 18\n 3.4h\n \n \n Caddyfile\n local domains and reverse proxy\n 7\n 1.2h\n \n \n Dockerfile\n runtime defaults and build context\n 11\n 2.1h\n \n \n docker-compose.yml\n dev databases, queues, object storage\n 13\n 2.5h\n \n \n .vscode/settings.json\n team editor and schema setup\n 8\n 1.1h\n \n \n tsconfig.json\n tooling defaults and path aliases\n 10\n 1.7h\n \n \n .gitignore\n generated files and local state\n 6\n 45m\n \n \n\n \n\n \n \n Stackpanel moves this into conventions.\n You still get the familiar files. You just stop hand-editing the same boilerplate every time the repo grows.\n \n \n 17h\n illustrative maintenance avoided per project\n \n \n\n \n You maintain: app code, app definitions, secret schemas, project choices\n Stackpanel handles: generated env, service glue, local routes, IDE/tooling defaults","choice":"maintenance-receipt","id":null,"timestamp":1777374587518} +{"type":"click","text":"illustrative setup tax\n Files your team keeps paying to maintain\n \n \n Not product code. Not differentiating. Just the conventional glue every repo needs.\n \n \n\n \n \n File\n Maintenance reason\n Commits\n Time\n \n\n \n process-compose.yml\n services, readiness, scripts\n 14\n 2.8h\n \n \n .sops.yaml\n recipients, keys, environments\n 9\n 1.6h\n \n \n packages/env/\n typed env drift across apps\n 18\n 3.4h\n \n \n Caddyfile\n local domains and reverse proxy\n 7\n 1.2h\n \n \n Dockerfile\n runtime defaults and build context\n 11\n 2.1h\n \n \n docker-compose.yml\n dev databases, queues, object storage\n 13\n 2.5h\n \n \n .vscode/settings.json\n team editor and schema setup\n 8\n 1.1h\n \n \n tsconfig.json\n tooling defaults and path aliases\n 10\n 1.7h\n \n \n .gitignore\n generated files and local state\n 6\n 45m\n \n \n\n \n\n \n \n Stackpanel moves this into conventions.\n You still get the familiar files. You just stop hand-editing the same boilerplate every time the repo grows.\n \n \n 17h\n illustrative maintenance avoided per project\n \n \n\n \n You maintain: app code, app definitions, secret schemas, project choices\n Stackpanel handles: generated env, service glue, local routes, IDE/tooling defaults","choice":"maintenance-receipt","id":null,"timestamp":1777374587912} +{"type":"click","text":"illustrative setup tax\n Files your team keeps paying to maintain\n \n \n Not product code. Not differentiating. Just the conventional glue every repo needs.\n \n \n\n \n \n File\n Maintenance reason\n Commits\n Time\n \n\n \n process-compose.yml\n services, readiness, scripts\n 14\n 2.8h\n \n \n .sops.yaml\n recipients, keys, environments\n 9\n 1.6h\n \n \n packages/env/\n typed env drift across apps\n 18\n 3.4h\n \n \n Caddyfile\n local domains and reverse proxy\n 7\n 1.2h\n \n \n Dockerfile\n runtime defaults and build context\n 11\n 2.1h\n \n \n docker-compose.yml\n dev databases, queues, object storage\n 13\n 2.5h\n \n \n .vscode/settings.json\n team editor and schema setup\n 8\n 1.1h\n \n \n tsconfig.json\n tooling defaults and path aliases\n 10\n 1.7h\n \n \n .gitignore\n generated files and local state\n 6\n 45m\n \n \n\n \n\n \n \n Stackpanel moves this into conventions.\n You still get the familiar files. You just stop hand-editing the same boilerplate every time the repo grows.\n \n \n 17h\n illustrative maintenance avoided per project\n \n \n\n \n You maintain: app code, app definitions, secret schemas, project choices\n Stackpanel handles: generated env, service glue, local routes, IDE/tooling defaults","choice":"maintenance-receipt","id":null,"timestamp":1777374588104} +{"type":"click","text":"illustrative setup tax\n Files your team keeps paying to maintain\n \n \n Not product code. Not differentiating. Just the conventional glue every repo needs.\n \n \n\n \n \n File\n Maintenance reason\n Commits\n Time\n \n\n \n process-compose.yml\n services, readiness, scripts\n 14\n 2.8h\n \n \n .sops.yaml\n recipients, keys, environments\n 9\n 1.6h\n \n \n packages/env/\n typed env drift across apps\n 18\n 3.4h\n \n \n Caddyfile\n local domains and reverse proxy\n 7\n 1.2h\n \n \n Dockerfile\n runtime defaults and build context\n 11\n 2.1h\n \n \n docker-compose.yml\n dev databases, queues, object storage\n 13\n 2.5h\n \n \n .vscode/settings.json\n team editor and schema setup\n 8\n 1.1h\n \n \n tsconfig.json\n tooling defaults and path aliases\n 10\n 1.7h\n \n \n .gitignore\n generated files and local state\n 6\n 45m\n \n \n\n \n\n \n \n Stackpanel moves this into conventions.\n You still get the familiar files. You just stop hand-editing the same boilerplate every time the repo grows.\n \n \n 17h\n illustrative maintenance avoided per project\n \n \n\n \n You maintain: app code, app definitions, secret schemas, project choices\n Stackpanel handles: generated env, service glue, local routes, IDE/tooling defaults","choice":"maintenance-receipt","id":null,"timestamp":1777374588505} +{"type":"click","text":"illustrative setup tax\n Files your team keeps paying to maintain\n \n \n Not product code. Not differentiating. Just the conventional glue every repo needs.\n \n \n\n \n \n File\n Maintenance reason\n Commits\n Time\n \n\n \n process-compose.yml\n services, readiness, scripts\n 14\n 2.8h\n \n \n .sops.yaml\n recipients, keys, environments\n 9\n 1.6h\n \n \n packages/env/\n typed env drift across apps\n 18\n 3.4h\n \n \n Caddyfile\n local domains and reverse proxy\n 7\n 1.2h\n \n \n Dockerfile\n runtime defaults and build context\n 11\n 2.1h\n \n \n docker-compose.yml\n dev databases, queues, object storage\n 13\n 2.5h\n \n \n .vscode/settings.json\n team editor and schema setup\n 8\n 1.1h\n \n \n tsconfig.json\n tooling defaults and path aliases\n 10\n 1.7h\n \n \n .gitignore\n generated files and local state\n 6\n 45m\n \n \n\n \n\n \n \n Stackpanel moves this into conventions.\n You still get the familiar files. You just stop hand-editing the same boilerplate every time the repo grows.\n \n \n 17h\n illustrative maintenance avoided per project\n \n \n\n \n You maintain: app code, app definitions, secret schemas, project choices\n Stackpanel handles: generated env, service glue, local routes, IDE/tooling defaults","choice":"maintenance-receipt","id":null,"timestamp":1777374589521} diff --git a/.superpowers/brainstorm/filetree-1777244419/.server-info b/.superpowers/brainstorm/filetree-1777244419/.server-info new file mode 100644 index 00000000..0f7c0ab9 --- /dev/null +++ b/.superpowers/brainstorm/filetree-1777244419/.server-info @@ -0,0 +1 @@ +{"type":"server-started","port":50966,"host":"127.0.0.1","url_host":"localhost","url":"http://localhost:50966","screen_dir":"/Users/cm/git/darkmatter/stackpanel/.superpowers/brainstorm/filetree-1777244419"} diff --git a/.superpowers/brainstorm/filetree-1777244419/.server.pid b/.superpowers/brainstorm/filetree-1777244419/.server.pid new file mode 100644 index 00000000..a275b045 --- /dev/null +++ b/.superpowers/brainstorm/filetree-1777244419/.server.pid @@ -0,0 +1 @@ +41378 diff --git a/.superpowers/brainstorm/filetree-1777244419/maintenance-receipt.html b/.superpowers/brainstorm/filetree-1777244419/maintenance-receipt.html new file mode 100644 index 00000000..c67b1fb9 --- /dev/null +++ b/.superpowers/brainstorm/filetree-1777244419/maintenance-receipt.html @@ -0,0 +1,310 @@ +

Configuration Maintenance Receipt

+

A calmer version: treat setup files like recurring line items. Stackpanel’s promise is that these stop being hand-managed costs.

+ + + +
+
+
+
illustrative setup tax
+
Files your team keeps paying to maintain
+
+
+ Not product code. Not differentiating. Just the conventional glue every repo needs. +
+
+ +
+
+ File + Maintenance reason + Commits + Time +
+ +
+ process-compose.yml + services, readiness, scripts + 14 + 2.8h +
+
+ .sops.yaml + recipients, keys, environments + 9 + 1.6h +
+
+ packages/env/ + typed env drift across apps + 18 + 3.4h +
+
+ Caddyfile + local domains and reverse proxy + 7 + 1.2h +
+
+ Dockerfile + runtime defaults and build context + 11 + 2.1h +
+
+ docker-compose.yml + dev databases, queues, object storage + 13 + 2.5h +
+
+ .vscode/settings.json + team editor and schema setup + 8 + 1.1h +
+
+ tsconfig.json + tooling defaults and path aliases + 10 + 1.7h +
+
+ .gitignore + generated files and local state + 6 + 45m +
+
+ +
+ +
+
+ Stackpanel moves this into conventions. + You still get the familiar files. You just stop hand-editing the same boilerplate every time the repo grows. +
+
+ 17h + illustrative maintenance avoided per project +
+
+ + +
diff --git a/.superpowers/brainstorm/manual-1777098555/.server-stopped b/.superpowers/brainstorm/manual-1777098555/.server-stopped new file mode 100644 index 00000000..cd819a72 --- /dev/null +++ b/.superpowers/brainstorm/manual-1777098555/.server-stopped @@ -0,0 +1 @@ +{"reason":"idle timeout","timestamp":1777100775264} diff --git a/.superpowers/brainstorm/prototypes-1777232838/.server-stopped b/.superpowers/brainstorm/prototypes-1777232838/.server-stopped new file mode 100644 index 00000000..fc23d912 --- /dev/null +++ b/.superpowers/brainstorm/prototypes-1777232838/.server-stopped @@ -0,0 +1 @@ +{"reason":"idle timeout","timestamp":1777234938415} diff --git a/.superpowers/brainstorm/prototypes-1777232838/.server.pid b/.superpowers/brainstorm/prototypes-1777232838/.server.pid new file mode 100644 index 00000000..cf642e8c --- /dev/null +++ b/.superpowers/brainstorm/prototypes-1777232838/.server.pid @@ -0,0 +1 @@ +31656 diff --git a/.superpowers/brainstorm/prototypes-1777232838/visual-prototypes.html b/.superpowers/brainstorm/prototypes-1777232838/visual-prototypes.html new file mode 100644 index 00000000..3183a431 --- /dev/null +++ b/.superpowers/brainstorm/prototypes-1777232838/visual-prototypes.html @@ -0,0 +1,369 @@ +

Visual Treatments for “What You Stop Managing”

+

Three ways to make the same idea concrete: Stackpanel turns scattered setup chores into a small source of truth that generates ordinary, inspectable files.

+ + + +
+
+
+
+

#2 Scatter → Source of Truth

+ best for “no lock-in” +
+
+
scattered chores
+
plain generated files
+ +
.env.example
+
docker-compose.yml
+
scripts/devshell.sh
+
.vscode/settings.json
+
Caddyfile
+ +
+
+
+
+ +
+ .stack/config.nix + codify conventions once +
+ +
packages/gen/env/*
+
process-compose services
+
.stack/gen/ide/*
+
local Caddy routes
+
.stack/state/stack.json
+
+

This one makes the “scattered files become a source of truth” story instantly visible. It also preserves the no-lock-in claim because the right side is still made of normal files.

+
+
+ +
+
+
+

#4 Manual Work Timeline

+ best for “stop wasting time” +
+
+
+

Every new repo today

+
1
Pick tool versions
Node, Bun, Go, Postgres, Redis, linters, CLIs.
+
2
Wire local services
Ports, domains, health checks, process commands.
+
3
Write env glue
Secrets, examples, generated types, app-specific loaders.
+
4
Document setup
Then debug every teammate’s machine anyway.
+ Repeated by hand, every project +
+
+

With Stackpanel

+
1
Describe the stack once
Apps, services, secrets, packages, modules.
+
2
Enter the devshell
nix develop --impure generates the boring parts.
+
3
Open Studio
See ports, processes, files, services, and config state.
+ Same outcome, fewer chores +
+
+

This version is the clearest emotional sell: the left side feels tedious, the right side feels obvious. It is less strong on no-lock-in unless paired with generated-file labels below.

+
+
+ +
+
+
+

#5 Exploded Blueprint

+ best for “how it works” +
+
+
+
+
+
+
+
+ +
+ .stack/ + apps + services + secrets + conventions +
+ +
typed env modulesplain TypeScript exports
+
process configservices, readiness, tasks
+
local domainsCaddy routes + ports
+
IDE workspacesVS Code, Zed, schemas
+
state JSONagent-readable config
+
Nix devshellpackages, hooks, scripts
+
+

This is the most “productized” visual. It explains that Stackpanel is not hiding the system; it is projecting one source of truth into the files and surfaces developers already know.

+
+
+
diff --git a/apps/api/.gitignore b/apps/api/.gitignore new file mode 100644 index 00000000..aa20f91f --- /dev/null +++ b/apps/api/.gitignore @@ -0,0 +1,4 @@ +.alchemy/ +.output/ +.wrangler/ +node_modules/ diff --git a/apps/api/alchemy.run.ts b/apps/api/alchemy.run.ts new file mode 100644 index 00000000..d5bc85fc --- /dev/null +++ b/apps/api/alchemy.run.ts @@ -0,0 +1,119 @@ +// apps/api/alchemy.run.ts +// +// Declarative cert + DNS orchestration for the api on Fly. +// +// The Fly machine is built and deployed by the CI workflow's +// `bun run build` → `nix build container-api` → skopeo push → flyctl +// deploy chain. This script handles the things that *aren't* the machine: +// +// 1. Ensure an ACME certificate for the public hostname exists on the +// Fly app (idempotent). +// 2. Look up the IP addresses Fly assigned to the app. +// 3. Create A + AAAA records on the stackpanel.com Cloudflare zone +// pointing at those IPs (proxy off — Fly terminates TLS). +// +// Replaces the previous `flyctl certs add` + manual Cloudflare DNS +// dance. Same Effect-native pattern as apps/web's domain binding via +// `@distilled.cloud/cloudflare` Workers. + +import { loadDeployEnv, resolveDeployStage } from "@stackpanel/infra/lib/deploy"; +import { + AppCertificatesAcmeCreate, + AppIPAssignmentsList, +} from "@distilled.cloud/fly-io/Operations"; +import { CredentialsFromEnv as FlyCredentialsFromEnv } from "@distilled.cloud/fly-io"; +import * as DNS from "@distilled.cloud/cloudflare/dns"; +import * as Stack from "alchemy-effect/Stack"; +import * as Effect from "effect/Effect"; +import * as Layer from "effect/Layer"; + +const PROJECT = "stackpanel"; +const SERVICE = "api"; + +const FLY_APP = "stackpanel-api"; +const STACKPANEL_ZONE = "d34628a3ab639230ff1f6dc1eb640eec"; + +// `appEnv` is our SOPS namespace (`prod` | `staging` | `dev`); CI sets +// FLY_IO_API_KEY from the FLY_API_TOKEN GH secret so the fly-io SDK +// picks it up via process.env. +const { stage, appEnv } = resolveDeployStage(); +await loadDeployEnv(SERVICE, appEnv); + +// Production lives at api.stackpanel.com; preview/staging deploys get +// api-.stackpanel.com so they don't collide with prod. +const hostnameFor = (stage: string): string => + stage === "production" ? "api.stackpanel.com" : `api-${stage}.stackpanel.com`; + +const program = Effect.gen(function* () { + if (stage === "dev") { + // Local/dev deploys serve from stackpanel-api.fly.dev directly — no + // custom hostname, no DNS records. + return { url: `https://${FLY_APP}.fly.dev` }; + } + + const hostname = hostnameFor(stage); + + // (1) Ensure ACME cert exists for the hostname. Idempotent: returns + // the existing cert if one's already on the app. + yield* AppCertificatesAcmeCreate({ app_name: FLY_APP, hostname }); + + // (2) Look up the IPs Fly assigned the app. Shared v4 + dedicated v6 + // is the default. We point DNS at whatever Fly returns rather than + // hard-coding 66.241.125.29. + const ipsResp = (yield* AppIPAssignmentsList({ app_name: FLY_APP })) as { + ips?: ReadonlyArray<{ ip?: string; service_name?: string; shared?: boolean }>; + }; + const ips = ipsResp.ips ?? []; + // Fly returns one row per assigned IP. Distinguish v4 (IPv4 dotted quad) + // from v6 (contains a colon) at the address level rather than relying on + // a `kind` field that the SDK schema doesn't expose. + const v4 = ips.find((i) => i.ip && !i.ip.includes(":"))?.ip; + const v6 = ips.find((i) => i.ip && i.ip.includes(":"))?.ip; + if (!v4 || !v6) { + return yield* Effect.fail( + new Error( + `Fly app ${FLY_APP} missing v4 or v6 IP (got: ${JSON.stringify(ips)})`, + ), + ); + } + + // (3) Reconcile DNS records: drop anything stale at this name, then + // create the A + AAAA pointing at Fly. Proxy off — Fly's ACME validation + // and TLS termination both need direct connections, not the CF proxy. + const existing = (yield* DNS.listRecords({ + zoneId: STACKPANEL_ZONE, + name: { exact: hostname }, + } as never)) as { result?: ReadonlyArray<{ id: string; name?: string; type?: string }> }; + for (const r of existing.result ?? []) { + if (r.name === hostname && (r.type === "A" || r.type === "AAAA")) { + yield* DNS.deleteRecord({ zoneId: STACKPANEL_ZONE, dnsRecordId: r.id }); + } + } + yield* DNS.createRecord({ + zoneId: STACKPANEL_ZONE, + name: hostname, + type: "A", + content: v4, + ttl: 1, + proxied: false, + }); + yield* DNS.createRecord({ + zoneId: STACKPANEL_ZONE, + name: hostname, + type: "AAAA", + content: v6, + ttl: 1, + proxied: false, + } as never); + + return { url: `https://${hostname}` }; +}); + +// Both providers' credentials read from process.env (set by loadDeployEnv). +const providers = Layer.mergeAll(FlyCredentialsFromEnv) as unknown as Layer.Layer< + any, + never, + any +>; + +export default Stack.make(`${PROJECT}-${SERVICE}`, providers)(program); diff --git a/apps/api/fly.toml b/apps/api/fly.toml new file mode 100644 index 00000000..f1403153 --- /dev/null +++ b/apps/api/fly.toml @@ -0,0 +1,33 @@ +# Generated by stackpanel - do not edit manually +# Regenerate by entering the devshell: nix develop --impure +# +# Deploy workflow (uses nix2container/dockerTools): +# 1. Build app: bun run build (in app directory) +# 2. Build container: container-build api +# 3. Push container: container-copy api docker://registry.fly.io/ +# 4. Deploy: flyctl deploy --config apps/api/fly.toml --image registry.fly.io/stackpanel-api:latest +# +# Or use turbo workflow: +# turbo run ship:api + +app = "stackpanel-api" +org = "darkmatter" + +# Build section removed - we use pre-built container images +# Container is built with nix2container/dockerTools and pushed via skopeo + +[env] +PORT = '3000' + +[http_service] +internal_port = 3000 +force_https = true +auto_stop_machines = "stop" +auto_start_machines = true +min_machines_running = 1 +processes = ["app"] + +[[vm]] +memory = "512mb" +cpu_kind = "shared" +cpus = 1 diff --git a/apps/api/package.json b/apps/api/package.json index d3b1159c..ccc22484 100644 --- a/apps/api/package.json +++ b/apps/api/package.json @@ -5,14 +5,23 @@ "type": "module", "scripts": { "dev": "bun run --watch src/index.ts", - "build": "bun build src/index.ts --outdir dist --target bun", + "build": "mkdir -p .output/server && bun build src/index.ts --outfile .output/server/index.mjs --target bun", "start": "bun run src/index.ts" }, "dependencies": { + "@stackpanel/api": "workspace:*", + "@stackpanel/auth": "workspace:*", + "@stackpanel/db": "workspace:*", + "@distilled.cloud/cloudflare": "catalog:", + "@distilled.cloud/fly-io": "catalog:", + "@trpc/server": "catalog:", + "alchemy-effect": "catalog:", + "effect": "catalog:", "hono": "catalog:" }, "devDependencies": { "@types/bun": "latest", + "@types/node": "^22.13.11", "typescript": "^5" } } diff --git a/apps/api/scripts/push-secrets.sh b/apps/api/scripts/push-secrets.sh new file mode 100755 index 00000000..f2c59eba --- /dev/null +++ b/apps/api/scripts/push-secrets.sh @@ -0,0 +1,58 @@ +#!/usr/bin/env bash +# ============================================================================== +# push-secrets.sh +# +# Decrypts the CI-accessible deploy-scope SOPS payload as dotenv format +# and pushes the subset of secrets the stackpanel-api Fly app needs. +# +# Reads from packages/gen/env/data/_envs/deploy.sops.json — which is +# encrypted against the GitHub Actions key (SECRETS_AGE_KEY_DEV) via the +# stackpanel codegen pipeline. Do NOT read .stack/secrets/vars/shared.sops.yaml +# here: it's encrypted only for humans and will fail in CI. +# +# DATABASE_URL is NOT set here — the deploy scope's POSTGRES_URL points at +# PlanetScale but the api uses Neon web_dev. Set it once manually: +# fly secrets set DATABASE_URL='postgres://...' --app stackpanel-api +# +# Usage: +# bash apps/api/scripts/push-secrets.sh # push to stackpanel-api +# FLY_APP=other bash apps/api/scripts/push-secrets.sh +# +# Requires: sops (3.9+ for --output-type dotenv), fly CLI, a key that +# decrypts the deploy payload (SOPS_AGE_KEY, ssh key, etc.). +# ============================================================================== +set -euo pipefail + +REPO_ROOT="$(git rev-parse --show-toplevel)" +DEPLOY_SOPS="${REPO_ROOT}/packages/gen/env/data/_envs/deploy.sops.json" +FLY_APP="${FLY_APP:-stackpanel-api}" + +if [[ ! -f "$DEPLOY_SOPS" ]]; then + echo "deploy sops payload not found: $DEPLOY_SOPS" >&2 + echo "run \`nix develop --impure\` once to regenerate it." >&2 + exit 1 +fi + +# dotenv output gives us KEY=VALUE lines directly. We select + rename a +# subset (AWS_SANDBOX_* → AWS_*) and append fixed non-secret env below. +SOURCE_ENV=$(sops --output-type dotenv -d "$DEPLOY_SOPS") + +{ + # Secrets from the deploy scope — renamed where the Fly app expects + # the unprefixed AWS var name. + echo "$SOURCE_ENV" | grep -E '^(BETTER_AUTH_SECRET|POLAR_ACCESS_TOKEN|POLAR_WEBHOOK_SECRET|POLAR_PRO_PRODUCT_ID_PRODUCTION|POLAR_FREE_PRODUCT_ID_PRODUCTION)=' + echo "$SOURCE_ENV" | grep -E '^AWS_SANDBOX_ACCESS_KEY_ID=' | sed 's/^AWS_SANDBOX_/AWS_/' + echo "$SOURCE_ENV" | grep -E '^AWS_SANDBOX_SECRET_ACCESS_KEY=' | sed 's/^AWS_SANDBOX_/AWS_/' + + # Fixed non-secret env — same across deploys of the production stage. + cat < { - return c.json({ name: "stackpanel-api", version: "0.0.1" }); -}); +// Origins allowed to call this API with credentials. The studio lives on +// local.stackpanel.com (production) or localhost during dev, so both need +// to be in the allowlist — `credentials: true` requires exact-match origins. +const allowedOrigins = (process.env.CORS_ALLOWED_ORIGINS ?? "") + .split(",") + .map((s: string) => s.trim()) + .filter(Boolean); + +const defaultOrigins = [ + "http://localhost:3000", + "http://localhost:5775", + "https://local.stackpanel.com", + "https://stackpanel.com", +]; + +const origins = allowedOrigins.length > 0 ? allowedOrigins : defaultOrigins; + +app.use( + "*", + cors({ + origin: origins, + credentials: true, + allowMethods: ["GET", "POST", "OPTIONS"], + allowHeaders: ["Content-Type", "Authorization", "Cookie"], + exposeHeaders: ["Set-Cookie"], + }), +); + +app.get("/", (c) => + c.json({ name: "stackpanel-api", version: "0.0.1" }), +); + +app.get("/health", (c) => + c.json({ + status: "ok", + region: process.env.FLY_REGION ?? process.env.REGION ?? "unknown", + timestamp: Date.now(), + }), +); + +// Better-Auth handler — covers /api/auth/* (sign-in, sign-up, session, +// social OAuth, Polar checkout/portal, and webhook mount). All routes +// emitted by the plugin tree are handled here. +app.on(["POST", "GET"], "/api/auth/*", (c) => auth.handler(c.req.raw)); -app.get("/health", (c) => { - return c.json({ - status: "ok", - region: process.env.REGION ?? "unknown", - timestamp: Date.now(), - }); -}); +// tRPC handler. Studio talks to this via @trpc/client httpBatchStreamLink. +app.all("/trpc/*", (c) => + fetchRequestHandler({ + endpoint: "/trpc", + req: c.req.raw, + router: appRouter, + createContext: () => + createTRPCContext({ headers: c.req.raw.headers, auth }), + }), +); export default { - port: Number(process.env.PORT ?? 3000), - fetch: app.fetch, + port: Number(process.env.PORT ?? 3000), + fetch: app.fetch, }; diff --git a/apps/docs/alchemy.run.ts b/apps/docs/alchemy.run.ts index 5be89a57..f1917f21 100644 --- a/apps/docs/alchemy.run.ts +++ b/apps/docs/alchemy.run.ts @@ -36,8 +36,36 @@ const program = Effect.gen(function* () { // resource only handles upload + binding wiring. const website = yield* Cloudflare.Worker("Docs", { main: ".open-next/worker.js", - assets: ".open-next/assets", + // OpenNext emits a plain Workers default export `{ fetch }` — the alchemy + // bootstrap that wraps `main` in `Layer.effect(tag, entry)` mis-handles + // that shape and the deployed worker throws CF 1101 on first request. + // `isExternal: true` skips the wrapper so the bundle keeps OpenNext's own + // entrypoint. + isExternal: true, + // Mirror apps/docs/wrangler.jsonc — OpenNext serves its own routing so the + // worker must run for missed asset paths, and we want the SPA-style + // trailing-slash handling for static MDX routes. + assets: { + directory: ".open-next/assets", + // OpenNext static incremental cache lives under `.open-next/cache`; preview + // copies it into assets, but CI `build:worker` does not. Mount the cache + // tree at the URL prefix OpenNext expects (`alchemy-effect` asset sources). + sources: [ + { directory: ".open-next/cache", prefix: "cdn-cgi/_next_cache" }, + ], + config: { + notFoundHandling: "none", + htmlHandling: "auto-trailing-slash", + runWorkerFirst: false, + }, + }, compatibility: { + // Must be >= 2026-03-17 — that's the date Cloudflare started providing + // node:perf_hooks as a native module. OpenNext (via Next.js's edge + // runtime) imports it transitively, and on earlier dates the unenv + // polyfill itself references node:perf_hooks, so the worker throws + // `No such module "node:perf_hooks"` on first request (CF error 1101). + date: "2026-03-17", flags: [ "nodejs_compat", "nodejs_compat_populate_process_env", diff --git a/apps/docs/content/docs/meta.json b/apps/docs/content/docs/meta.json index 3002fc30..c016d0b6 100644 --- a/apps/docs/content/docs/meta.json +++ b/apps/docs/content/docs/meta.json @@ -16,6 +16,7 @@ "extensions", "apps-ci", "deployment", + "stacks", "ide", "---Plugins---", "modules", diff --git a/apps/docs/content/docs/stacks/alchemy.mdx b/apps/docs/content/docs/stacks/alchemy.mdx new file mode 100644 index 00000000..3aea0751 --- /dev/null +++ b/apps/docs/content/docs/stacks/alchemy.mdx @@ -0,0 +1,104 @@ +--- +title: Alchemy +description: Resource-graph IaC for Cloudflare, AWS, Vercel, GitHub and Stripe — without managing Terraform yourself +icon: workflow +--- + +import { Callout } from "fumadocs-ui/components/callout"; + +The **Alchemy** Production Stack ships a maintained Nix flake that wires the [Alchemy](https://distilled.cloud) resource graph into your Stackpanel project. You declare resources in TypeScript, Stackpanel handles the deploy machinery, and we maintain the modules so you don't have to. + +## What's in the flake + +- **Per-app modules** that auto-wire your apps: `apps..alchemy.cloudflareWorker.enable = true` is enough to get a deploy. +- **Provider modules** for Cloudflare (Workers, Pages, R2, KV, Durable Objects, Queues, Hyperdrive), AWS (Lambda, S3, DynamoDB, IAM, KMS, ECR), Vercel, GitHub, and Stripe. +- **State storage** wired to filesystem (dev), S3 / R2 (staging + prod), or DO (per-PR preview environments). +- **Secrets bridge** that pulls SOPS-encrypted variables from your Stackpanel project and provisions them as worker bindings, env vars, or Secret resources. +- **Per-PR preview environments** — each PR branch deploys to a uniquely named stage and tears down on PR close. + +## Installation + +Add the flake input to your root `flake.nix`: + +```nix +{ + inputs = { + stackpanel.url = "github:darkmatter/stackpanel"; + stack-alchemy.url = "github:darkmatter/stack-alchemy"; + }; +} +``` + +Then enable the stack in `.stack/config.nix`: + +```nix +{ + stackpanel.stacks.alchemy = { + enable = true; + branch = "stable"; # community | stable | early + state = { + kind = "r2"; + bucket = "acme-alchemy-state"; + }; + }; +} +``` + +## A minimal example + +```nix +{ + stackpanel.apps.web = { + path = "apps/web"; + framework.tanstack-start.enable = true; + + alchemy.cloudflareWorker = { + enable = true; + domain = "app.acme.com"; + previewDomain = "*.preview.acme.dev"; + bindings = { + DB = { kind = "hyperdrive"; project = "acme-prod"; }; + SESSIONS = { kind = "kv"; namespace = "sessions"; }; + UPLOADS = { kind = "r2"; bucket = "acme-uploads"; }; + }; + }; + }; +} +``` + +That's it. Stackpanel generates the Alchemy script, wires bindings to env vars in your `@gen/env/web` package, and gives you `bun run deploy:web` (and a per-PR preview pipeline in CI). + +## What we maintain + +The Alchemy stack covers a moving surface — providers ship breaking changes regularly. Subscriptions get patches for: + +- **Cloudflare**: new bindings, Wrangler API changes, `routes` semantics, the Pages → Workers migration, etc. +- **AWS**: API version bumps, IAM policy hardening, KMS / Secrets Manager interactions. +- **Vercel**: project / deployment API changes, build-output API. +- **State backends**: R2 + DO compatibility as both move forward. +- **Alchemy core**: tracking upstream releases, deprecation notices, type changes. + +See [the SLA matrix](./#pricing) for patch turnaround by tier. + +## Tier differences + +| Resource family | Community | Team | Business | Enterprise | +| --- | --- | --- | --- | --- | +| Cloudflare Workers / Pages / R2 / KV | ✓ | ✓ | ✓ | ✓ | +| Cloudflare Durable Objects / Queues | ✓ | ✓ | ✓ | ✓ | +| AWS Lambda / S3 / DynamoDB | ✓ | ✓ | ✓ | ✓ | +| Vercel | — | ✓ | ✓ | ✓ | +| Stripe products & prices | — | ✓ | ✓ | ✓ | +| GitHub repo / secrets | — | ✓ | ✓ | ✓ | +| Custom AWS resources via raw SDK | — | — | ✓ | ✓ | +| Indemnification | — | — | — | ✓ | + + + All tiers get the same source code. The differences above describe which modules we **maintain** for which tiers — not what you're allowed to use. + + +## Related + +- [Production Stacks overview](./) +- [Deployment / Cloudflare](../deployment/cloudflare) +- [Deployment / Containers](../deployment/containers) diff --git a/apps/docs/content/docs/stacks/colmena.mdx b/apps/docs/content/docs/stacks/colmena.mdx new file mode 100644 index 00000000..bae2d7bf --- /dev/null +++ b/apps/docs/content/docs/stacks/colmena.mdx @@ -0,0 +1,114 @@ +--- +title: Colmena +description: Real Nix deployments to bare metal — atomic rollbacks, agenix secrets, Caddy + Step CA wired up +icon: server +--- + +import { Callout } from "fumadocs-ui/components/callout"; + +The **Colmena** Production Stack ships maintained NixOS modules and a [Colmena](https://colmena.cli.rs) deployment topology that take your Stackpanel apps to bare metal — Hetzner, OVH, your own datacenter — with the same atomic rollback semantics you'd expect from `nixos-rebuild switch`. + +If you already love NixOS for production but don't want to write the Caddy / Step CA / agenix glue yourself, this is the stack for you. + +## What's in the flake + +- **Per-app modules** that turn `apps..colmena.enable = true` into a systemd unit, a Caddy site, and (optionally) a database role. +- **Machine groups**: declare classes of hosts (`web`, `worker`, `db`) with shared base packages and group-level overrides. +- **agenix** wired to the same recipients as your Stackpanel SOPS files, so deployment secrets re-use the keys you already manage. +- **Caddy** configured with on-host TLS (Let's Encrypt) or per-machine Step CA certs. +- **Process supervision**: long-running workers ship as `systemd.services.` with restart policies and journal-based logging. +- **Atomic rollbacks**: every deploy uses `nixos-rebuild switch`. Roll back with `colmena rollback`. +- **Hetzner Cloud helper modules** for provisioning servers via the Hetzner API. + +## Installation + +Add the flake input: + +```nix +{ + inputs = { + stackpanel.url = "github:darkmatter/stackpanel"; + stack-colmena.url = "github:darkmatter/stack-colmena"; + }; +} +``` + +Enable the stack: + +```nix +{ + stackpanel.stacks.colmena = { + enable = true; + branch = "stable"; + hosts = { + "web-1.acme.io" = { group = "web"; ipv4 = "5.75.190.10"; }; + "web-2.acme.io" = { group = "web"; ipv4 = "5.75.190.11"; }; + "db-1.acme.io" = { group = "db"; ipv4 = "5.75.190.20"; }; + }; + }; +} +``` + +## A minimal example + +```nix +{ + stackpanel.apps.api = { + path = "apps/server"; + framework.hono.enable = true; + + colmena = { + enable = true; + group = "web"; # deploys to all hosts in the "web" group + port = 3000; # port the app binds to internally + domain = "api.acme.io"; + tls = { + provider = "letsencrypt"; # or "step-ca" + email = "ops@acme.io"; + }; + env = [ "DATABASE_URL" "STRIPE_SECRET_KEY" ]; # pulled from SOPS via agenix + }; + }; +} +``` + +Stackpanel generates the Colmena hive, the systemd unit, the Caddy site, and the agenix secret bindings. Deploy with: + +```bash +colmena apply switch --on @web +``` + +## What we maintain + +NixOS modules are the easy part — keeping the deployment glue working as upstream evolves is the work. Subscriptions get patches for: + +- **NixOS releases**: tracking 23.11 / 24.05 / 24.11 etc. with timely module updates. +- **Caddy module**: as `caddy` upstream changes its configuration semantics. +- **agenix integration**: tracking the agenix module + tooling. +- **Hetzner Cloud module**: as the Hetzner API evolves. +- **systemd hardening**: enabling new sandboxing options as they ship in systemd. +- **Step CA bridge**: as your Step CA root rotates, host certs are re-issued automatically. + +## Tier differences + +| Capability | Community | Team | Business | Enterprise | +| --- | --- | --- | --- | --- | +| Single-host deploys | ✓ | ✓ | ✓ | ✓ | +| Multi-host machine groups | ✓ | ✓ | ✓ | ✓ | +| Caddy + Let's Encrypt | ✓ | ✓ | ✓ | ✓ | +| Step CA TLS | — | ✓ | ✓ | ✓ | +| Hetzner Cloud provisioning | — | ✓ | ✓ | ✓ | +| Multi-region rollouts (canary) | — | — | ✓ | ✓ | +| Air-gapped binary cache mirror | — | — | — | ✓ | +| Custom NixOS module review | — | — | — | ✓ | + + + Colmena deploys assume you have SSH access to the target hosts. Stackpanel does not provision underlying servers for you (except via the optional Hetzner Cloud module). For fully managed compute, use the [Fly.io stack](./fly). + + +## Related + +- [Production Stacks overview](./) +- [Deployment / Containers](../deployment/containers) — for OCI images instead of NixOS hosts +- [Networking / Caddy](../networking) +- [Secrets / agenix](../secrets) diff --git a/apps/docs/content/docs/stacks/fly.mdx b/apps/docs/content/docs/stacks/fly.mdx new file mode 100644 index 00000000..19ffbe7d --- /dev/null +++ b/apps/docs/content/docs/stacks/fly.mdx @@ -0,0 +1,125 @@ +--- +title: Fly.io +description: Containerized apps on Fly machines — multi-region, autoscale, secrets sync, observability +icon: globe +--- + +import { Callout } from "fumadocs-ui/components/callout"; + +The **Fly.io** Production Stack ships maintained modules that turn your Stackpanel apps into Fly Machines, complete with multi-region distribution, autoscale, health probes, secret sync, and observability dashboards. + +If you want apps close to users in many places without running your own bare-metal fleet, this is the stack for you. + + + Fly itself ships an excellent CLI (`flyctl`). The Fly Production Stack adds the **maintained Nix module + image build pipeline** on top — so you get reproducible OCI images from Nix and a unified deploy flow with the rest of your Stackpanel apps. + + +## What's in the flake + +- **Per-app Fly modules** that auto-generate `apps//fly.toml`, a per-app deploy task, and wrapped `flyctl` commands like `fly-api status`. +- **Container builds** via [nix2container](https://github.com/nlewo/nix2container) — minimal, reproducible OCI images with no Dockerfile required. +- **Secrets sync** that pushes SOPS-encrypted Stackpanel variables into Fly secrets on deploy. +- **Multi-region machine layouts**: declare regions, machine sizes, autoscale rules, and minimum machines per region. +- **Health probes** wired to your app's `/healthz` (or custom path). +- **Observability**: Fly's built-in metrics shipped to your dashboards; optional OpenTelemetry exporters. +- **PR preview apps** with auto-cleanup on PR close (Business tier and above). + +## Installation + +Add the flake input: + +```nix +{ + inputs = { + stackpanel.url = "github:darkmatter/stackpanel"; + stack-fly.url = "github:darkmatter/stack-fly"; + }; +} +``` + +Enable the stack: + +```nix +{ + stackpanel.stacks.fly = { + enable = true; + branch = "stable"; + organization = "acme"; + defaultRegion = "iad"; + }; +} +``` + +## A minimal example + +```nix +{ + stackpanel.apps.api = { + path = "apps/server"; + framework.hono.enable = true; + + fly = { + enable = true; + appName = "acme-api"; + regions = [ "iad" "fra" "syd" ]; + machine = { + cpus = 1; + memory = "512mb"; + }; + autoscale = { + minMachines = 1; + maxMachines = 6; + autoStop = "suspend"; + autoStart = true; + }; + health = { + path = "/healthz"; + intervalSeconds = 15; + }; + env = [ "DATABASE_URL" "STRIPE_SECRET_KEY" ]; + }; + }; +} +``` + +Deploy: + +```bash +# Wrapped flyctl commands generated per deployable app +fly-api status +fly-api logs +fly-api deploy + +# Or via the per-app deploy task +bun --cwd packages/infra run deploy:api +``` + +## What we maintain + +Fly ships features fast — autoscale rules, machine classes, regional features. Subscriptions get patches for: + +- **Fly Machines API**: when Fly bumps their REST API or `flyctl` flags. +- **`fly.toml` schema**: tracking new fields (vm sizing, services, mounts, etc.). +- **nix2container**: rebuilding minimal OCI images as the base layers evolve. +- **Health probe semantics**: as Fly tightens the liveness / readiness contract. +- **Secret rotation**: automated re-sync on SOPS recipient changes. +- **Observability exporters**: Prometheus, OpenTelemetry collector configs. + +## Tier differences + +| Capability | Community | Team | Business | Enterprise | +| --- | --- | --- | --- | --- | +| Single-region deploys | ✓ | ✓ | ✓ | ✓ | +| Multi-region machines | ✓ | ✓ | ✓ | ✓ | +| Autoscale rules | ✓ | ✓ | ✓ | ✓ | +| Health probes + restart policies | ✓ | ✓ | ✓ | ✓ | +| Secret sync | ✓ | ✓ | ✓ | ✓ | +| PR preview apps with auto-cleanup | — | ✓ | ✓ | ✓ | +| OpenTelemetry exporter wiring | — | — | ✓ | ✓ | +| Custom machine class advice | — | — | — | ✓ | + +## Related + +- [Production Stacks overview](./) +- [Deployment / Fly.io](../deployment/fly) — the underlying Fly module reference +- [Deployment / Containers](../deployment/containers) diff --git a/apps/docs/content/docs/stacks/index.mdx b/apps/docs/content/docs/stacks/index.mdx new file mode 100644 index 00000000..159f3244 --- /dev/null +++ b/apps/docs/content/docs/stacks/index.mdx @@ -0,0 +1,75 @@ +--- +title: Production Stacks +description: Maintained Nix flakes that take your apps from devshell to production +icon: rocket +--- + +import { Callout } from "fumadocs-ui/components/callout"; + +**Production Stacks** are opinionated, maintained Nix flakes that handle a complete deployment pipeline — Terraform you don't have to write or maintain. The core Stackpanel framework is MIT and free forever; Production Stacks ship as managed subscriptions on top. + + + Stackpanel core (devshells, services, secrets, IDE integration) is and will + always be MIT-licensed and free. Production Stacks are an optional layer for + teams that want to outsource deployment maintenance to us. + + +## What you get + +When you subscribe to a Production Stack, you get: + +1. **A Nix flake** you import as a normal flake input — no code generation, no vendor lock-in. +2. **Composable modules** that follow the Stackpanel convention. Set `apps...enable = true` and the module wires up the rest. +3. **Maintained recipes** — when Cloudflare ships a breaking change, when AWS bumps an IAM API version, when Fly.io rolls out a new machine class, we update the recipe so you don't have to. +4. **A patch SLA** appropriate to your tier (best-effort on Community, 30-day on Team, 7-day on Business, 24-hour critical CVE on Enterprise). + +## The three stacks + +Stackpanel ships and maintains three Production Stacks today: + +### [Alchemy](./alchemy) + +**Resource-graph IaC for the full TypeScript stack.** Type-safe bindings for Cloudflare, AWS, Vercel, GitHub, Stripe, and more. Per-PR preview environments. Secrets pulled from your Stackpanel SOPS files. Flexible state storage (filesystem, S3, R2, DO). + +Best for: TypeScript teams shipping to Cloudflare Workers / Pages, or hybrid Cloudflare + AWS architectures. + +### [Colmena](./colmena) + +**Real Nix deployments to bare metal.** Atomic rollbacks via `nixos-rebuild`, machine groups, agenix-encrypted secrets, Caddy + Step CA wired up automatically. + +Best for: Hetzner / OVH / on-prem teams who want NixOS hosts without writing the operator playbook themselves. + +### [Fly.io](./fly) + +**Containerized apps at the edge.** Multi-region machines, health probes, autoscale rules, Fly secrets sync, built-in observability. Stackpanel generates `fly.toml` + a per-app deploy task and wraps `flyctl` for you. + +Best for: Bun / Hono / long-running workers that need to be near users globally. + +## Pricing + +| Tier | Branch | Patch SLA | Support | +| --- | --- | --- | --- | +| Community ($0) | `community` | Best-effort | GitHub Discussions | +| Team ($19/seat/mo) | `stable` | 30 days | Email · next business day | +| Business ($49/seat/mo) | `stable` + `early` | 7 days | Discord + 4-hour email | +| Enterprise (from $5k/mo) | `stable` + `early` | 24h critical CVE | Slack channel · on-call · named CSM | + +See the [pricing page](/pricing) for the full breakdown. + +## How updates work + +Every Production Stack is published as a Nix flake on a versioned branch. To pull a fix you bump the input: + +```bash +nix flake update stack-alchemy +``` + +You get the full diff in your PR — no surprise changes, no auto-applied patches. We treat your `flake.lock` as the source of truth for what's actually running. + + + Subscriptions cover **maintenance and updates**, not access. If your subscription lapses you keep using whatever version of the flake your `flake.lock` is pinned to. You just stop receiving new updates from us. + + +## Marketplace (planned) + +Third-party authors will be able to publish Production Stacks of their own through the Stackpanel marketplace, with a 80/20 revenue split (creator/Stackpanel). If you maintain a popular open-source deployment recipe and want a sustainable way to get paid for it, [get in touch](mailto:hello@stackpanel.dev). diff --git a/apps/docs/content/docs/stacks/meta.json b/apps/docs/content/docs/stacks/meta.json new file mode 100644 index 00000000..a7f993e1 --- /dev/null +++ b/apps/docs/content/docs/stacks/meta.json @@ -0,0 +1,6 @@ +{ + "title": "Production Stacks", + "description": "Maintained Nix flakes that get your apps from devshell to production", + "icon": "rocket", + "pages": ["index", "alchemy", "colmena", "fly"] +} diff --git a/apps/stackpanel-go/.stackpanel-root b/apps/stackpanel-go/.stackpanel-root new file mode 100644 index 00000000..7612f398 --- /dev/null +++ b/apps/stackpanel-go/.stackpanel-root @@ -0,0 +1 @@ +/Users/cm/git/darkmatter/stackpanel/apps/stackpanel-go diff --git a/apps/web/alchemy.run.ts b/apps/web/alchemy.run.ts index 6646ff6b..cd167019 100644 --- a/apps/web/alchemy.run.ts +++ b/apps/web/alchemy.run.ts @@ -44,35 +44,49 @@ const program = Effect.gen(function* () { let url: Output.Output = website.url; if (stage !== "dev") { - const hostname = - stage === "production" ? "stackpanel.com" : `${stage}.stackpanel.com`; + // Production binds two hostnames to the same worker: + // - apex stackpanel.com → marketing/landing (`/`, `/login`, …) + // - local.stackpanel.com → studio (mirrors local.drizzle.studio: the + // `/studio/*` routes talk to the user's machine via + // http://127.0.0.1:9876). + // Both ship the same bundle today; auth cookies are scoped to + // `.stackpanel.com` so a session from the apex carries into the studio. + // Non-prod stages only get the studio hostname — there's no marketing + // preview to host on the apex. + const hostnames = + stage === "production" + ? ["local.stackpanel.com", "stackpanel.com"] + : [`local.${stage}.stackpanel.com`]; + const primary = hostnames[0]!; url = Output.all(website.accountId, website.workerName).pipe( Output.mapEffect(([accountId, workerName]) => Effect.gen(function* () { - const existing = yield* Workers.listDomains({ - accountId, - hostname, - }); - const stale = existing.result.filter( - (d) => d.hostname === hostname && d.id, - ); - if (stale.length > 0) { - yield* Effect.log( - `[alchemy] purging ${stale.length} existing binding(s) at ${hostname}: ${stale - .map((d) => `${d.service ?? "?"}#${d.id}`) - .join(", ")}`, + for (const hostname of hostnames) { + const existing = yield* Workers.listDomains({ + accountId, + hostname, + }); + const stale = existing.result.filter( + (d) => d.hostname === hostname && d.id, ); + if (stale.length > 0) { + yield* Effect.log( + `[alchemy] purging ${stale.length} existing binding(s) at ${hostname}: ${stale + .map((d) => `${d.service ?? "?"}#${d.id}`) + .join(", ")}`, + ); + } + for (const d of stale) { + yield* Workers.deleteDomain({ accountId, domainId: d.id! }); + } + yield* Workers.putDomain({ + accountId, + hostname, + service: workerName, + zoneId: STACKPANEL_ZONE, + }); } - for (const d of stale) { - yield* Workers.deleteDomain({ accountId, domainId: d.id! }); - } - yield* Workers.putDomain({ - accountId, - hostname, - service: workerName, - zoneId: STACKPANEL_ZONE, - }); - return `https://${hostname}` as string | undefined; + return `https://${primary}` as string | undefined; }).pipe(Effect.orDie), ), ); diff --git a/apps/web/src/components/demo/demo-banner.tsx b/apps/web/src/components/demo/demo-banner.tsx new file mode 100644 index 00000000..54bd7ce4 --- /dev/null +++ b/apps/web/src/components/demo/demo-banner.tsx @@ -0,0 +1,48 @@ +"use client"; + +import { Button } from "@ui/button"; +import { ArrowRight, Sparkles, X } from "lucide-react"; +import { useState } from "react"; +import { useWaitlist } from "@/components/landing/waitlist-dialog"; + +export function DemoBanner() { + const [dismissed, setDismissed] = useState(false); + const waitlist = useWaitlist(); + + if (dismissed) return null; + + return ( +
+
+
+ + + Demo mode.{" "} + Realistic fixture data. Actions are no-ops. Pair a real local + agent to use the actual Studio. + +
+
+ + +
+
+
+ ); +} diff --git a/apps/web/src/components/demo/demo-fixtures.ts b/apps/web/src/components/demo/demo-fixtures.ts new file mode 100644 index 00000000..9f4093c7 --- /dev/null +++ b/apps/web/src/components/demo/demo-fixtures.ts @@ -0,0 +1,406 @@ +/** + * Static fixture data powering the /demo Studio. + * + * Numbers are deliberately consistent (the Postgres port appears in the + * apps panel, the variables panel and the dev-shell environment) so the + * demo feels like a real, coherent project — not a collection of + * disconnected screenshots. + */ + +export const DEMO_PROJECT = { + name: "acme-platform", + root: "/Users/sam/code/acme-platform", + branch: "feat/billing", + basePort: 6400, + devshellEntered: true, + team: [ + { name: "Sam Carter", email: "sam@acme.dev", role: "owner" }, + { name: "Priya Anand", email: "priya@acme.dev", role: "admin" }, + { name: "Jordan Liu", email: "jordan@acme.dev", role: "member" }, + { name: "Marta Vega", email: "marta@acme.dev", role: "member" }, + ], +} as const; + +export type DemoApp = { + id: string; + name: string; + stack: string; + domain: string; + url: string; + port: number; + status: "running" | "stopped" | "building"; + uptime?: string; + commit: string; + previewUrl?: string; + deployTarget?: "Cloudflare Workers" | "Fly.io" | "Hetzner (Colmena)"; +}; + +export const DEMO_APPS: DemoApp[] = [ + { + id: "web", + name: "web", + stack: "TanStack Start · Vite", + domain: "web.acme.local", + url: "https://web.acme.local", + port: 6400, + status: "running", + uptime: "2h 14m", + commit: "f3a4c12", + previewUrl: "https://feat-billing.web.acme.dev", + deployTarget: "Cloudflare Workers", + }, + { + id: "api", + name: "api", + stack: "Hono · Cloudflare Workers", + domain: "api.acme.local", + url: "https://api.acme.local", + port: 6401, + status: "running", + uptime: "2h 14m", + commit: "f3a4c12", + previewUrl: "https://feat-billing.api.acme.dev", + deployTarget: "Cloudflare Workers", + }, + { + id: "worker", + name: "worker", + stack: "Bun + BullMQ", + domain: "worker.acme.local", + url: "https://worker.acme.local", + port: 6402, + status: "running", + uptime: "1h 47m", + commit: "f3a4c12", + deployTarget: "Fly.io", + }, + { + id: "docs", + name: "docs", + stack: "Fumadocs · Next.js", + domain: "docs.acme.local", + url: "https://docs.acme.local", + port: 6403, + status: "building", + commit: "8b29ee1", + previewUrl: "https://feat-billing.docs.acme.dev", + deployTarget: "Cloudflare Workers", + }, +]; + +export type DemoService = { + id: string; + name: string; + kind: "global" | "network" | "orchestrator"; + status: "running" | "stopped"; + port?: number; + envVar?: string; + uptime?: string; + cpu?: string; + memory?: string; + connection?: string; + notes?: string; +}; + +export const DEMO_SERVICES: DemoService[] = [ + { + id: "postgres", + name: "PostgreSQL 17", + kind: "global", + status: "running", + port: 6410, + envVar: "STACKPANEL_POSTGRES_PORT", + uptime: "2h 14m", + cpu: "0.6%", + memory: "184 MB", + connection: "postgresql://acme:****@localhost:6410/acme", + }, + { + id: "redis", + name: "Redis 7", + kind: "global", + status: "running", + port: 6411, + envVar: "STACKPANEL_REDIS_PORT", + uptime: "2h 14m", + cpu: "0.1%", + memory: "12 MB", + connection: "redis://localhost:6411/0", + }, + { + id: "minio", + name: "MinIO", + kind: "global", + status: "running", + port: 6412, + envVar: "STACKPANEL_MINIO_PORT", + uptime: "2h 14m", + cpu: "0.4%", + memory: "92 MB", + connection: "http://localhost:6412 (console: 6413)", + }, + { + id: "caddy", + name: "Caddy reverse proxy", + kind: "network", + status: "running", + port: 443, + uptime: "2h 14m", + cpu: "0.2%", + memory: "28 MB", + notes: "Routes *.acme.local → app ports with TLS from Step CA", + }, + { + id: "step-ca", + name: "Step CA", + kind: "network", + status: "running", + port: 9000, + uptime: "2h 14m", + cpu: "0.0%", + memory: "16 MB", + notes: "Issues per-device certs trusted by your OS root store", + }, + { + id: "process-compose", + name: "process-compose", + kind: "orchestrator", + status: "running", + port: 8080, + uptime: "2h 14m", + cpu: "0.1%", + memory: "22 MB", + notes: "Supervises all dev processes with health probes", + }, +]; + +export type DemoVariable = { + key: string; + scope: "shared" | "app"; + app?: string; + dev: string; + staging: string; + prod: string; + encrypted?: boolean; +}; + +export const DEMO_VARIABLES: DemoVariable[] = [ + { + key: "DATABASE_URL", + scope: "shared", + dev: "postgresql://acme:****@localhost:6410/acme", + staging: "postgresql://****@neon.tech/acme-staging", + prod: "postgresql://****@neon.tech/acme-prod", + encrypted: true, + }, + { + key: "REDIS_URL", + scope: "shared", + dev: "redis://localhost:6411/0", + staging: "rediss://****@upstash.io", + prod: "rediss://****@upstash.io", + encrypted: true, + }, + { + key: "STRIPE_SECRET_KEY", + scope: "app", + app: "api", + dev: "sk_test_****", + staging: "sk_test_****", + prod: "sk_live_****", + encrypted: true, + }, + { + key: "RESEND_API_KEY", + scope: "app", + app: "api", + dev: "re_test_****", + staging: "re_test_****", + prod: "re_live_****", + encrypted: true, + }, + { + key: "PUBLIC_APP_URL", + scope: "app", + app: "web", + dev: "https://web.acme.local", + staging: "https://feat-billing.acme.dev", + prod: "https://app.acme.com", + }, + { + key: "FEATURE_BILLING_V2", + scope: "shared", + dev: "true", + staging: "true", + prod: "false", + }, +]; + +export type DemoNetworkRoute = { + host: string; + target: string; + tls: boolean; + app?: string; + notes?: string; +}; + +export const DEMO_NETWORK_ROUTES: DemoNetworkRoute[] = [ + { + host: "web.acme.local", + target: "http://127.0.0.1:6400", + tls: true, + app: "web", + }, + { + host: "api.acme.local", + target: "http://127.0.0.1:6401", + tls: true, + app: "api", + }, + { + host: "worker.acme.local", + target: "http://127.0.0.1:6402", + tls: true, + app: "worker", + }, + { + host: "docs.acme.local", + target: "http://127.0.0.1:6403", + tls: true, + app: "docs", + }, + { + host: "minio.acme.local", + target: "http://127.0.0.1:6412", + tls: true, + notes: "Object storage console + S3 API", + }, +]; + +export type DemoGenerated = { + path: string; + tool: string; + bytes: number; + updated: string; +}; + +export const DEMO_GENERATED_FILES: DemoGenerated[] = [ + { + path: ".vscode/settings.json", + tool: "stackpanel.ide", + bytes: 4_822, + updated: "2 minutes ago", + }, + { + path: ".vscode/extensions.json", + tool: "stackpanel.ide", + bytes: 1_204, + updated: "2 minutes ago", + }, + { + path: ".zed/settings.json", + tool: "stackpanel.ide", + bytes: 2_109, + updated: "2 minutes ago", + }, + { + path: "process-compose.yaml", + tool: "stackpanel.process-compose", + bytes: 7_680, + updated: "2 minutes ago", + }, + { + path: "Caddyfile", + tool: "stackpanel.network.caddy", + bytes: 3_412, + updated: "2 minutes ago", + }, + { + path: "packages/gen/env/src/web.ts", + tool: "stackpanel.secrets", + bytes: 5_604, + updated: "8 minutes ago", + }, + { + path: "packages/gen/env/src/api.ts", + tool: "stackpanel.secrets", + bytes: 6_241, + updated: "8 minutes ago", + }, + { + path: "apps/web/wrangler.jsonc", + tool: "stackpanel.deploy.alchemy", + bytes: 2_905, + updated: "1 hour ago", + }, +]; + +export type DemoActivity = { + at: string; + actor: string; + icon: + | "deploy" + | "secret" + | "shell" + | "code" + | "warn" + | "user"; + title: string; + detail?: string; +}; + +export const DEMO_ACTIVITY: DemoActivity[] = [ + { + at: "2 min ago", + actor: "sam@acme.dev", + icon: "shell", + title: "Entered devshell", + detail: "13 services healthy · 0 warnings", + }, + { + at: "8 min ago", + actor: "sam@acme.dev", + icon: "code", + title: "Edited .stack/config.nix", + detail: "Added api.app.cron.enable = true", + }, + { + at: "12 min ago", + actor: "priya@acme.dev", + icon: "secret", + title: "Rotated STRIPE_SECRET_KEY (prod)", + detail: "Re-keyed for 4 recipients", + }, + { + at: "47 min ago", + actor: "ci", + icon: "deploy", + title: "Deployed feat-billing → preview", + detail: "alchemy · web, api, docs · 28s", + }, + { + at: "1 hour ago", + actor: "jordan@acme.dev", + icon: "user", + title: "Joined the team", + detail: "Added AGE recipient and re-keyed dev secrets", + }, + { + at: "3 hours ago", + actor: "ci", + icon: "warn", + title: "Flake check warning", + detail: "Unused module argument `lib` in apps/worker", + }, +]; + +export const DEMO_HEALTH = { + devshellHash: "sha256-9f1c…74ab", + flakeCheck: "passing" as const, + openPorts: 13, + teamRecipients: 4, + encryptedFiles: 6, + disk: "12.4 GB available", + processesUp: 13, + processesTotal: 13, +}; diff --git a/apps/web/src/components/demo/demo-header.tsx b/apps/web/src/components/demo/demo-header.tsx new file mode 100644 index 00000000..28ae9bc5 --- /dev/null +++ b/apps/web/src/components/demo/demo-header.tsx @@ -0,0 +1,72 @@ +"use client"; + +import { Link } from "@tanstack/react-router"; +import { Badge } from "@ui/badge"; +import { Button } from "@ui/button"; +import { SidebarTrigger } from "@ui/sidebar"; +import { CheckCircle2, ExternalLink, GitBranch, Home } from "lucide-react"; +import { useWaitlist } from "@/components/landing/waitlist-dialog"; +import { DEMO_PROJECT } from "./demo-fixtures"; + +export function DemoHeader() { + const waitlist = useWaitlist(); + + return ( +
+ + +
+
+
+ +
+
+

+ {DEMO_PROJECT.name} +

+

+ {DEMO_PROJECT.root} +

+
+
+ + + + {DEMO_PROJECT.branch} + + + + + Devshell entered + +
+ +
+ + +
+
+ ); +} diff --git a/apps/web/src/components/demo/demo-sidebar.tsx b/apps/web/src/components/demo/demo-sidebar.tsx new file mode 100644 index 00000000..0cae3839 --- /dev/null +++ b/apps/web/src/components/demo/demo-sidebar.tsx @@ -0,0 +1,159 @@ +"use client"; + +import { Logo } from "@stackpanel/ui-core/logo"; +import { Link, useRouterState } from "@tanstack/react-router"; +import { Badge } from "@ui/badge"; +import { + Sidebar, + SidebarContent, + SidebarFooter, + SidebarGroup, + SidebarGroupContent, + SidebarGroupLabel, + SidebarHeader, + SidebarMenu, + SidebarMenuButton, + SidebarMenuItem, + SidebarRail, + useSidebar, +} from "@ui/sidebar"; +import { + AppWindow, + BookOpen, + FileCode, + Home, + Network, + Server, + Variable, +} from "lucide-react"; +import { cn } from "@/lib/utils"; + +type DemoNavItem = { + id: string; + label: string; + icon: React.ElementType; + to: string; + badge?: string; +}; + +const overviewItems: DemoNavItem[] = [ + { id: "overview", label: "Overview", icon: Home, to: "/demo" }, +]; + +const mainItems: DemoNavItem[] = [ + { id: "apps", label: "Apps", icon: AppWindow, to: "/demo/apps", badge: "4" }, + { + id: "services", + label: "Services", + icon: Server, + to: "/demo/services", + badge: "6", + }, + { + id: "variables", + label: "Variables / Secrets", + icon: Variable, + to: "/demo/variables", + }, + { id: "network", label: "Network", icon: Network, to: "/demo/network" }, + { id: "files", label: "Generated files", icon: FileCode, to: "/demo/files" }, +]; + +function NavItem({ item }: { item: DemoNavItem }) { + const routerState = useRouterState(); + const pathname = routerState.location.pathname; + const { state } = useSidebar(); + const isCollapsed = state === "collapsed"; + + const isActive = + item.to === "/demo" + ? pathname === "/demo" || pathname === "/demo/" + : pathname === item.to || pathname.startsWith(`${item.to}/`); + + const Icon = item.icon; + + return ( + + + + + {item.label} + {item.badge && !isCollapsed ? ( + + {item.badge} + + ) : null} + + + + ); +} + +export function DemoSidebar() { + const { state } = useSidebar(); + const isCollapsed = state === "collapsed"; + + return ( + + + + + {!isCollapsed && ( + + DEMO + + )} + + + + + + + + {overviewItems.map((item) => ( + + ))} + + + + + + Manage + + + {mainItems.map((item) => ( + + ))} + + + + + + + + + + + {!isCollapsed && Read the docs} + + + + + + + + ); +} diff --git a/apps/web/src/components/landing/comparison-section.tsx b/apps/web/src/components/landing/comparison-section.tsx new file mode 100644 index 00000000..f54bb670 --- /dev/null +++ b/apps/web/src/components/landing/comparison-section.tsx @@ -0,0 +1,226 @@ +import { Check, Minus, X } from "lucide-react"; + +type Cell = "yes" | "partial" | "no"; + +type Row = { + feature: string; + detail?: string; + stackpanel: Cell; + devenv: Cell; + docker: Cell; + paas: Cell; +}; + +const rows: Row[] = [ + { + feature: "Reproducible across machines", + detail: "Same Bun, Go, Postgres versions", + stackpanel: "yes", + devenv: "yes", + docker: "partial", + paas: "no", + }, + { + feature: "Deterministic shared ports", + detail: "Same ports on every laptop", + stackpanel: "yes", + devenv: "no", + docker: "no", + paas: "no", + }, + { + feature: "Real HTTPS in dev", + detail: "Internal CA + reverse proxy", + stackpanel: "yes", + devenv: "no", + docker: "no", + paas: "yes", + }, + { + feature: "Encrypted secrets in repo", + detail: "SOPS + AGE recipients in Nix", + stackpanel: "yes", + devenv: "partial", + docker: "no", + paas: "no", + }, + { + feature: "Type-safe env per app", + detail: "Generated TS / Go / Python", + stackpanel: "yes", + devenv: "no", + docker: "no", + paas: "no", + }, + { + feature: "IDE settings & extensions", + detail: "VS Code + Zed, version-controlled", + stackpanel: "yes", + devenv: "no", + docker: "no", + paas: "no", + }, + { + feature: "Visual studio for the team", + detail: "Web UI for non-Nix users", + stackpanel: "yes", + devenv: "no", + docker: "no", + paas: "yes", + }, + { + feature: "Maintained deployment recipes", + detail: "Production Stacks updated for you", + stackpanel: "yes", + devenv: "no", + docker: "no", + paas: "partial", + }, + { + feature: "No vendor lock-in", + detail: "Eject and the repo still works", + stackpanel: "yes", + devenv: "yes", + docker: "yes", + paas: "no", + }, + { + feature: "Self-hosted", + detail: "Runs on your laptop and your cloud", + stackpanel: "yes", + devenv: "yes", + docker: "yes", + paas: "no", + }, +]; + +const headers = [ + { + key: "stackpanel" as const, + title: "Stackpanel", + emphasis: true, + subtitle: "Open source", + }, + { + key: "devenv" as const, + title: "Raw Nix / devenv", + subtitle: "DIY", + }, + { + key: "docker" as const, + title: "Docker Compose", + subtitle: "Container-only", + }, + { + key: "paas" as const, + title: "Hosted PaaS", + subtitle: "Vercel · Render · Fly", + }, +]; + +function CellIcon({ value }: { value: Cell }) { + if (value === "yes") { + return ( + + + + ); + } + if (value === "partial") { + return ( + + + + ); + } + return ( + + + + ); +} + +export function ComparisonSection() { + return ( +
+
+
+

Comparison

+

+ Why not just use what we already have? +

+

+ Each of these tools solves part of the problem. Stackpanel composes + them — so you stop maintaining the glue. +

+
+ +
+
+ + + + + {headers.map((header) => ( + + ))} + + + + {rows.map((row) => ( + + + {headers.map((header) => ( + + ))} + + ))} + +
+ Capability + +
+ {header.title} + + {header.subtitle} + +
+
+

+ {row.feature} +

+ {row.detail ? ( +

+ {row.detail} +

+ ) : null} +
+
+ +
+
+
+
+ +

+ Comparison reflects out-of-the-box behavior on a fresh repo. Most + stacks can replicate parts of Stackpanel with enough custom tooling — + that's the tooling we're replacing. +

+
+
+ ); +} diff --git a/apps/web/src/components/landing/config-showcase-section.tsx b/apps/web/src/components/landing/config-showcase-section.tsx new file mode 100644 index 00000000..8a584bf4 --- /dev/null +++ b/apps/web/src/components/landing/config-showcase-section.tsx @@ -0,0 +1,197 @@ +import { ArrowRight, FileCode2, FolderTree } from "lucide-react"; + +const configLines: Array<{ + text: string; + tone?: "muted" | "comment" | "key" | "string" | "value" | "punct"; +}> = [ + { text: "{ pkgs, ... }: {", tone: "punct" }, + { text: " stackpanel = {", tone: "key" }, + { text: " enable = true;", tone: "value" }, + { text: ' name = "myapp";', tone: "value" }, + { text: "" }, + { text: " # Apps get sequential ports from the hashed base", tone: "comment" }, + { text: " apps = {", tone: "key" }, + { text: " web = { port = 0; }; # → :4200", tone: "value" }, + { text: " api = { port = 1; }; # → :4201", tone: "value" }, + { text: " };", tone: "punct" }, + { text: "" }, + { text: " # Background services managed by process-compose", tone: "comment" }, + { text: " globalServices = {", tone: "key" }, + { text: " enable = true;", tone: "value" }, + { text: " postgres.enable = true;", tone: "value" }, + { text: " redis.enable = true;", tone: "value" }, + { text: " minio.enable = true;", tone: "value" }, + { text: " };", tone: "punct" }, + { text: "" }, + { text: " # Real HTTPS for *.myapp.local", tone: "comment" }, + { text: " caddy.enable = true;", tone: "value" }, + { text: " step-ca.enable = true;", tone: "value" }, + { text: "" }, + { text: " # Editor settings + extensions, generated for the team", tone: "comment" }, + { text: " ide = {", tone: "key" }, + { text: " enable = true;", tone: "value" }, + { text: " vscode.enable = true;", tone: "value" }, + { text: " zed.enable = true;", tone: "value" }, + { text: " };", tone: "punct" }, + { text: "" }, + { text: " # SOPS recipients live in Nix", tone: "comment" }, + { text: " secrets.recipients = config.stackpanel.users.allKeys;", tone: "value" }, + { text: "" }, + { text: " # Project commands available on $PATH", tone: "comment" }, + { text: " scripts.dev = {", tone: "key" }, + { text: ' exec = "bun run --filter \'./apps/*\' dev";', tone: "value" }, + { text: ' description = "Start every app in dev mode";', tone: "value" }, + { text: " };", tone: "punct" }, + { text: " };", tone: "punct" }, + { text: "}", tone: "punct" }, +]; + +const toneClasses: Record, string> = { + muted: "text-muted-foreground", + comment: "text-muted-foreground/70 italic", + key: "text-foreground", + string: "text-emerald-300", + value: "text-foreground/90", + punct: "text-muted-foreground", +}; + +const generated: Array<{ + path: string; + description: string; +}> = [ + { + path: ".vscode/settings.json", + description: "Workspace settings + recommended extensions for VS Code", + }, + { + path: ".zed/settings.json", + description: "Language server config + Nix integration for Zed", + }, + { + path: ".stack/secrets/.sops.yaml", + description: "SOPS creation rules rendered from declared recipients", + }, + { + path: "packages/gen/env/src/.ts", + description: "Type-safe env modules per app with embedded payloads", + }, + { + path: ".stack/state/stack.json", + description: "Resolved ports, URLs, services for the Go agent", + }, + { + path: ".stack/gen/process-compose.yaml", + description: "Service definitions with health probes and dependencies", + }, + { + path: ".vscode/launch.json", + description: "Debug configurations contributed by app modules", + }, + { + path: "Caddyfile", + description: "Reverse-proxy routes for *.local hostnames with TLS", + }, +]; + +export function ConfigShowcaseSection() { + return ( +
+
+
+
+

+ One config, everything generated +

+

+ Declare your stack once. +

+

+ A single{" "} + .stack/config.nix{" "} + describes your apps, services, secrets, ports, IDE settings, and + deployment. Everything else — the dotfiles, the Caddyfile, the + SOPS rules, the type-safe env modules — is build output. +

+ +
+
+
+ + .stack/config.nix +
+
+ + + +
+
+
+
+									{configLines.map((line, idx) => (
+										
+ {line.text || "\u00A0"} +
+ ))} +
+
+
+ +

+ Don't want to write Nix? Open the studio — every option has a + form, and changes are written back to this file. +

+
+ +
+
+ + Builds into +
+

+ The dotfiles you would have written by hand. +

+

+ Generated files live where every tool expects them, in formats + every teammate already knows. Studio shows you which files are + stale, which module wrote them, and what would change if you + regenerated. +

+ +
+
+ + Generated by Stackpanel + + {generated.length} files + +
+
    + {generated.map((file) => ( +
  • + +
    +

    + {file.path} +

    +

    + {file.description} +

    +
    +
  • + ))} +
+
+
+
+
+
+ ); +} diff --git a/apps/web/src/components/landing/cta-section.tsx b/apps/web/src/components/landing/cta-section.tsx index 6293a60a..6cf5a665 100644 --- a/apps/web/src/components/landing/cta-section.tsx +++ b/apps/web/src/components/landing/cta-section.tsx @@ -1,36 +1,101 @@ +import { Link } from "@tanstack/react-router"; import { Button } from "@ui/button"; -import { ArrowRight } from "lucide-react"; +import { ArrowRight, BookOpen, Github, MonitorPlay } from "lucide-react"; +import { useWaitlist } from "./waitlist-dialog"; export function CTASection() { + const waitlist = useWaitlist(); return (
-
-
+
+
+
-

- Ready to own your infrastructure? +
+ + Private beta · Open source core (MIT) +
+ +

+ Reserve your spot in the beta.

-

- Stop paying per seat. Stop dealing with vendor lock-in. Start with - a platform that grows with your team. +

+ Stackpanel core ships free for everyone. Production Stacks land + as managed subscriptions on top. Join the beta to get early + access to both, plus a direct line to the team building it.

-
+
+ - +
-

- No credit card required · 14-day free trial · Cancel anytime +

+
+

+ Step 1 +

+

+ nix flake init -t … +

+
+
+

+ Step 2 +

+

+ direnv allow +

+
+
+

+ Step 3 +

+

dev

+
+
+ +

+ + Prefer to read first?{" "} + + Browse the docs +

diff --git a/apps/web/src/components/landing/dev-experience-section.tsx b/apps/web/src/components/landing/dev-experience-section.tsx index e13132d5..5a86658a 100644 --- a/apps/web/src/components/landing/dev-experience-section.tsx +++ b/apps/web/src/components/landing/dev-experience-section.tsx @@ -1,67 +1,123 @@ -import { Code2, GitBranch, Package, Sparkles } from "lucide-react"; +import { + ArrowDownToLine, + Check, + Globe, + KeyRound, + Sparkles, + TerminalSquare, +} from "lucide-react"; -export function DevExperienceSection() { - const features = [ - { - icon: Code2, - title: "Nix Flakes & Devenv", - description: - "A managed nix flake based on devenv. Enable toolchains, git hooks, and dev environments through the UI.", - }, - { - icon: Package, - title: "create-app Command", - description: - "Pre-configured to use your stack's tools. Creates a GitHub repo with turborepo template, CI/CD, and all the scaffolding.", - }, - { - icon: GitBranch, - title: "x install Integration", - description: - "Run x install neon to add Neon to your stack with full scaffolding. Works for dozens of services.", - }, - { - icon: Sparkles, - title: "Zero Install Tools", - description: - "Scripts and tools available in your PATH automatically. No manual installation required for your team.", - }, - ]; +const pillars = [ + { + icon: ArrowDownToLine, + title: "Onboarding without docs", + description: + "git clone, direnv allow, done. Devshell installs every runtime, generates IDE settings, drops scripts on $PATH, and opens the studio.", + stat: "≈ 2 commands", + }, + { + icon: Globe, + title: "Real URLs, real ports", + description: + "Hit https://web.myapp.local in any browser. OAuth callbacks, secure cookies, and webhooks behave the same as in production.", + stat: "TLS in dev", + }, + { + icon: KeyRound, + title: "Secrets that just work", + description: + "Add a teammate's AGE key, rekey the SOPS files, commit the diff. Their next direnv reload pulls the new keys with zero config.", + stat: "SOPS · AGE", + }, + { + icon: TerminalSquare, + title: "Project commands on $PATH", + description: + "Declared scripts (dev, lint, test, deploy) become real binaries. Every teammate runs the same command — no per-shell aliases.", + stat: "scripts.* in Nix", + }, +]; + +const onboardingSteps = [ + { command: "git clone …", detail: "Pull the repo as usual" }, + { command: "direnv allow", detail: "Devshell builds in the background" }, + { command: "dev", detail: "Apps + services come up; studio opens" }, +]; +export function DevExperienceSection() { return (
-
-

- Developer Experience -

-

- Local development, supercharged -

-

- The StackPanel isn't just for infrastructure. It's your - team's local development hub with managed dev shells, - toolchains, and scripts. -

-
+
+
+

+ Developer experience +

+

+ Onboarding measured in minutes, not days. +

+

+ The README on most repos starts with a 14-step setup guide. + Stackpanel replaces it with{" "} + direnv allow{" "} + — and a teammate is running the full stack on the same ports as + everyone else. +

-
- {features.map((feature, index) => ( -
-
- +
+
+ + + New hire, day one +
-

- {feature.title} -

-

- {feature.description} -

+
    + {onboardingSteps.map((step, idx) => ( +
  1. + + {idx + 1} + +
    +

    + $ {step.command} +

    +

    + {step.detail} +

    +
    + +
  2. + ))} +
- ))} +
+ +
+ {pillars.map((pillar) => ( +
+
+
+ +
+ + {pillar.stat} + +
+

+ {pillar.title} +

+

+ {pillar.description} +

+
+ ))} +
diff --git a/apps/web/src/components/landing/features-section.tsx b/apps/web/src/components/landing/features-section.tsx index 3f7c4eeb..48969edf 100644 --- a/apps/web/src/components/landing/features-section.tsx +++ b/apps/web/src/components/landing/features-section.tsx @@ -1,67 +1,113 @@ -import { GitBranch, Key, Layers, Server, Shield, Users } from "lucide-react"; +import { + Boxes, + Code2, + GitBranch, + Hash, + KeyRound, + Layers, + LockKeyhole, + Network, + Package, + Puzzle, + Server, + ShieldCheck, +} from "lucide-react"; -export function FeaturesSection() { - const features = [ - { - icon: Server, - title: "GitOps Infrastructure", - description: - "An internal repo that deploys your servers, load balancers, databases, and everything you need in production. We manage it for you.", - }, - { - icon: Shield, - title: "Internal CA & mTLS", - description: - "Deploy an internal Certificate Authority. Issue certs to all machines and team members for zero-trust networking.", - }, - { - icon: Users, - title: "SSO Authentication", - description: - "Built-in SSO auth system for frictionless team onboarding. Add team members in minutes, not days.", - }, - { - icon: Key, - title: "Secrets Management", - description: - "Manage secrets using age encryption with your team members' public keys. Secure by default.", - }, - { - icon: GitBranch, - title: "Tailscale Integration", - description: - "Private networking for your team out of the box. Share databases and services internally with zero friction.", - }, - { - icon: Layers, - title: "Scale-to-Zero", - description: - "Load balancers and services that scale to zero when not in use. Only pay for what you actually need.", - }, - ]; +const features = [ + { + icon: Boxes, + title: "Reproducible devshells", + description: + "flake.lock pins every package, runtime, and version. Every teammate gets the exact same Node, Bun, Go, Postgres — independent of their OS.", + tag: "Nix · devenv", + }, + { + icon: Hash, + title: "Deterministic ports", + description: + "Ports are hashed from your project name, then sequenced for apps and services. Same ports on every machine, no .env coordination, no clashes between projects.", + tag: "STACKPANEL_*_PORT", + }, + { + icon: Server, + title: "Service orchestration", + description: + "Postgres, Redis, Minio, Caddy, and Step CA managed by process-compose. One command to start the whole stack with health probes wired up.", + tag: "process-compose", + }, + { + icon: KeyRound, + title: "Encrypted secrets", + description: + "SOPS-encrypted YAML with AGE recipients declared in Nix. Add a teammate's public key, run rekey, commit the diff. No external KMS to manage.", + tag: "SOPS · AGE", + }, + { + icon: ShieldCheck, + title: "Real HTTPS in dev", + description: + "Step CA issues internal certificates and Caddy reverse-proxies your apps to https://*.local — no browser warnings, no self-signed cert wrangling.", + tag: "Step CA · Caddy", + }, + { + icon: Code2, + title: "IDE auto-config", + description: + "VS Code and Zed workspace settings, recommended extensions, and devshell loaders are generated and committed. New hires open the repo and the editor is ready.", + tag: ".vscode · .zed", + }, + { + icon: Package, + title: "Type-safe @gen/env", + description: + "Per-app codegen turns your secret schemas into typed TypeScript modules with embedded encrypted payloads. Import from @gen/env/ and ship.", + tag: "TS · Go · Python", + }, + { + icon: Puzzle, + title: "Extension registry", + description: + "Browse extensions in the studio and enable them with one click. Stackpanel writes the Nix config for you and contributes generated files, scripts, and panels.", + tag: "One-click install", + }, + { + icon: GitBranch, + title: "No vendor lock-in", + description: + "Generated files are standard config in standard locations. Stop using Stackpanel and the repo keeps working — there is nothing to migrate.", + tag: "Eject anytime", + }, +]; +export function FeaturesSection() { return (
-

Platform Features

-

- Everything you need to ship +

Platform

+

+ Everything that lives between code and production

- Self-hosted infrastructure that scales with your business. No - per-seat pricing, no vendor lock-in, just powerful tools that work. + Stackpanel collapses the dozens of files, services, and + integrations every team rebuilds from scratch into a single + declarative configuration.

-
+
{features.map((feature) => (
-
- +
+
+ +
+ + {feature.tag} +

{feature.title} @@ -72,6 +118,21 @@ export function FeaturesSection() {

))}
+ +
+ + + No data leaves your machine in dev + + + + Composes with devenv, flake-parts, and SST + + + + Local-first · works offline + +
); diff --git a/apps/web/src/components/landing/footer.tsx b/apps/web/src/components/landing/footer.tsx index 431f52c3..375884b5 100644 --- a/apps/web/src/components/landing/footer.tsx +++ b/apps/web/src/components/landing/footer.tsx @@ -1,87 +1,135 @@ import { Link } from "@tanstack/react-router"; import { Github, Twitter } from "lucide-react"; -export function Footer() { - const links = { - product: [ - { label: "Features", href: "#features" }, - { label: "Infrastructure", href: "#infrastructure" }, - { label: "DevEx", href: "#devex" }, - { label: "Pricing", href: "#pricing" }, +type LinkGroup = { + label: string; + items: Array<{ label: string; href: string; external?: boolean }>; +}; + +const linkGroups: LinkGroup[] = [ + { + label: "Product", + items: [ + { label: "Features", href: "/#features" }, + { label: "How it works", href: "/#how-it-works" }, + { label: "Production Stacks", href: "/#stacks" }, + { label: "Pricing", href: "/pricing" }, + { label: "Studio", href: "/studio" }, ], - resources: [ - { label: "Documentation", href: "#" }, - { label: "API Reference", href: "#" }, - { label: "Changelog", href: "#" }, - { label: "Status", href: "#" }, + }, + { + label: "Resources", + items: [ + { label: "Documentation", href: "/docs" }, + { label: "Quick start", href: "/docs/quick-start" }, + { label: "Why Stackpanel", href: "/docs/why" }, + { label: "Changelog", href: "/docs/changelog" }, ], - company: [ - { label: "About", href: "#" }, - { label: "Blog", href: "#" }, - { label: "Careers", href: "#" }, - { label: "Contact", href: "#" }, + }, + { + label: "Open source", + items: [ + { + label: "GitHub", + href: "https://github.com/darkmatter/stackpanel", + external: true, + }, + { + label: "Issues", + href: "https://github.com/darkmatter/stackpanel/issues", + external: true, + }, + { + label: "Discussions", + href: "https://github.com/darkmatter/stackpanel/discussions", + external: true, + }, + { + label: "Releases", + href: "https://github.com/darkmatter/stackpanel/releases", + external: true, + }, ], - legal: [ - { label: "Privacy", href: "#" }, - { label: "Terms", href: "#" }, - { label: "Security", href: "#" }, + }, + { + label: "Legal", + items: [ + { label: "Privacy", href: "/privacy" }, + { label: "Terms", href: "/terms" }, + { label: "Security", href: "/security" }, + { label: "License (MIT)", href: "/docs/license" }, ], - }; + }, +]; +export function Footer() { return (
-
-
- +
+
+
- - SP + + S
- - StackPanel + + Stackpanel -

- Your entire company, one panel. +

+ The open-source dev platform that turns one Nix config into a + reproducible environment, encrypted secrets, real HTTPS, and a + studio for everything else.

- - {Object.entries(links).map(([category, items]) => ( -
-

- {category} + {linkGroups.map((group) => ( +
+

+ {group.label}

@@ -89,9 +137,13 @@ export function Footer() { ))}
-
-

- © {new Date().getFullYear()} StackPanel. All rights reserved. +

+

+ © {new Date().getFullYear()} Stackpanel · MIT licensed · Built on + Nix, devenv, Caddy, SOPS, and process-compose +

+

+ Not affiliated with NixOS, devenv, or Cloudflare.

diff --git a/apps/web/src/components/landing/header.tsx b/apps/web/src/components/landing/header.tsx index c532caae..fa212468 100644 --- a/apps/web/src/components/landing/header.tsx +++ b/apps/web/src/components/landing/header.tsx @@ -3,72 +3,70 @@ import { Logo } from "@stackpanel/ui-core/logo"; import { Link } from "@tanstack/react-router"; import { Button } from "@ui/button"; -import { Menu, X } from "lucide-react"; +import { Github, Menu, X } from "lucide-react"; import { useState } from "react"; +import { useWaitlist } from "./waitlist-dialog"; + +const navItems = [ + { label: "How it works", href: "/#how-it-works" }, + { label: "Features", href: "/#features" }, + { label: "Stacks", href: "/#stacks" }, + { label: "Pricing", href: "/pricing" }, + { label: "Compare", href: "/#compare" }, + { label: "Demo", href: "/demo" }, + { label: "Docs", href: "/docs" }, +]; export function Header() { const [isMenuOpen, setIsMenuOpen] = useState(false); + const waitlist = useWaitlist(); return (
- {/*
- - SP - -
*/} - {/* - StackPanel - */}
-
+
+
@@ -88,52 +86,50 @@ export function Header() { {isMenuOpen && (
-

*/} -const PACKAGE_PREVIEW: NixpkgsPackage[] = [ - { - name: "postgresql", - attr_path: "pkgs.postgresql_16", - version: "16.3", - description: "Production-grade relational database with extensions support.", - license: "PostgreSQL", - homepage: "https://www.postgresql.org/", - }, +type AppPreview = { + id: string; + name: string; + stack: string; + status: "running" | "building" | "stopped"; + domain: string; + port: number; +}; + +const APP_PREVIEW: AppPreview[] = [ { - name: "redis", - attr_path: "pkgs.redis", - version: "7.2.4", - description: "In-memory data structure store used as a cache, database, and message broker.", - license: "BSD-3-Clause", - homepage: "https://redis.io/", + id: "web", + name: "web", + stack: "TanStack Start · Vite", + status: "running", + domain: "web.myapp.local", + port: 4200, }, { - name: "bun", - attr_path: "pkgs.bun", - version: "1.1.8", - description: "Fast JavaScript runtime, bundler, and test runner.", - license: "MIT", - homepage: "https://bun.sh/", + id: "api", + name: "api", + stack: "Hono · Cloudflare Workers", + status: "running", + domain: "api.myapp.local", + port: 4201, }, { - name: "caddy", - attr_path: "pkgs.caddy", - version: "2.7.6", - description: "Extensible web server with automatic HTTPS and great defaults.", - license: "Apache-2.0", - homepage: "https://caddyserver.com/", + id: "worker", + name: "queue-worker", + stack: "Go · process-compose", + status: "building", + domain: "—", + port: 4202, }, ]; -type AppPreview = { +type ServicePreview = { id: string; name: string; - stack: string; - status: "running" | "staging" | "deploying"; - domain: string; + envVar: string; port: number; - tasks: Record; + description: string; }; -const APP_PREVIEW: AppPreview[] = [ +const SERVICE_PREVIEW: ServicePreview[] = [ { - id: "api", - name: "api-gateway", - stack: "Bun · SST", - status: "running", - domain: "api.stackpanel.local", - port: 6401, - tasks: { - dev: { key: "dev", command: "bun run dev", env: {} }, - test: { key: "test", command: "bun test", env: {} }, - }, + id: "postgres", + name: "PostgreSQL 16", + envVar: "STACKPANEL_POSTGRES_PORT", + port: 4237, + description: "Hashed from project name · isolated per-project", }, { - id: "web", - name: "web", - stack: "React · TanStack", - status: "staging", - domain: "web.stackpanel.local", - port: 6402, - tasks: { - dev: { key: "dev", command: "bun run dev -- --host", env: {} }, - lint: { key: "lint", command: "bun run lint", env: {} }, - }, + id: "redis", + name: "Redis 7", + envVar: "STACKPANEL_REDIS_PORT", + port: 4252, + description: "Cache + pub/sub managed by process-compose", }, { - id: "worker", - name: "queue-worker", - stack: "Go · Temporal", - status: "deploying", - domain: "worker.stackpanel.local", - port: 6410, - tasks: { - dev: { key: "dev", command: "go run ./cmd/worker", env: {} }, - }, + id: "minio", + name: "Minio (S3)", + envVar: "STACKPANEL_MINIO_PORT", + port: 4263, + description: "Local object storage with presigned URLs", + }, + { + id: "caddy", + name: "Caddy + Step CA", + envVar: "STACKPANEL_CADDY_PORT", + port: 4280, + description: "Real HTTPS for *.myapp.local in dev", }, ]; type VariablePreview = { id: string; key: string; - type: string; - description: string; - environments: string[]; - linkedApps: string[]; + kind: "secret" | "config" | "service"; + scope: string; + masked?: string; }; const VARIABLE_PREVIEW: VariablePreview[] = [ { id: "DATABASE_URL", key: "DATABASE_URL", - type: "secret", - description: "Encrypted connection string for primary Postgres cluster.", - environments: ["dev", "staging", "prod"], - linkedApps: ["api-gateway", "queue-worker"], + kind: "secret", + scope: "api · worker", + masked: "ref+sops://prod.sops.yaml#/DATABASE_URL", }, { - id: "NEXT_PUBLIC_API_URL", - key: "NEXT_PUBLIC_API_URL", - type: "config", - description: "Public API endpoint exposed to frontend clients.", - environments: ["dev", "staging"], - linkedApps: ["web"], + id: "STACKPANEL_API_PORT", + key: "STACKPANEL_API_PORT", + kind: "service", + scope: "all apps", + masked: "4201", }, { - id: "TS_AUTH_DOMAIN", - key: "TS_AUTH_DOMAIN", - type: "service", - description: "Tailscale auth domain issued by Stackpanel network.", - environments: ["dev"], - linkedApps: ["web", "api-gateway"], + id: "VITE_PUBLIC_API_URL", + key: "VITE_PUBLIC_API_URL", + kind: "config", + scope: "web", + masked: "https://api.myapp.local", + }, + { + id: "AGE_RECIPIENTS", + key: "AGE_RECIPIENTS", + kind: "secret", + scope: "team · 4 keys", + masked: "age1qd…ek7c, age1n4…tlkq, +2", }, ]; +const STATUS_STYLES: Record = { + running: "border-emerald-400/40 bg-emerald-500/10 text-emerald-300", + building: "border-amber-400/40 bg-amber-500/10 text-amber-300", + stopped: "border-zinc-400/30 bg-zinc-500/10 text-zinc-300", +}; + +const VARIABLE_STYLES: Record = { + secret: + "bg-fuchsia-500/10 text-fuchsia-300 border border-fuchsia-400/30", + config: "bg-sky-500/10 text-sky-300 border border-sky-400/30", + service: "bg-emerald-500/10 text-emerald-300 border border-emerald-400/30", +}; + type PreviewSlide = { id: string; label: string; tagline: string; + icon: typeof Boxes; content: ReactNode; }; -const ROTATION_MS = 6200; +const ROTATION_MS = 6500; -function PackagesPreview() { +function PreviewFrame({ + title, + subtitle, + icon: Icon, + children, +}: { + title: string; + subtitle: string; + icon: typeof Boxes; + children: ReactNode; +}) { return ( -
- -
- {PACKAGE_PREVIEW.map((pkg) => ( - - ))} -
-
- - - Cached in devshell - - - - Services + runtimes, side-by-side - - - - Deterministic channels - +
+
+
+
+ +
+
+

{title}

+

{subtitle}

+
+
+ + Studio +
+
{children}
); } function AppsPreview() { - const statusStyles: Record = { - running: "border-emerald-400/40 bg-emerald-500/15 text-emerald-300", - staging: "border-amber-400/30 bg-amber-500/10 text-amber-300", - deploying: "border-blue-400/30 bg-blue-500/10 text-blue-300", - }; - return ( -
- +
{APP_PREVIEW.map((app) => ( - - -
-
- - - {app.name - .split("-") - .map((part) => part[0]) - .join("") - .slice(0, 2) - .toUpperCase()} - - -
-

{app.name}

-

{app.stack}

-
+
+
+
+
+ {app.name.slice(0, 2).toUpperCase()} +
+
+

+ {app.name} +

+

{app.stack}

- - {app.status} -
+ + {app.status === "running" ? ( + + ) : app.status === "building" ? ( + + ) : ( + + )} + {app.status} + +
+
+ + + {app.domain} + + + :{app.port} + +
+
+ ))} +
+ + ); +} -
- - - {app.domain} - - - - Port {app.port} - +function ServicesPreview() { + return ( + +
+ {SERVICE_PREVIEW.map((svc) => ( +
+
+
+
+ +
+
+

+ {svc.name} +

+

+ {svc.description} +

+
- -
- +
+

:{svc.port}

+

+ {svc.envVar} +

- - +
+
))}
-
+
); } function VariablesPreview() { return ( -
- -
- {VARIABLE_PREVIEW.map((variable) => { - const typeConfig = getTypeConfig(variable.type); - const Icon = typeConfig.icon; - - return ( - - -
-
-
- -
-
-

- {variable.key} -

-

- {variable.description} -

-
-
- - {typeConfig.label} - -
- -
-
- - Environments -
- {variable.environments.map((env) => ( - - {env} - - ))} -
-
-
- Linked apps -
- {variable.linkedApps.map((app) => ( - - {app} - - ))} -
-
+ +
+ {VARIABLE_PREVIEW.map((variable) => ( +
+
+
+ + {variable.kind} + +
+

+ {variable.key} +

+

+ {variable.masked} +

- - - ); - })} +
+ + {variable.scope} + +
+
+ ))}
-
+
+ + + Resolved at shell entry + + + + Type-safe codegen + +
+ ); } function StudioPreviewRotator() { const previews = useMemo( () => [ - { - id: "packages", - label: "Packages", - tagline: "Pin runtimes, services, and tools to devshell", - content: , - }, { id: "apps", label: "Apps", - tagline: "Ports, domains, and tasks ready to ship", + tagline: "Apps with stable ports, URLs, and tasks", + icon: Boxes, content: , }, + { + id: "services", + label: "Services", + tagline: "Postgres, Redis, Minio — orchestrated", + icon: Server, + content: , + }, { id: "variables", label: "Variables", - tagline: "Secrets + config scoped per environment", + tagline: "Encrypted secrets + computed config", + icon: KeyRound, content: , }, ], @@ -437,59 +421,75 @@ function StudioPreviewRotator() { return (
-
+
- - Studio previews + + + Studio preview - + {previews[activeIndex]?.tagline}
- Open the studio + Open the studio
+
- {previews.map((preview, index) => ( -
- {preview.content} +
+
+ + +
- ))} + + + http://localhost:9876 + +
+
+ {previews.map((preview, index) => ( +
+ {preview.content} +
+ ))} +
- {previews.map((preview, index) => ( - - ))} + key={preview.id} + onClick={() => setActiveIndex(index)} + type="button" + > + + {preview.label} + + ); + })}
); @@ -506,48 +506,72 @@ export function HeroSection() {
- The new localhost:3001 + Open source · Powered by Nix · Self-hosted
- {/*

- Build products not - plumbing. -

*/}

- From idea to production-ready app. StackPanel unifies - infrastructure, tooling, secrets, and local development into one - internal platform your whole team can access. + One .stack/config.nix + {" "}replaces dozens of config files. Reproducible dev environments, + encrypted secrets, deterministic ports, and real HTTPS — generated + for your whole team.{" "} + No Nix knowledge required.

-
+
+ + -
-
+
+
+ + Reproducible +
- + Self-hosted
- - Zero vendor lock-in + + No lock-in
- - Cost-effective + + Local-first +
+
+ + Works offline +
+
+ + MIT licensed
@@ -558,3 +582,16 @@ export function HeroSection() {
); } + +function HeroCTAs() { + const waitlist = useWaitlist(); + return ( + + ); +} diff --git a/apps/web/src/components/landing/how-it-works-section.tsx b/apps/web/src/components/landing/how-it-works-section.tsx new file mode 100644 index 00000000..7eba0ca1 --- /dev/null +++ b/apps/web/src/components/landing/how-it-works-section.tsx @@ -0,0 +1,174 @@ +import { + ArrowRight, + Box, + Cpu, + FileTerminal, + GitBranch, + MonitorPlay, + Radio, +} from "lucide-react"; + +const planes = [ + { + id: "nix", + number: "01", + icon: FileTerminal, + title: "Nix plane", + tagline: "Declarative source of truth", + description: + "Evaluates your config, computes ports from your project name, provisions the devshell, and generates files. Runs once on shell entry.", + highlights: [ + "flake-parts + devenv adapter", + "Per-app code generation", + "SOPS recipients in Nix config", + ], + }, + { + id: "agent", + number: "02", + icon: Cpu, + title: "Local agent", + tagline: "Bridge to your environment", + description: + "A Go binary on localhost:9876 that wraps Nix evaluation, manages services via process-compose, watches files, and serves the studio.", + highlights: [ + "REST + Connect-RPC + SSE", + "JWT pairing flow", + "Works fully offline", + ], + }, + { + id: "studio", + number: "03", + icon: MonitorPlay, + title: "Web studio", + tagline: "Manage everything visually", + description: + "A React app for browsing extensions, managing services, editing config, viewing generated files, and resolving secrets — without writing Nix.", + highlights: [ + "Real-time SSE updates", + "Form-based config editor", + "Per-extension panels", + ], + }, +]; + +export function HowItWorksSection() { + return ( +
+
+
+

+ How it works +

+

+ Three planes, one project +

+

+ Stackpanel runs as a Nix configuration, a local Go agent, and a web + studio. Each plane has a clear job — together they replace the + boilerplate that lives between your code and production. +

+
+ +
+
+
+ {planes.map((plane, index) => ( +
+
+
+
+ +
+
+

+ {plane.number} +

+

+ {plane.title} +

+
+
+ {index < planes.length - 1 && ( + + )} +
+

+ {plane.tagline} +

+

+ {plane.description} +

+
    + {plane.highlights.map((highlight) => ( +
  • + + {highlight} +
  • + ))} +
+
+ ))} +
+
+ +
+
+
+
+ +
+
+

+ Git is the deploy target +

+

+ Studio writes to your real config files. Diffs show up in + code review like any other change. +

+
+
+
+
+ +
+
+

+ Real-time, locally +

+

+ SSE streams config and service updates from the agent — no + polling, no cloud round-trips. +

+
+
+
+
+ +
+
+

+ Eject without migration +

+

+ Generated files live in standard locations. Stop using + Stackpanel and your repo keeps working. +

+
+
+
+
+
+
+ ); +} diff --git a/apps/web/src/components/landing/index.ts b/apps/web/src/components/landing/index.ts index f23cb87f..c2368923 100644 --- a/apps/web/src/components/landing/index.ts +++ b/apps/web/src/components/landing/index.ts @@ -1,9 +1,14 @@ +export { ComparisonSection } from "./comparison-section"; +export { ConfigShowcaseSection } from "./config-showcase-section"; export { CTASection } from "./cta-section"; export { DevExperienceSection } from "./dev-experience-section"; export { FeaturesSection } from "./features-section"; export { Footer } from "./footer"; export { Header } from "./header"; export { HeroSection } from "./hero-section"; +export { HowItWorksSection } from "./how-it-works-section"; export { InfrastructureSection } from "./infrastructure-section"; +export { PricingSection } from "./pricing-section"; +export { ProductionStacksSection } from "./production-stacks-section"; export { StatsSection } from "./stats-section"; export { TerminalSection } from "./terminal-section"; diff --git a/apps/web/src/components/landing/infrastructure-section.tsx b/apps/web/src/components/landing/infrastructure-section.tsx index 808dc0f9..ff3f8629 100644 --- a/apps/web/src/components/landing/infrastructure-section.tsx +++ b/apps/web/src/components/landing/infrastructure-section.tsx @@ -1,71 +1,172 @@ +import { Link } from "@tanstack/react-router"; import { Button } from "@ui/button"; import { - BarChart3, + ArrowRight, + Cloud, Database, HardDrive, Lock, Network, - Search, + Radio, + ShieldCheck, + Workflow, } from "lucide-react"; -export function InfrastructureSection() { - const services = [ - { icon: Search, name: "ELK Stack", description: "Full logging and search" }, - { icon: Database, name: "PostgreSQL", description: "Managed databases" }, - { icon: Network, name: "Load Balancers", description: "Scale-to-zero LBs" }, - { icon: HardDrive, name: "Object Storage", description: "S3-compatible" }, - { - icon: BarChart3, - name: "Monitoring", - description: "Prometheus + Grafana", - }, - { icon: Lock, name: "Vault", description: "Secrets management" }, - ]; +type Service = { + icon: typeof Database; + name: string; + description: string; + envVar: string; + tag: string; +}; + +const coreServices: Service[] = [ + { + icon: Database, + name: "PostgreSQL", + description: "Local cluster with persistent data dir, ready for migrations.", + envVar: "STACKPANEL_POSTGRES_PORT", + tag: "global", + }, + { + icon: Radio, + name: "Redis", + description: "Single-node cache for sessions, queues, and rate limits.", + envVar: "STACKPANEL_REDIS_PORT", + tag: "global", + }, + { + icon: HardDrive, + name: "MinIO", + description: "S3-compatible object storage with admin console exposed.", + envVar: "STACKPANEL_MINIO_PORT", + tag: "global", + }, + { + icon: Network, + name: "Caddy", + description: "Reverse proxy that wires *.local hostnames to your apps.", + envVar: "STACKPANEL_CADDY_PORT", + tag: "network", + }, + { + icon: ShieldCheck, + name: "Step CA", + description: "Internal certificate authority — real HTTPS in dev, no warnings.", + envVar: "STACKPANEL_STEP_CA_PORT", + tag: "network", + }, + { + icon: Workflow, + name: "process-compose", + description: "Health probes, dependencies, and restart policies for everything above.", + envVar: "STACKPANEL_PC_PORT", + tag: "orchestrator", + }, +]; + +const tagStyles: Record = { + global: "border-emerald-500/30 bg-emerald-500/10 text-emerald-300", + network: "border-sky-500/30 bg-sky-500/10 text-sky-300", + orchestrator: "border-purple-500/30 bg-purple-500/10 text-purple-300", +}; +export function InfrastructureSection() { return (
-
-
-

Infrastructure

-

- One-click deploy robust systems +
+
+

+ Local infrastructure +

+

+ Production-shaped services on your laptop.

- Deploy an entire ELK stack, a Kubernetes cluster, or a managed - database with a single click. We manage the complexity so you can - focus on building. + Stackpanel runs the same data stores you use in production — + Postgres, Redis, MinIO — orchestrated by{" "} + process-compose{" "} + with health probes and dependency ordering.

- All services are self-hosted on your infrastructure. No data - leaves your network, and costs scale linearly with actual usage. + Caddy and an internal Step CA give you real HTTPS at clean + hostnames like{" "} + + https://api.myapp.local + {" "} + — so OAuth, secure cookies, and webhooks behave like prod.

-
- + +
+ +
+
+ + + Same shape, your cloud + +
+

+ Modules can target NixOS or container runtimes for staging and + production. Same config language, same generated Caddyfile, + same SOPS recipients — different host. +

-
- {services.map((service) => ( +
+ {coreServices.map((service) => (
-
- +
+
+ +
+ + {service.tag} + +
+
+

+ {service.name} +

+

+ {service.description} +

+
+
+ + + {service.envVar} +
-

- {service.name} -

-

- {service.description} -

))}
diff --git a/apps/web/src/components/landing/pricing-section.tsx b/apps/web/src/components/landing/pricing-section.tsx new file mode 100644 index 00000000..f26404dc --- /dev/null +++ b/apps/web/src/components/landing/pricing-section.tsx @@ -0,0 +1,223 @@ +import { Link } from "@tanstack/react-router"; +import { Button } from "@ui/button"; +import { ArrowRight, Check } from "lucide-react"; +import { cn } from "@/lib/utils"; +import { useWaitlist } from "./waitlist-dialog"; + +type CTA = + | { label: string; kind: "waitlist"; tier: string } + | { label: string; kind: "link"; to: string } + | { label: string; kind: "email"; href: string }; + +type Tier = { + id: string; + name: string; + tagline: string; + price: string; + priceDetail: string; + highlight?: boolean; + cta: CTA; + bullets: string[]; +}; + +const tiers: Tier[] = [ + { + id: "community", + name: "Community", + tagline: "Solo dev. Free forever.", + price: "$0", + priceDetail: "1 seat · no card required", + cta: { label: "Join the waitlist", kind: "waitlist", tier: "community" }, + bullets: [ + "Stackpanel core (MIT)", + "All 3 stacks on community branch", + "Best-effort patches", + "GitHub Discussions support", + ], + }, + { + id: "team", + name: "Team", + tagline: "For shipping teams.", + price: "$19", + priceDetail: "per user / month, billed monthly", + highlight: true, + cta: { label: "Request beta access", kind: "waitlist", tier: "team" }, + bullets: [ + "Everything in Community", + "Stable branch of every Production Stack", + "30-day patch SLA", + "Email support, next business day", + ], + }, + { + id: "business", + name: "Business", + tagline: "For platform teams.", + price: "$49", + priceDetail: "per user / month, billed monthly", + cta: { label: "Request beta access", kind: "waitlist", tier: "business" }, + bullets: [ + "Everything in Team", + "7-day patch SLA + early access channel", + "Multi-org, SSO, audit logs", + "Discord channel + 4-hour email response", + ], + }, + { + id: "enterprise", + name: "Enterprise", + tagline: "For companies that ship the world.", + price: "Custom", + priceDetail: "from $5,000 / month", + cta: { label: "Talk to us", kind: "email", href: "mailto:sales@stackpanel.dev" }, + bullets: [ + "24-hour critical CVE SLA", + "Air-gapped mirror license", + "Slack channel, on-call, named CSM", + "Indemnification, SCIM, custom RBAC", + ], + }, +]; + +export function PricingSection() { + const waitlist = useWaitlist(); + return ( +
+
+
+

Pricing

+

+ Free dev environment. Paid production support. +

+

+ The Stackpanel core is MIT and free forever. Subscribe when + you're ready to outsource the maintenance of your production + deploys to a team that does it full-time. +

+
+ +
+ {tiers.map((tier) => ( +
+
+
+

+ {tier.name} +

+ {tier.highlight ? ( + + Most popular + + ) : null} +
+

+ {tier.tagline} +

+
+ +
+

+ {tier.price} +

+

+ {tier.priceDetail} +

+
+ + {tier.cta.kind === "waitlist" ? ( + + ) : tier.cta.kind === "link" ? ( + + ) : ( + + )} + +
    + {tier.bullets.map((bullet) => ( +
  • + + {bullet} +
  • + ))} +
+
+ ))} +
+ +
+

+ Same Production Stacks across every tier. You pay for SLA, support, + and team features — not for access to the recipes. +

+ +
+
+
+ ); +} diff --git a/apps/web/src/components/landing/production-stacks-section.tsx b/apps/web/src/components/landing/production-stacks-section.tsx new file mode 100644 index 00000000..bca05db9 --- /dev/null +++ b/apps/web/src/components/landing/production-stacks-section.tsx @@ -0,0 +1,293 @@ +import { Link } from "@tanstack/react-router"; +import { Button } from "@ui/button"; +import { + ArrowRight, + CheckCircle2, + Cloud, + Globe2, + HardDrive, + type LucideIcon, + Network, + Server, + Workflow, +} from "lucide-react"; + +type Stack = { + id: string; + name: string; + tagline: string; + icon: LucideIcon; + targets: string[]; + bullets: string[]; + example: { line: string; tone?: "comment" | "value" | "punct" }[]; + docHref: string; +}; + +const stacks: Stack[] = [ + { + id: "alchemy", + name: "Alchemy", + tagline: + "Resource-graph IaC for the full TypeScript stack — Cloudflare, AWS, Vercel.", + icon: Workflow, + targets: ["Cloudflare", "AWS", "Vercel", "GitHub", "Stripe"], + bullets: [ + "Type-safe bindings generated into your app", + "Per-PR preview environments out of the box", + "Secrets pulled straight from .stack/secrets", + "State stored in your repo or your S3 bucket", + ], + example: [ + { line: "# .stack/config.nix", tone: "comment" }, + { line: "apps.web = {", tone: "punct" }, + { line: ' framework = "nextjs";', tone: "value" }, + { line: " alchemy = {", tone: "punct" }, + { line: ' target = "cloudflare"; # or "aws" | "vercel"', tone: "value" }, + { line: " previews = true;", tone: "value" }, + { line: " };", tone: "punct" }, + { line: "};", tone: "punct" }, + ], + docHref: "/docs/stacks/alchemy", + }, + { + id: "colmena", + name: "Colmena", + tagline: + "NixOS deploys for the people who want full control — Hetzner, bare metal, your own racks.", + icon: Server, + targets: ["Hetzner CAX/CCX", "Bare metal", "Any NixOS host", "Tailscale"], + bullets: [ + "Atomic switch with one-command rollback", + "Machine groups for canary + production fleets", + "Secrets via agenix, recipients managed in Nix", + "Caddy + Step CA wired identically to dev", + ], + example: [ + { line: "# .stack/config.nix", tone: "comment" }, + { line: "apps.api = {", tone: "punct" }, + { line: " colmena = {", tone: "punct" }, + { line: ' host = "cax21.fra";', tone: "value" }, + { line: " replicas = 2;", tone: "value" }, + { line: " rollback.enable = true;", tone: "value" }, + { line: " };", tone: "punct" }, + { line: "};", tone: "punct" }, + ], + docHref: "/docs/stacks/colmena", + }, + { + id: "fly", + name: "Fly.io", + tagline: + "Containerized apps at the edge — regional placement, Fly volumes, Fly Postgres.", + icon: Globe2, + targets: ["Fly Machines", "Fly Postgres", "Fly Volumes", "Tigris (S3)"], + bullets: [ + "Multi-region machines from one Nix config", + "Health probes, autoscale, and graceful drain", + "Fly secrets sync from .stack/secrets at deploy", + "Built-in observability via Fly Metrics", + ], + example: [ + { line: "# .stack/config.nix", tone: "comment" }, + { line: "apps.api.fly = {", tone: "punct" }, + { line: ' regions = [ "iad" "fra" "sin" ];', tone: "value" }, + { line: " machines = 3;", tone: "value" }, + { line: ' postgres.cluster = "myapp-pg";', tone: "value" }, + { line: " autoscale.maxMachines = 10;", tone: "value" }, + { line: "};", tone: "punct" }, + ], + docHref: "/docs/stacks/fly", + }, +]; + +const toneClasses: Record = { + comment: "text-muted-foreground/70 italic", + value: "text-foreground/90", + punct: "text-muted-foreground", +}; + +const promiseBullets: { icon: LucideIcon; text: string }[] = [ + { + icon: CheckCircle2, + text: "Same-day patches when nixpkgs ships breaking updates", + }, + { + icon: CheckCircle2, + text: "Tested against every flake.lock bump before release", + }, + { + icon: CheckCircle2, + text: "Provider API drift handled before it breaks your deploys", + }, +]; + +export function ProductionStacksSection() { + return ( +
+
+
+

+ Production stacks +

+

+ Deploy without becoming a platform team. +

+

+ Production Stacks are maintained Nix flake inputs that take your + app from{" "} + .stack/config.nix{" "} + all the way to production. Stick to the conventions and you should + never need more than one option flip. +

+ +
+ + apps.<myapp>.nextjs.enable = true; + +
+
+ +
+ {stacks.map((stack) => ( +
+
+
+
+ +
+
+

+ {stack.name} +

+

+ Stable · maintained +

+
+
+
+ +

+ {stack.tagline} +

+ +
+
+ {stack.targets.map((target) => ( + + {target} + + ))} +
+
+ +
+
+ Example +
+
+									{stack.example.map((row, i) => (
+										
+ {row.line || "\u00A0"} +
+ ))} +
+
+ +
    + {stack.bullets.map((bullet) => ( +
  • + + {bullet} +
  • + ))} +
+
+ ))} +
+ +
+
+
+ + Our promise +
+

+ You write the convention. We keep it green. +

+

+ Stacks are versioned Nix flake inputs. Subscribers get the + private stable channel and a maintenance commitment — same-day + patches, tested against every nixpkgs and provider API change. +

+ +
+ + +
+
+ +
    + {promiseBullets.map((item) => ( +
  • + +

    + {item.text} +

    +
  • + ))} +
  • + +
    +

    + Marketplace coming soon +

    +

    + Third-party creators can ship and sell their own + Production Stacks — Stackpanel takes 20%, you keep 80%. +

    +
    +
  • +
+
+ +

+ + Solo developers always have free access to the community branch of + every stack. +

+
+
+ ); +} diff --git a/apps/web/src/components/landing/stats-section.tsx b/apps/web/src/components/landing/stats-section.tsx index 03312e38..095ea591 100644 --- a/apps/web/src/components/landing/stats-section.tsx +++ b/apps/web/src/components/landing/stats-section.tsx @@ -1,21 +1,54 @@ +import { FileCode2, Hash, Layers, Network } from "lucide-react"; + export function StatsSection() { const stats = [ - { value: "80%", label: "reduction in setup time" }, - { value: "Zero", label: "vendor lock-in" }, - { value: "10x", label: "faster onboarding" }, - { value: "$0", label: "per-seat pricing" }, + { + icon: FileCode2, + value: "1", + label: "config file", + detail: ".stack/config.nix declares it all", + }, + { + icon: Hash, + value: "Deterministic", + label: "ports", + detail: "Hashed from project name", + }, + { + icon: Network, + value: "60+", + label: "agent endpoints", + detail: "REST + Connect-RPC + SSE", + }, + { + icon: Layers, + value: "Zero", + label: "vendor lock-in", + detail: "Generated files look hand-written", + }, ]; return (
-
+
{stats.map((stat) => ( -
-

+

+
+ +
+

{stat.value}

-

{stat.label}

+

+ {stat.label} +

+

+ {stat.detail} +

))}
diff --git a/apps/web/src/components/landing/terminal-section.tsx b/apps/web/src/components/landing/terminal-section.tsx index af2ba50f..c0b3ca35 100644 --- a/apps/web/src/components/landing/terminal-section.tsx +++ b/apps/web/src/components/landing/terminal-section.tsx @@ -3,127 +3,175 @@ import { useState } from "react"; import { cn } from "@/lib/utils"; -export function TerminalSection() { - const [activeTab, setActiveTab] = useState("create"); +type TerminalLine = { + text: string; + tone?: "info" | "success" | "warning" | "muted" | "highlight"; +}; + +type Tab = { + id: string; + label: string; + prompt: string; + command: string; + lines: TerminalLine[]; +}; + +const tabs: Tab[] = [ + { + id: "init", + label: "Bootstrap", + prompt: "~/myapp $", + command: "nix flake init -t github:darkmatter/stackpanel#default", + lines: [ + { text: "→ Cloning template into ./", tone: "info" }, + { text: "✓ flake.nix written", tone: "success" }, + { text: "✓ .stack/config.nix written", tone: "success" }, + { text: "✓ .envrc written (direnv)", tone: "success" }, + { text: "" }, + { text: "Next:", tone: "muted" }, + { text: " direnv allow # build & enter the devshell", tone: "muted" }, + { text: " dev # start every app + service", tone: "muted" }, + { text: " stackpanel studio # open Studio in your browser", tone: "muted" }, + ], + }, + { + id: "shell", + label: "Enter devshell", + prompt: "~/myapp $", + command: "direnv allow", + lines: [ + { text: "direnv: loading .envrc", tone: "muted" }, + { text: "→ Evaluating .stack/config.nix", tone: "info" }, + { text: "→ Building devshell (cached: 142 / 144 derivations)", tone: "info" }, + { text: "✓ Toolchain ready: bun 1.x · go 1.25 · postgres 16", tone: "success" }, + { text: "✓ IDE config generated for VS Code + Zed", tone: "success" }, + { text: "✓ Ports allocated: web :4200 · api :4201 · postgres :4210", tone: "success" }, + { text: "✓ Stackpanel agent listening on http://localhost:9876", tone: "success" }, + { text: "" }, + { text: "STACKPANEL_PROJECT=myapp", tone: "muted" }, + { text: "STACKPANEL_POSTGRES_PORT=4210", tone: "muted" }, + { text: "Welcome back, charles. Type `dev` to start.", tone: "highlight" }, + ], + }, + { + id: "services", + label: "Run the stack", + prompt: "(myapp) $", + command: "dev", + lines: [ + { text: "→ process-compose: starting myapp", tone: "info" }, + { text: "✓ step-ca READY internal CA + ACME", tone: "success" }, + { text: "✓ caddy READY https://*.myapp.local", tone: "success" }, + { text: "✓ postgres READY :4210", tone: "success" }, + { text: "✓ redis READY :4211", tone: "success" }, + { text: "✓ minio READY :4212 console :4213", tone: "success" }, + { text: "✓ web READY https://web.myapp.local", tone: "success" }, + { text: "✓ api READY https://api.myapp.local", tone: "success" }, + { text: "" }, + { text: "→ Studio: https://studio.myapp.local", tone: "highlight" }, + ], + }, + { + id: "secrets", + label: "Add a secret", + prompt: "(myapp) $", + command: "stackpanel secrets edit dev", + lines: [ + { text: "→ Decrypting .stack/secrets/dev.sops.yaml with local AGE key", tone: "info" }, + { text: "→ Opening $EDITOR…", tone: "muted" }, + { text: "+ STRIPE_SECRET_KEY: sk_test_…", tone: "success" }, + { text: "✓ Re-encrypted for 4 recipients", tone: "success" }, + { text: "✓ Regenerated @gen/env/api with new field", tone: "success" }, + { text: "" }, + { text: "Next:", tone: "muted" }, + { text: " git add .stack/secrets/dev.sops.yaml packages/gen/env", tone: "muted" }, + { text: " git commit -m 'feat: stripe secret'", tone: "muted" }, + ], + }, +]; - const tabs = [ - { id: "create", label: "Create App" }, - { id: "install", label: "Install Service" }, - { id: "deploy", label: "Deploy" }, - ]; +const toneClasses: Record, string> = { + info: "text-muted-foreground", + success: "text-accent", + warning: "text-yellow-400", + muted: "text-muted-foreground/80", + highlight: "font-semibold text-accent", +}; - const terminalContent: Record = - { - create: { - command: "create-app payments-api --template=api", - output: [ - "→ Creating new application...", - "✓ Initialized turborepo workspace", - "✓ Applied stack configuration (neon, redis, observability)", - "✓ Created GitHub repo: acme-corp/payments-api", - "✓ Configured CI/CD pipeline", - "✓ Added to StackPanel dashboard", - "", - "cd payments-api && nix develop", - ], - }, - install: { - command: "x install neon", - output: [ - "→ Installing Neon integration...", - "✓ Added @neondatabase/serverless to dependencies", - "✓ Created lib/db.ts with connection pool", - "✓ Added DATABASE_URL to secrets", - "✓ Updated nix flake with pg_dump tools", - "✓ Generated migration scaffolding", - "", - "Run 'x db:migrate' to create your first migration", - ], - }, - deploy: { - command: "x deploy production", - output: [ - "→ Deploying to production...", - "✓ Building container image (2.3s)", - "✓ Pushing to internal registry", - "✓ Updating GitOps repo", - "✓ ArgoCD syncing deployment", - "✓ Health checks passing", - "", - "🚀 Live at https://payments-api.acme-corp.com", - ], - }, - }; +export function TerminalSection() { + const [activeTab, setActiveTab] = useState(tabs[0].id); + const current = tabs.find((tab) => tab.id === activeTab) ?? tabs[0]; return ( -
+
-

CLI Tools

-

- Powerful commands at your fingertips +

CLI

+

+ Real commands, no proprietary glue

- Tools automatically available in your PATH. No installation - needed—just enter your dev shell and start building. + The{" "} + stackpanel CLI + speaks Nix, SOPS, and process-compose. Every command operates on + standard files in your repo.

-
-
-
-
+
+
+
+
{tabs.map((tab) => ( ))}
-
+
-
-
- $ - - {terminalContent[activeTab].command} +
+
+ {current.prompt} + + {current.command}
- {terminalContent[activeTab].output.map((line, i) => ( + {current.lines.map((line, i) => (
- {line} + {line.text}
))}
+ +

+ Same command works on macOS, Linux, and NixOS — same versions, same + output. +

diff --git a/apps/web/src/components/landing/waitlist-dialog.tsx b/apps/web/src/components/landing/waitlist-dialog.tsx new file mode 100644 index 00000000..38aad1cf --- /dev/null +++ b/apps/web/src/components/landing/waitlist-dialog.tsx @@ -0,0 +1,351 @@ +"use client"; + +import { Button } from "@ui/button"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from "@ui/dialog"; +import { Input } from "@ui/input"; +import { Label } from "@ui/label"; +import { Textarea } from "@ui/textarea"; +import { useMutation } from "@tanstack/react-query"; +import { + ArrowRight, + CheckCircle2, + ExternalLink, + Loader2, + PlayCircle, +} from "lucide-react"; +import { + createContext, + type ReactNode, + useCallback, + useContext, + useMemo, + useState, +} from "react"; +import { Link } from "@tanstack/react-router"; +import { toast } from "sonner"; +import { useTRPC } from "@/utils/trpc"; +import { cn } from "@/lib/utils"; + +type WaitlistOpenOptions = { + source?: string; + tier?: string; +}; + +type WaitlistContextValue = { + open: (opts?: WaitlistOpenOptions) => void; + close: () => void; +}; + +const WaitlistContext = createContext(null); + +/** + * Provides a single global waitlist dialog mounted at the layout level. + * Any descendant can call `useWaitlist().open({ source: "..." })` to + * trigger it; only one instance is ever rendered. + */ +export function WaitlistProvider({ children }: { children: ReactNode }) { + const [openState, setOpenState] = useState(false); + const [source, setSource] = useState(); + const [tier, setTier] = useState(); + + const open = useCallback((opts?: WaitlistOpenOptions) => { + setSource(opts?.source); + setTier(opts?.tier); + setOpenState(true); + }, []); + + const close = useCallback(() => setOpenState(false), []); + + const value = useMemo( + () => ({ open, close }), + [open, close], + ); + + return ( + + {children} + + + ); +} + +export function useWaitlist(): WaitlistContextValue { + const ctx = useContext(WaitlistContext); + if (!ctx) { + throw new Error("useWaitlist must be used within a "); + } + return ctx; +} + +type WaitlistDialogProps = { + open: boolean; + onOpenChange: (open: boolean) => void; + source?: string; + tier?: string; +}; + +function WaitlistDialog({ + open, + onOpenChange, + source, + tier, +}: WaitlistDialogProps) { + const trpc = useTRPC(); + const [email, setEmail] = useState(""); + const [name, setName] = useState(""); + const [company, setCompany] = useState(""); + const [notes, setNotes] = useState(""); + const [submitted, setSubmitted] = useState(null); + + const referrer = + typeof document !== "undefined" ? document.referrer || undefined : undefined; + + const join = useMutation( + trpc.waitlist.join.mutationOptions({ + onSuccess: (data) => { + setSubmitted({ alreadyOnList: data.alreadyOnList }); + if (data.alreadyOnList) { + toast.success("You're already on the list — we'll be in touch."); + } else { + toast.success("You're on the list. We'll send a beta invite soon."); + } + }, + onError: (err) => { + toast.error(err.message ?? "Could not join the waitlist."); + }, + }), + ); + + const reset = useCallback(() => { + setEmail(""); + setName(""); + setCompany(""); + setNotes(""); + setSubmitted(null); + }, []); + + const handleOpenChange = useCallback( + (next: boolean) => { + if (!next) { + setTimeout(reset, 200); + } + onOpenChange(next); + }, + [onOpenChange, reset], + ); + + const handleSubmit = useCallback( + (event: React.FormEvent) => { + event.preventDefault(); + if (!email.trim()) return; + join.mutate({ + email: email.trim(), + name: name.trim() || undefined, + company: company.trim() || undefined, + notes: notes.trim() || undefined, + source: tier ? `${source ?? "unknown"}.${tier}` : source, + referrer, + }); + }, + [email, name, company, notes, source, tier, referrer, join], + ); + + const isPending = join.isPending; + + return ( + + + {submitted ? ( + handleOpenChange(false)} + /> + ) : ( +
+ + + Join the Stackpanel beta + + + We're rolling out access in waves. Tell us a bit about what + you're building and we'll send you an invite when there's a + slot. + + + +
+
+ + setEmail(e.target.value)} + disabled={isPending} + /> +
+ +
+
+ + setName(e.target.value)} + disabled={isPending} + /> +
+
+ + setCompany(e.target.value)} + disabled={isPending} + /> +
+
+ +
+ +