Cloud-gate v1: apps/api, hosted alchemy state, Polar subs, landing + demo studio#14
Cloud-gate v1: apps/api, hosted alchemy state, Polar subs, landing + demo studio#14cooper (czxtm) merged 47 commits intomainfrom
Conversation
Organizations (user-facing: "workspaces") become the scope for paid features, licenses, and state ownership. User-session extended with active_organization_id so every request resolves an organization context. Uses Better-Auth's built-in organization plugin; roles owner/admin/member. Schema follows the project's snake_case DB / camelCase TS convention. Unblocks: protectedPaidProcedure (stackpanel-9uo), hosted state workspace scoping. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
paidProcedure extends protectedProcedure and gates cloud features on an
active Pro subscription. Reads from a local user_subscription mirror
(populated by the Polar webhook — separate task) so every API call is
a single indexed SELECT, not a Polar round-trip.
plan is a stable internal identifier ("free" | "pro") decoupled from
Polar product IDs so product churn never touches gating code.
Unblocks: hosted state router, any future paid cloud feature.
Beads: stackpanel-9uo.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Per-organization state with envelope encryption: master KMS key ─► per-org DEK (32 bytes) ─► AES-256-GCM over blob Each organization has exactly one DEK, wrapped by the master KMS key aliased `alias/stackpanel-secrets` (override via STACKPANEL_KMS_ALIAS). Plaintext DEKs never touch disk; the master key never leaves AWS. Router surface: get / put / list / delete / listStages — all gated on paidProcedure. put/delete require expectedVersion for optimistic concurrency so two racing deploys can't silently clobber each other. Organization is resolved from the session's activeOrganizationId, never from client input. Beads: stackpanel-9zb (schema + encryption), stackpanel-ehz (tRPC router). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Wires the Polar webhooks plugin to a callback bundle that translates subscription.* events into user_subscription rows. Polar product id → internal plan name mapping lives in polar-webhooks.ts so product churn never touches gating code. Event handling: - created/active/updated/uncanceled -> upsert with subscription fields - canceled -> mark status=canceled, keep plan (access until period ends) - revoked -> downgrade plan=free, status=revoked Idempotency: polar_event table dedupes on `<eventType>:<subscriptionId>`. Same subscription can't transition through the same state twice, so replay safety with no race windows. Mount-on-secret: webhook endpoint only registers when POLAR_WEBHOOK_SECRET is set. Missing secret = server boots but paid features refuse everyone. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Turns the stub apps/api Hono server into the production cloud API for api.stackpanel.com. Mounts Better-Auth (sign-in, sign-up, Polar checkout and webhook endpoints) at /api/auth/*, tRPC routers at /trpc/*. Runs on Fly with Node/Bun, not Cloudflare Workers. Paid procedures use node:crypto and @aws-sdk/client-kms for envelope encryption — neither is available on Workers. Keeping the unpaid studio on CF Workers and paid cloud features on Fly is the cleanest runtime split. CORS allowlist via CORS_ALLOWED_ORIGINS env (defaults to local.stackpanel.com, stackpanel.com, and common localhost ports). credentials: true requires exact-match origins, not wildcards. README documents the deploy flow — run `fly deploy` from the monorepo root so Docker build context includes workspace packages. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
alchemy-effect's StateService.set has no versioning semantics, so forcing
expectedVersion on every put broke the HostedStateStore adapter pattern.
Keep the check available (pass expectedVersion for strict mode) but make
it opt-in — omit for last-writer-wins, matching LocalState behavior.
Delete becomes idempotent in the same way: returns { deleted: bool }
instead of throwing NOT_FOUND, mirroring alchemy-effect's LocalState
which treats delete-of-missing as a no-op.
Added listStacks procedure to complete the StateService contract
(listStacks + listStages + list/get/set/delete covers everything the
adapter needs to implement).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Implements alchemy-effect's StateService contract backed by the cloud alchemyState.* tRPC router. Drop-in Layer replacement for LocalState — provide HostedState to a Stack and every state read/write round-trips through api.stackpanel.com with Bearer auth. Env-driven config (same binary works in dev, CI, prod): STACKPANEL_API_URL override for the tRPC endpoint ALCHEMY_STATE_TOKEN capability/session token (required) Error mapping: TRPCClientError → StateStoreError with actionable text. - 403 FORBIDDEN → "requires an active Pro subscription" - 401 UNAUTHORIZED → "run stackpanel auth login" - 412 PRECONDITION_FAILED → "no active organization on your session" Everything else propagates the server's code + message so the CLI can surface the real failure without leaking tRPC internals. getReplacedResources composes list + get on the client (same pattern as LocalState) — keeps the server router generic and status-agnostic. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Toggles between the filesystem state backend (.alchemy/state/, default) and the hosted backend (api.stackpanel.com, Pro tier). Flipping to hosted automatically marks ALCHEMY_STATE_TOKEN as required in the deploy scope so preflight fails fast on a missing secret instead of surfacing a 401 at first write. Exposes: stackpanel.deploy.stateBackend : "hosted" | "local" stackpanel.deploy.apiUrl : override for the cloud API base Contributes to stackpanel.envs.deploy: STACKPANEL_STATE_BACKEND literal from option STACKPANEL_API_URL literal from option ALCHEMY_STATE_TOKEN required=true + secret=true when hosted Beads: stackpanel-bni. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replaces the hardcoded product ids in the Better-Auth checkout config
with polarProducts(env), keyed by STACKPANEL_DEPLOY_ENV. Production
reads overrides from POLAR_{PRO,FREE}_PRODUCT_ID_PRODUCTION Fly secrets
so preview deploys stay on the sandbox products and never charge real
cards. Dev and preview return the sandbox IDs unconditionally.
Webhook handler's planForProduct now delegates to the shared lookup in
polar-products.ts so webhooks hitting any env resolve correctly —
critical for users migrating from sandbox to prod mid-cycle.
Also wires a push-secrets.sh helper that uses `sops exec-env` to pipe
the shared sops file into `fly secrets import`. Adds polar_access_token,
polar_webhook_secret, better_auth_secret, and production product ID
placeholders to the sops file.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds four entries to .stack/secrets/vars/shared.sops.yaml: better_auth_secret generated 32-byte random polar_access_token TBD placeholder (prod OAT) polar_webhook_secret TBD placeholder (whsec_*) polar_pro_product_id_production TBD placeholder polar_free_product_id_production TBD placeholder push-secrets.sh decrypts these and pipes into `fly secrets import` so the deploy pipeline is a one-liner after the TBDs are filled in from the Polar dashboard. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Per-user direction: use the project's existing nix2container + Fly
deployment module instead of a custom Dockerfile. The Dockerfile path
was also hitting a docs package postinstall unrelated to the API.
Changes:
- Register stackpanel.apps.api in .stack/config.apps.nix with
container.enable + deployment.host = "fly" so the containers and
fly modules auto-generate fly.toml, per-app .tasks/bin/ scripts,
and packages/infra package.json deploy scripts
- Wire `projectRoot` through per-system-outputs.nix so the
containers module can find the working tree during pure eval
- Un-gitignore `.stackpanel-root` (holds just "." so it's portable
across machines via the stackpanel-root flake input)
- Delete the hand-written Dockerfile, .dockerignore, apps/api/README
- Drop the hand-written apps/api/fly.toml in favor of the one the
module generates on devshell entry
- Create Fly app `stackpanel-api` in org `darkmatter`, region `iad`
- Sops placeholders for polar_access_token + polar_{pro,free}_product_id_production
Deploy workflow:
nix develop --impure
cd packages/infra
bun run container:build:api # nix builds image via remote builders
bun run container:push:api # skopeo-nix2container → registry.fly.io
bun run deploy:api # flyctl deploy with the pushed image
Known issue: `container:push:api` currently fails on aarch64-darwin
because nix2container's `skopeo-nix2container` has a vendor-path bug
on darwin. Workaround: run the push step from a Linux CI runner (which
is the expected production pipeline anyway).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Runs container build + push + Fly deploy on ubuntu-latest because skopeo-nix2container doesn't build on darwin (upstream vendor-path bug). Linux runners are the expected production pipeline anyway; this just makes it explicit. Triggers on push to main touching api-adjacent paths + manual dispatch with a skip_build toggle for deploy-only runs (e.g., to roll forward an already-pushed image). Requires repo secrets: FLY_API_TOKEN from `flyctl auth token` SECRETS_AGE_KEY_DEV AGE key for SOPS decryption Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…merge verification Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The shared.sops.yaml file is encrypted only for team members' AGE keys — GitHub Actions can't decrypt it. Route the Fly api's secrets through stackpanel.envs.deploy instead, which codegens to packages/gen/env/data/_envs/deploy.sops.json and is encrypted for the CI AGE key via the existing rekey pipeline. Adds the following to the deploy scope (config.nix): BETTER_AUTH_SECRET, POLAR_ACCESS_TOKEN, POLAR_WEBHOOK_SECRET, POLAR_PRO_PRODUCT_ID_PRODUCTION, POLAR_FREE_PRODUCT_ID_PRODUCTION push-secrets.sh now decrypts the rendered deploy payload and runs jq to select + rename the subset the api needs. DATABASE_URL is intentionally omitted (deploy scope has PlanetScale; api uses Neon) and set manually via `fly secrets set` after first deploy. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Drops the jq pipeline in favor of sops's native dotenv renderer. Selects + renames the subset the Fly api needs via grep+sed, appends fixed non-secret env vars, pipes to `fly secrets import`. Note: this script will be deleted as a follow-up. The right home for per-app deploy secret pushing is the stackpanel fly deployment module reading from stackpanel.apps.<app>.env — same declarative pattern the Cloudflare/alchemy path uses. Blocked on fixing the SECRETS_AGE_KEY_DEV GitHub secret first (current value doesn't decrypt the github_actions age recipient). Also rekeys test.sops.yaml via bash .stack/secrets/bin/rekey.sh. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
New github_actions pubkey: age1d9h9mm3u5qalmpl2pf62pyzqj8t654n435emn93rutv0cg9sr32sg64fdj Updated in .sops.yaml and .stack/config.nix, then rekey.sh propagated the new recipient across every .stack/secrets/vars/*.sops.yaml and every packages/gen/env/data/**/*.sops.json payload. Paired with a SECRETS_AGE_KEY_DEV GitHub secret update that holds the matching private key. Unblocks the deploy-web, deploy-docs, and deploy-api workflows — all three were failing at sops decryption because the previous secret no longer matched the pubkey recipient. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
nix2container's skopeo-nix2container doesn't build against skopeo 1.20 (nlewo/nix2container's patch cd's into vendor/go.podman.io/image/v5, but skopeo 1.20 vendors the image lib at vendor/github.com/containers/ image/v5). dockerTools emits a docker-archive tarball the system skopeo can push without any patch. Works on both darwin and Linux CI (confirmed end-to-end push to registry.fly.io/stackpanel-api:latest from darwin). Switch back to nix2container when the upstream fix lands. Also bumps nix2container input to 76be9608 (latest master) while we're at it — didn't fix the skopeo patch but it's worth taking the newer config defaults. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The stackpanel containers module ships the `${appPath}/.output/{server,public}`
directory as the container's /app dir (TanStack-Start layout convention).
bun build bundles src/index.ts into a single ESM file at the expected path,
so the default `/bin/bun /app/.output/server/index.mjs` Cmd works without
overrides.
Confirmed live end-to-end — https://stackpanel-api.fly.dev/health returns
200 with the expected JSON body, image pushed via nix dockerTools + skopeo
to registry.fly.io/stackpanel-api:latest.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
….output) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The mkAppDir derivation imports apps/api/.output via builtins.path. When magic-nix-cache has a prior container-api store path cached, nix substitutes it even though the underlying source tree has a new .output/server/index.mjs. `--rebuild` forces a local rebuild so `bun run build` output is picked up. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Follows the same pattern as apps/web and apps/docs: declarative alchemy.run.ts on the alchemy-effect deploy path, no hand-maintained fly.toml/Dockerfile/scripts. Subdomain routing handled by the standard Workers for Platforms + Cloudflare.providers() flow. Runtime changes to make the API work in the Workers runtime: - encryption.ts rewritten on Web Crypto API (AES-GCM) + aws4fetch for KMS JSON calls (no @aws-sdk/client-kms, which drags Node-only handlers) - index.ts now exports a Hono app directly (Workers fetch handler) and reads secrets from the request env binding via a per-request globalThis.__env shim — same object keys library code used to read from process.env Tore down Fly infrastructure: - apps/api/Dockerfile, .dockerignore, fly.toml, README, scripts/ gone - deploy-api.yaml rewritten to mirror deploy-docs.yaml: alchemy-effect deploy with SOPS_AGE_KEY + a cached .alchemy/state/ directory, plus a destroy job on PR close toBufferSource helper copies Uint8Array buffers into fresh ArrayBuffer views — TS 5.7 tightened BufferSource to reject Uint8Array<ArrayBufferLike> because it may alias SharedArrayBuffer. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
ALCHEMY_STATE_TOKEN is required by stackpanel.deployment.alchemy even though we use filesystem state (cached across runners), not the CloudflareStateStore. Placeholder satisfies the validator. CLOUDFLARE_API_TOKEN / CLOUDFLARE_ACCOUNT_ID are in the sops-encrypted deploy scope, not GH repo secrets. loadDeployEnv injects them at alchemy.run.ts startup. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
stackpanel.modules.deploy declares ALCHEMY_STATE_TOKEN as required and points its sops ref at /common/alchemy-state-token, but common.sops.yaml never existed. Creating it with a ci-local-state-placeholder value satisfies preflight validation without wiring the Cloudflare state store (we use filesystem state cached across runners). rekey.sh ran against the new file and rekeyed dev + test along the way (no-op for shared). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Switched Cloudflare.Worker `main` from src/index.ts to a prebuilt .output/server/index.mjs (bun build --target bun) to avoid alchemy's default loader leaving standardwebhooks unresolved - Still fails at runtime with 'Uncaught TypeError: m is not a function' — a minified call into something the Workers V8 isolate can't resolve Parked. Next step is either (a) a different bundler (esbuild with platform=browser + explicit polyfills), or (b) revert to Fly where everything already works end-to-end. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…OLUTE Backing out the CF Workers migration — the bundler runtime compat issues (standardwebhooks unresolved, TypeError: m is not a function post-bundle) were deeper than the cost of a dedicated Fly machine for the backend. Keep Fly for apps/api (Node/Bun runtime = home for Better-Auth + Polar + Drizzle + AWS KMS); keep Workers for the frontends (apps/web, apps/docs) where the dep surface is small. Restored from 28962f7: apps/api/src/index.ts Hono default export with { port, fetch } apps/api/package.json drops alchemy-effect / @distilled.cloud/cloudflare apps/api/scripts/push-secrets.sh .github/workflows/deploy-api.yaml nix build + skopeo push + flyctl deploy Deleted apps/api/alchemy.run.ts + apps/api/wrangler.toml. Proper option (b) for the untracked-.output problem: mkAppDir now checks STACKPANEL_ROOT_ABSOLUTE (set by the deploy workflow to ${GITHUB_WORKSPACE}) and reads .output from there if present. The flake's store copy filters out git-ignored directories, so freshly- built output from `bun run build` in a previous step was invisible before — now it's visible without needing to commit .output or git-add it in CI. Requires --impure which we already use for alchemy deploys. Dropped `--rebuild` from the nix build step (was papering over this bug — not needed once mkAppDir resolves the right path). Kept packages/api/src/lib/encryption.ts on Web Crypto + aws4fetch despite reverting the runtime target. Both APIs work on Node too, and keeping them leaves the door open to retry Workers later without rewriting crypto. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The push step independently re-evaluates the flake to resolve copy-container-api -- it needs the same STACKPANEL_ROOT_ABSOLUTE so mkAppDir computes the same chosenPath (and therefore the same derivation hash) as the build step. Without it, the push step builds docker-image-stackpanel-api with the placeholder branch and pushes the no-output layer. Job-scope env applies to every step, fixing the divergence. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replaces the manual `flyctl certs add` + Cloudflare DNS dance with a declarative apps/api/alchemy.run.ts running after the Fly machine deploys. Uses @distilled.cloud/fly-io for ACME cert creation + @distilled.cloud/cloudflare for the A/AAAA records on the stackpanel.com zone (proxy off — Fly terminates TLS). Same pattern as apps/web's Workers.putDomain binding; both providers' credentials read from process.env via loadDeployEnv. Workflow: a new "Bind public hostname" step runs after Verify health, gated on main/develop/PR — feature branches still serve from stackpanel-api.fly.dev. FLY_IO_API_KEY mirrors the existing FLY_API_TOKEN GH secret so no new secret is required. @distilled.cloud/fly-io added to catalog at ^0.11.0; matches the Effect-native shape of the existing distilled.cloud packages. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The previous CLOUDFLARE_API_TOKEN had only Zone:Read on stackpanel.com, which blocked the api/alchemy.run.ts DNS reconcile. New token has the DNS Edit scope required to create A/AAAA records. Manually applied to api.stackpanel.com today (records: A → 66.241.125.29, AAAA → 2a09:8280:1::10a:7635:0); Fly cert is "Awaiting certificates" pending Let's Encrypt issuance. Subsequent stages will reconcile via the workflow's "Bind public hostname" step now that the token has the right scope. Also cleans up apps/api/wrangler.toml — leftover from the abandoned Workers migration; api runs on Fly. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…-agent first) Studio mirrors local.drizzle.studio: browser app at local.stackpanel.com talks to the user-machine agent via http://127.0.0.1:9876. Apex stackpanel.com stays reserved for marketing. Staging/PR previews: local.<stage>.stackpanel.com (parallel to docs.<stage>.stackpanel.com). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
SECRETS_AGE_KEY_PROD pubkey was never added to the production sops payloads — only SECRETS_AGE_KEY_DEV (the github_actions recipient) matches anything on every stage payload. The conditional was a leftover from when separate prod/dev keys were planned but never wired. Always use SECRETS_AGE_KEY_DEV. Fix mirrors the apps/api workflow (which already does this). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
In 0.12 the Provider tag moved off the resource instance. The new shape is
`Provider.effect(ResourceClass, eff)` and `ResourceClass.Provider.of({...})`.
Why: production deploys for apps/web were failing at evaluation time with
`TypeError: undefined is not an object (evaluating 'NeonProject.provider.effect')`.
Touched: NeonProject (both copies) + Docker.{Container,Volume} (both copies).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Without --yes, alchemy-effect renders an Ink/React confirm prompt before deploy. In CI the React profiler walks fiber.memoizedProps and crashes on "TypeError: Symbol.toPrimitive returned an object" — the bundled React performance-measure path can't stringify some Effect-shaped value passed through props. apps/web already passes --yes; doing the same here. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Mirrors apps/docs/wrangler.jsonc. Without explicit config, the assets binding defaults to runWorkerFirst:true which makes CF route every request through the OpenNext worker; the worker then assumes static asset misses fall through and throws CF error 1101 on the homepage. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
OpenNext emits a plain Workers default export shape ({ fetch }), but the
alchemy Worker bootstrap wraps `main` with Layer.effect(tag, entry).asEffect()
expecting an Effect entrypoint. With a plain object that wrapper crashes the
worker at runtime (CF error 1101 on every request). isExternal:true bypasses
the wrapper so OpenNext's own entrypoint stays intact.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Tracks the OpenNext + alchemy-effect 0.12 incompatibility. apps/web is unblocked; apps/docs production needs deeper investigation (likely rolldown inlining vs OpenNext's dynamic-import code-splitting). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Production deploy was throwing CF error 1101 with the runtime exception `No such module "node:perf_hooks". imported from "handler-BwC-NBMH.js"`. Cloudflare added node:perf_hooks as a native module on 2026-03-17. Before that date, unenv's perf_hooks polyfill itself imports from node:perf_hooks inside the worker bundle, so it can't substitute itself. Next.js's edge runtime depends on perf_hooks transitively, so the docs worker hit this on every request. Bumping the date past the threshold gates on the native module instead. Captured the actual exception via wrangler tail (cf-tail.ts → tail API) on script stackpanel-docs-docs-production-4xnefbcjqnacqgx6. Refs: stackpanel-dh5 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Homepage now returns 200. /docs/* routes still fail because rolldown silences `unresolvedImport` warnings and leaves OpenNext's pre-bundled chunk imports literal in the deployed worker. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Production now serves both hostnames from the same Worker:
- stackpanel.com → marketing/landing
- local.stackpanel.com → studio (`/studio/*` routes talking to the
user's local agent at 127.0.0.1:9876)
Both ship the same bundle today; better-auth's crossSubDomainCookies is
scoped to `.stackpanel.com` in production so a sign-in from the apex
carries into the studio subdomain. Outside production cookies stay
host-only — preview stages live on isolated per-PR subdomains and dev
runs on localhost where domain attributes are ignored.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Rework landing pages with new sections (production stacks, pricing,
comparison, how-it-works, config showcase) and dedicated /pricing route.
- Add beta waitlist: Drizzle `beta_waitlist` schema, `waitlist.join` tRPC
procedure, and global waitlist dialog provider; rewire all "Get started"
/ trial CTAs to open it instead of /login.
- Add /demo route: standalone studio chrome (sidebar/header/banner) +
overview, apps, services, variables, network, files pages backed by a
shared fixture file. Honest demo-mode banner, no agent stack required.
- Draft /docs/stacks/{index,alchemy,colmena,fly}.mdx and wire into docs
meta.
Includes regenerated route tree, env payloads, and infra/scripts/vendor
changes pulled along.
PR SummaryCursor Bugbot is generating a summary for commit c3b1a4d. Configure here. |
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 2 potential issues.
Bugbot Autofix is ON, but it could not run because the branch was deleted or merged before autofix could start.
Reviewed by Cursor Bugbot for commit c3b1a4d. Configure here.
| @@ -0,0 +1 @@ | |||
| /Users/cm/git/darkmatter/stackpanel | |||
There was a problem hiding this comment.
Committed .stackpanel-root has developer's absolute path instead of "."
High Severity
The .gitignore comment explicitly states .stackpanel-root "holds a single '.' so the stackpanel-root flake input resolves to the flake source dir across machines," but the committed file contains /Users/cm/git/darkmatter/stackpanel — a developer's local absolute path. The readStackpanelRoot Nix helper in nix/flake/exports.nix will use this verbatim absolute path instead of the flake source directory, breaking Nix evaluation for any other developer or CI runner. The flake templates also still list .stackpanel-root in their .gitignore, suggesting this file was never intended to be tracked with a machine-specific value.
Additional Locations (1)
Reviewed by Cursor Bugbot for commit c3b1a4d. Configure here.
| @@ -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"} | |||
There was a problem hiding this comment.
Local development artifacts committed to repository
Medium Severity
The .superpowers/brainstorm/ directory contains local development artifacts that appear accidentally committed: PID files (.server.pid with value 41378), local server info (.server-info with localhost:50966), click-tracking .events data, .server-stopped files, and an HTML prototype. These are runtime artifacts from a local brainstorming tool, not source files. The .superpowers directory is not in .gitignore.
Additional Locations (2)
Reviewed by Cursor Bugbot for commit c3b1a4d. Configure here.


Summary
Long-running branch that lands the first cloud-gate slice plus the marketing surface that goes with it. 47 commits, ~12.9k LOC added across infra, app, and content.
Cloud platform
apps/apiscaffolded and migrated from Fly → Cloudflare Workers viaalchemy-effect. Hono + tRPC + Better-Auth wrapper, declarative cert/DNS via@distilled.cloud/fly-iofor the Fly era.HostedStateStoreadapter,alchemyStateput/delete concurrency-check now optional,listStacksadded.stackpanel.deploy.stateBackendNix option for choosing local vs hosted state.Auth + billing
Landing site + studio surface
Docs
Infra plumbing
Test plan
Notes for reviewers