Skip to content

Cloud-gate v1: apps/api, hosted alchemy state, Polar subs, landing + demo studio#14

Merged
cooper (czxtm) merged 47 commits intomainfrom
cursor/97312fc2
Apr 29, 2026
Merged

Cloud-gate v1: apps/api, hosted alchemy state, Polar subs, landing + demo studio#14
cooper (czxtm) merged 47 commits intomainfrom
cursor/97312fc2

Conversation

@czxtm
Copy link
Copy Markdown
Contributor

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/api scaffolded and migrated from Fly → Cloudflare Workers via alchemy-effect. Hono + tRPC + Better-Auth wrapper, declarative cert/DNS via @distilled.cloud/fly-io for the Fly era.
  • Hosted Alchemy state: schema + envelope encryption + tRPC router; HostedStateStore adapter, alchemyState put/delete concurrency-check now optional, listStacks added.
  • stackpanel.deploy.stateBackend Nix option for choosing local vs hosted state.
  • Deploy pipeline: GitHub Actions `deploy-api` workflow, Nix-native container + Fly pipeline, then Cloudflare Workers via alchemy-effect, plus assorted CI hardening (token rotation, secret push, build diagnostics).

Auth + billing

  • Polar webhook → `user_subscription` mirror with env-keyed product IDs (dev / preview / prod).
  • `paidProcedure` middleware that gates tRPC procedures by subscription status.
  • Organization schema + Better-Auth organization plugin enabled.

Landing site + studio surface

  • New landing sections: Production Stacks, Pricing, How it works, Config showcase, Comparison; dedicated `/pricing` route with full feature matrix and FAQ.
  • Beta waitlist: `beta_waitlist` Drizzle schema, `waitlist.join` tRPC procedure (idempotent on email, captures source/tier/UA/IP-hash), global `` dialog. All "Get started" / trial CTAs now open it instead of `/login`.
  • Demo Studio at `/demo`: standalone studio chrome (sidebar/header/banner) backed by a single fixture file. Six pages — overview, apps, services, variables, network, files. Honest "demo mode" banner; no agent stack required.

Docs

  • New `apps/docs/content/docs/stacks/{index,alchemy,colmena,fly}.mdx` describing the Production Stack offering, pricing tiers, and what's maintained per tier. Wired into the docs nav.
  • Cloudflare Worker compatibility_date bumped to 2026-03-17 for `node:perf_hooks`.

Infra plumbing

  • AGE recipient rotation for `github_actions` + SOPS rekey; `push-secrets` simplified via `sops --output-type dotenv`.
  • `alchemy-effect-opennext` overlay vendored under `vendor/` (with upstream-tracking notes).
  • Generated env payloads regenerated for all apps × envs.

Test plan

  • `bun run check` + `bunx tsc --noEmit` clean across `apps/web`, `apps/api`, `packages/{api,db,auth,infra}`.
  • Verify `/`, `/pricing`, `/demo`, `/demo/{apps,services,variables,network,files}` render in dev.
  • Submit waitlist form locally; confirm row in `beta_waitlist`; resubmit same email returns `alreadyOnList: true`.
  • `drizzle-kit generate` then push to dev DB to materialize `beta_waitlist` + `user_subscription` + organization tables.
  • Trigger `deploy-api` workflow on a feat branch; confirm Worker deploys and Polar webhook reaches `/api/webhooks/polar`.
  • Subscribe via Polar in dev; confirm mirror row written and `paidProcedure` lets the user through.
  • Open the docs site, confirm Production Stacks pages render and nav order is correct.

Notes for reviewers

  • The branch has been used as a "cloud-gate v1" integration line for several weeks; consider squash-merge.
  • `apps/api/fly.toml` is left in tree intentionally as a fallback even though the active deploy is Workers.
  • `vendor/alchemy-effect-opennext-overlay/` should be removed once `alchemy-effect` ships the same overlay upstream — see `scripts/ALCHEMY_EFFECT_OPENNEXT_UPSTREAM.md`.

cooper (czxtm) and others added 30 commits April 23, 2026 23:18
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>
cooper (czxtm) and others added 17 commits April 24, 2026 08:40
…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.
@czxtm cooper (czxtm) marked this pull request as ready for review April 29, 2026 03:12
@czxtm cooper (czxtm) merged commit 65245fd into main Apr 29, 2026
4 of 7 checks passed
@cursor
Copy link
Copy Markdown

cursor Bot commented Apr 29, 2026

PR Summary

Cursor Bugbot is generating a summary for commit c3b1a4d. Configure here.

@czxtm cooper (czxtm) deleted the cursor/97312fc2 branch April 29, 2026 03:12
Copy link
Copy Markdown

@cursor cursor Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 2 potential issues.

Fix All in Cursor

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.

Comment thread .stackpanel-root
@@ -0,0 +1 @@
/Users/cm/git/darkmatter/stackpanel
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)
Fix in Cursor Fix in Web

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"}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit c3b1a4d. Configure here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant