01 — foundation-infra (design)
01 — foundation-infra (design)
Section titled “01 — foundation-infra (design)”Status: Draft
Date: 2026-05-23
Owner: infra agent
Parent: docs/superpowers/specs/2026-05-23-nowyourlink-master-design.md
Process diagram: docs/superpowers/processes/01-infra-provisioning.md
1. Purpose & scope
Section titled “1. Purpose & scope”Bootstrapping layer for nowyourlink.com. Owns what a new contributor (or cloud agent) needs to run, deploy, and tear down the platform — but no domain logic.
In scope: pnpm/Turbo monorepo skeleton; ghardenlab.config.json + the ghardenlab create / add command sequence; Terraform for CF zone, Access (admin), R2, D1, KV, Queues, Workflows scaffolding, Crons, custom-domain bindings (R2 s3 state backend, world-forest pattern infra/terraform/main.tf:11-22); GitHub Actions (ci, preview, deploy, lint-seo, claude*); wrangler.jsonc per env; secrets via wrangler secret put + docs/cloud-environment.md; .env.example as source of truth.
Out of scope: auction logic / DOs → 04; Stripe + saga → 05; WS protocol → 06; legal copy → 12.
2. Repo skeleton
Section titled “2. Repo skeleton”Matches Master §4.8 (apps/web serves apex + app.*). Pattern: world-forest/ root (CLAUDE.md §Monorepo Structure).
nowyourlink/├── apps/│ ├── web/ # Astro+islands → nowyourlink.com + nowyourlink.com/app│ ├── api/ # REST + webhook + pixel ingest → api.*│ ├── ws/ # WS hub + per-auction DOs → ws.*│ ├── admin/ # CF Access-gated → admin.*│ ├── queue/ # Notify, moderation, pixel rollup consumer│ ├── cron/ # auction.open/close + report sweeps│ └── webhooks/ # Stripe + Resend webhooks (isolated blast radius)├── packages/│ ├── shared/ config/ observability/ security/ consent/ legal/ data-rights/│ ├── seo/ web-vitals/ email/ stripe/ ui/├── docs-site/ # Starlight → docs.nowyourlink.com (separate Pages deploy)├── infra/terraform/ # See §5├── scripts/ # lint-seo, dev-seed, perf-cost-audit├── docs/{cloud-environment.md, superpowers/{specs,processes,runbooks}/}├── ghardenlab.config.json, legal.config.json, security.config.json├── pnpm-workspace.yaml, turbo.json, tsconfig.base.json, .env.exampleCross-worker types (User, Bid, Auction, event taxonomy §4.4) live in packages/shared.
3. ghardenlab CLI invocation (exact commands)
Section titled “3. ghardenlab CLI invocation (exact commands)”Run from ~/dev-arne/:
ghardenlab create nowyourlink --type mvp --domain nowyourlink.com --workers web,api,admin,queue,cron,webhooks --features legal,consent,auth,email,payments,dataRights,seo,schemaOrg,discovery,webVitals,lintSeo,sentry --db d1 --storage kv,r2,queues --ci github-actions --ci-claude --cloud-env claude --git-init --default-branch mainThis emits the monorepo skeleton, ghardenlab.config.json, wrangler.jsonc per worker, GitHub Actions matrix, and the cloud-env doc stub.
Follow-on add calls (idempotent; run in this order):
ghardenlab add worker ws --type websocketghardenlab add sentry --workers web,api,ws,admin,queue,cron,webhooksghardenlab add seoghardenlab add schema-orgghardenlab add discoveryghardenlab add web-vitalsghardenlab add lint-seoadd worker ws --type websocket emits the hibernatable WebSocketHub DO + ws.* route (per add worker help). --cloud-env claude writes docs/cloud-environment.md (extended in §7). feature payments scaffolds Stripe boilerplate; 05-payment-saga owns implementation.
4. ghardenlab CLI gaps (manual wrangler.jsonc patches)
Section titled “4. ghardenlab CLI gaps (manual wrangler.jsonc patches)”CLI covers Workers, Queues, KV, D1, R2 (via --storage), Sentry, SEO. It does not scaffold:
| Gap | Workaround | Follow-up |
|---|---|---|
G1 — Workflows binding (workflows[]). Needed on api + webhooks for the payment saga. | Hand-add {"name":"payment-saga","binding":"PAYMENT_SAGA","class_name":"PaymentSagaWorkflow","script_name":"nowyourlink-api-${env}"} to apps/api/wrangler.jsonc per env. | Issue: add feature workflows. |
G2 — Cron triggers (triggers.crons). Need apps/cron to fire daily auction open/close + hourly VIES retry + post-campaign sweep. | Append "triggers": {"crons": ["0 0 * * *", "5 * * * *"]}. | Issue: worker --type cron with crons[]. |
G3 — Multi-queue named bindings. Need notify-queue, moderation-queue, pixel-queue, saga-deadletter-queue. | Hand-edit producers/consumers per worker. | Issue: multi-queue producers. |
G4 — R2 named bindings per worker. Need CREATIVES, INVOICES, OG_CACHE, PIXEL_LOGS_ARCHIVE. | Hand-edit. | Issue. |
G5 — D1 named binding (DB) on api/admin/queue/cron/webhooks all pointing at one prod + one preview DB. | Hand-edit. | Issue. |
G6 — CF Access application for admin.* (not in CLI). | Author infra/terraform/access.tf (§5). | n/a (TF). |
G7 — DO migrations (migrations[] tag="v1" new_sqlite_classes=[...]). | Hand-edit after add worker ws. | Issue. |
| G8 — Hyperdrive / D1 Sessions API. | Defer; not MVP-blocking. | Phase 2. |
Each gap-diff is committed to the spec PR before implementation.
5. Terraform layout
Section titled “5. Terraform layout”Mirrors world-forest/infra/terraform/main.tf (R2 s3 backend, cloudflare/cloudflare ~> 5.0, ghardenlab worker-app module). Split for readability:
infra/terraform/├── backend.tf # providers + R2 s3 backend (key=apps/nowyourlink/terraform.tfstate)├── variables.tf # cloudflare_account_id, cloudflare_zone_id├── locals.tf # project_slug, domain, worker_domains[], dns_records[]├── workers.tf # module "worker_app" → ghardenlab//terraform/modules/worker-app├── d1.tf # cloudflare_d1_database (prod + preview)├── kv.tf # sessions, rate-limit, saga-dedupe, config├── r2.tf # creatives, invoices, og-cache, pixel-archive├── queues.tf # notify, moderation, pixel, saga-dlq (if module doesn't cover)├── access.tf # zero_trust_access_application + policy for admin.*├── dns.tf # apex, cdn.* CNAME, MX (Resend), SPF/DKIM TXT└── outputs.tf # IDs for wrangler.jsonc paste (not auto-injected MVP)State backend verbatim from main.tf:11-22: R2 bucket gharden-terraform-state, path-style, HMAC TF_R2_* → AWS_* (DEPLOY.md §7). Superseded 2026-08-16: the state now lives at nowyourlink/terraform.tfstate in the terraform-state bucket with endpoints.s3 inlined in backend.tf — see the R6 supersession note in 2026-08-16-nowyourlink-rename-and-rebuild-design.md; the gharden-terraform-state bucket is shared with the owner’s other projects; this project’s key was migrated out and the retired stack’s state object was deleted with that stack on 2026-08-16. Modules: github.com/Ghardenlab/ghardenlab//terraform/modules/worker-app?ref=main (call site = main.tf:79-93) + inline cloudflare_zero_trust_access_application/_policy. Email-Sending + Zaraz stay dashboard-configured (provider gap, precedent main.tf:95-147).
Divergence — 5-file layout (issue #120)
Section titled “Divergence — 5-file layout (issue #120)”Shipped layout collapses the 11-file split above into five files, per the HashiCorp Terraform style guide (file-names): group by logical function (a feature/component), not per resource type.
infra/terraform/├── backend.tf # providers + R2 s3 backend├── main.tf # vars + locals + module "worker_app" + docs custom domain├── access.tf # docs-internal + admin Zero Trust apps├── outputs.tf # one output per __*_ID__ in apps/*/wrangler.jsonc└── github.tf # branch protection + tag ruleset (separate provider; out of spec-01 scope, kept verbatim)dns.tf is deferred to the sibling DNS issue. variables.tf / locals.tf /
workers.tf are absorbed into main.tf; d1.tf / kv.tf / r2.tf /
queues.tf are absorbed into the module "worker_app" call (the module owns
those resources, so a per-type split would be vacuous).
Two KV namespaces beyond the §6 matrix are provisioned because
apps/*/wrangler.jsonc already reference them as __KV_MAGIC_LINKS_*_ID__
(admin AUTH_TOKENS) and __KV_MOD_BLACKLIST_*_ID__ (spec 03 §2.5 keyword
blacklist). nowyourlink-oauth-state-kv from the §6 list is not
provisioned (no wrangler reference).
Three additional Queues beyond the §6 matrix are provisioned because
apps/api/wrangler.jsonc already binds them as producers today:
q-moderation (spec 03 §4 auto-filter pipeline), q-transforms (spec 03
§9 image-transforms consumer), and q-vies-retry (spec 02 §4 VIES
degraded-mode retry). All three include -preview variants.
Resolution mechanism: a new scripts/render-wrangler.sh reads
terraform output -json and substitutes the __*_ID__ tokens into per-app
wrangler.rendered.jsonc before wrangler deploy. See issue #120 for the
full inventory and acceptance.
6. CF resources matrix
Section titled “6. CF resources matrix”| Resource | Name(s) | Binding | Worker(s) | Env | Purpose |
|---|---|---|---|---|---|
| D1 | nowyourlink-prod, nowyourlink-preview | DB | api, admin, queue, cron, webhooks | both | Users, bids, ads, auctions, saga mirror, strikes |
| KV | nowyourlink-sessions | SESSIONS | api, web | both | Magic-link sessions (TTL 30d) |
| KV | nowyourlink-rate-limit | RATE_LIMIT | api | both | Per-IP, per-user buckets |
| KV | nowyourlink-saga-dedupe | SAGA_DEDUPE | api, webhooks | both | Stripe webhook event.id dedupe |
| KV | nowyourlink-config | CONFIG | all | both | Anti-snipe flags, feature toggles |
| R2 | nowyourlink-creatives | CREATIVES | api, admin, queue | both | Approved + pending creatives |
| R2 | nowyourlink-invoices | INVOICES | webhooks, admin | both | §14 UStG PDF mirror (10y retention, see master §10.3) |
| R2 | nowyourlink-og-cache | OG_CACHE | web | both | Per-day OG images |
| R2 | nowyourlink-pixel-archive | PIXEL_LOGS_ARCHIVE | queue | both | Raw conversion-pixel events |
| Queue | notify-queue | NOTIFY_QUEUE (P), consumer @ queue | api → queue | both | Email + push fan-out |
| Queue | moderation-queue | MODERATION_QUEUE (P), consumer @ queue | api → queue | both | Auto-filter pipeline |
| Queue | pixel-queue | PIXEL_QUEUE (P), consumer @ queue | api → queue | both | Conversion pixel ingest rollup |
| Queue | saga-deadletter-queue | SAGA_DLQ (P), consumer @ queue | webhooks → queue | both | Workflow-fail escalation |
| DO migration | v1: AuctionDO, WebSocketHub (SQLite-backed) | AUCTION, HUB | ws | both | One DO per YYYY-MM-DD; one hibernatable hub |
| Workflow | payment-saga (class PaymentSagaWorkflow) | PAYMENT_SAGA | api, webhooks | both | Master §10.1 invoice-first saga |
| Cron | 0 0 * * * | — | cron | both | Daily auction open + close fan-out |
| Cron | 5 * * * * | — | cron | both | Hourly VIES retry + post-campaign-report sweep |
Preview names suffixed -preview (precedent main.tf:89).
7. Secrets layout (mirrors master §4.7)
Section titled “7. Secrets layout (mirrors master §4.7)”.env.example reproduces master §4.7 verbatim (empty values). Local dev uses per-worker .dev.vars. Prod + preview use wrangler secret put <KEY> --env <env>.
| Worker | Secrets |
|---|---|
| api | STRIPE_SECRET_KEY, STRIPE_WEBHOOK_SECRET, RESEND_API_KEY, TURNSTILE_SECRET_KEY, VAPID_*, CONVERSION_PIXEL_SECRET, SENTRY_DSN_API |
| webhooks | STRIPE_WEBHOOK_SECRET, STRIPE_SECRET_KEY, SENTRY_DSN_QUEUE |
| ws | SENTRY_DSN_WS |
| admin | ADMIN_API_TOKEN, CF_ACCESS_AUD, CF_ACCESS_TEAM, SENTRY_DSN_ADMIN |
| web | TURNSTILE_SITE_KEY (vars), SENTRY_DSN_WEB |
| queue | RESEND_API_KEY, VAPID_PRIVATE_KEY, SENTRY_DSN_QUEUE |
| cron | SENTRY_DSN_CRON |
docs/cloud-environment.md extends world-forest/docs/cloud-environment.md:1-60 with STRIPE_*, RESEND_API_KEY, VAPID_*, TURNSTILE_*, SENTRY_DSN_*, CONVERSION_PIXEL_SECRET, CF_ACCESS_AUD, CF_ACCESS_TEAM, ADMIN_API_TOKEN. Six Sentry projects (one per worker) created in docs/superpowers/runbooks/sentry-setup.md. Set with: wrangler secret put <KEY> --env <preview|production>.
#122 rename: the row originally listed
ACCESS_AUD. The shipped admin worker middleware (apps/admin/src/cf-access.ts) readsCF_ACCESS_AUD+CF_ACCESS_TEAM; implementation wins. The Terraform Access apps ininfra/terraform/access.tfemit one AUD per env; the team slug (e.g.nyl, notnyl.cloudflareaccess.com— the middleware appends the suffix itself) is supplied viaTF_VAR_cloudflare_access_team_name. Seedocs/superpowers/runbooks/admin-access-bootstrap.mdfor the apply
- secret-put + deploy order.
Sentry — one project, not six (supersedes the
SENTRY_DSN_*column). The table’s per-workerSENTRY_DSN_API/_WS/_ADMIN/_WEB/_QUEUE/_CRONnames implied six projects. The shipped design uses a singlenowyourlinkSentry project: every worker reads a plainSENTRY_DSN(same value) plus the committedENVIRONMENTvar (preview/production), and is attributed by a hardcodedservicetag in itsSentry.withSentry(...)options. Rationale (code is DSN-agnostic,world-forestprecedent, org-level quota, the §7 table already reused_QUEUEforwebhook) and the split-a-worker-out-later path are recorded indocs/superpowers/runbooks/sentry-setup.md. The only Sentry secret isSENTRY_DSN(not theSENTRY_DSN_*names); the environment is the single committedENVIRONMENTvar — shared by the logger and (in admin) the CF-Access docker-local bypass — replacing the earlierSENTRY_ENVIRONMENT.
Live per-worker secret list (#267). The reconciled, verified deploy-time secret surface — including
ADMIN_SECRET(wasADMIN_API_TOKEN),CF_ACCESS_AUD/CF_ACCESS_TEAM(wasACCESS_AUD), and the reserved not-yet-wired secrets (VAPID_*, OAuth, webhook Stripe) — is maintained indocs/cloud-environment.mdand.env.example. Treat those as canonical for what towrangler secret putper worker; this §7 table is the original intent.
8. CI/CD — GitHub Actions matrix
Section titled “8. CI/CD — GitHub Actions matrix”Mirrors world-forest .github/workflows/:
| Workflow | Trigger | Purpose |
|---|---|---|
ci.yml | PR + push main | install / lint / build / test / wrangler deploy --dry-run --env preview per worker (pattern: world-forest/ci.yml:9-50). |
preview.yml | PR touching apps/**, packages/**, infra/terraform/** | terraform apply preview + wrangler deploy --env preview per worker. Concurrency preview-nowyourlink, cancel-in-progress. |
deploy.yml | push main + workflow_dispatch | terraform apply prod + wrangler deploy --env production. Concurrency deploy-nowyourlink, no cancel. GitHub production env gate. |
lint-seo.yml | PR | From ghardenlab add lint-seo; fails PRs regressing AI-SEO contract. |
claude.yml + claude-code-review.yml | PR/issue | From --ci-claude. |
perf-cost-audit.yml (optional, weekly) | cron | Alerts on DO-duration creep (master §10.2). |
perf-hibernation-lint.yml (optional) | PR | Enforces acceptWebSocket + auto-response usage. |
Env block shared from world-forest/deploy.yml:18-25: CLOUDFLARE_API_TOKEN, AWS_ACCESS_KEY_ID=R2_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY=R2_SECRET_ACCESS_KEY, AWS_ENDPOINT_URL_S3=R2_ENDPOINT, TF_VAR_cloudflare_*.
9. Local development setup
Section titled “9. Local development setup”git clone git@github.com:ghardenlab/nowyourlink.git && cd nowyourlinkcp .env.example .dev.varspnpm install && pnpm buildghardenlab doctor && ghardenlab config validate# Per worker, in separate terminals:cd apps/web && npx wrangler dev --local --persist-to=.wrangler/statecd apps/api && npx wrangler dev --local --port 8788cd apps/ws && npx wrangler dev --local --port 8789cd apps/queue && npx wrangler dev --local --port 8790cd apps/cron && npx wrangler dev --local --port 8791 --test-scheduledcd apps/admin && npx wrangler dev --local --port 8792stripe listen --forward-to localhost:8788/webhooks/stripeD1 bootstraps via miniflare SQLite (--persist-to); pnpm dev:seed seeds. Cron: curl http://localhost:8791/__scheduled?cron=0+0+*+*+*.
10. Acceptance criteria (binary)
Section titled “10. Acceptance criteria (binary)”- §3
ghardenlab createruns to completion against empty dir. - All seven
ghardenlab addcalls idempotent. -
ghardenlab config validate+ghardenlab doctorpass. -
pnpm install && build && lint && testgreen. -
wrangler deploy --dry-run --env previewpasses for every worker. -
terraform validate+plansucceed with prod vars. - §6 resources exist in CF dashboard for both envs after
terraform apply. -
wrangler secret list --env productionreturns every §7 secret per worker. -
docs/cloud-environment.mdlists every §7 secret. - CF Access gates
admin.nowyourlink.comwith allowlist. -
ci,preview,deploy,lint-seoworkflows green on fresh PR. - Smoke:
curl api.preview.nowyourlink.com/healthz→ 200;wss://ws.preview.nowyourlink.com/auction/2026-06-01upgrades.
11. Agent-team hints
Section titled “11. Agent-team hints”- Wrangler v4 = JSONC, not TOML. Gap-patches (§4) edit
wrangler.jsonc. - R2 creds scoped per bucket.
TF_R2_*authenticates theterraform-statebucket (rotated 2026-08-16 to the account-wide pair); a bucket-scoped HMAC answers HeadObject with an opaque 403 at init. Do not reuse for app buckets (world-forestDEPLOY.md:89-99). - Workflows bindings on every worker that calls
instance.create()/sendEvent()— minimumapi+webhooks. - CF Access JWT subject is the admin identity (master §4.1); never create
usersrows for admins. - Hibernation is non-negotiable (master §10.2). Hub + auction DOs must use
state.acceptWebSocket()+setWebSocketAutoResponse().perf-hibernation-lint.ymlenforces this. - Sentry DSNs are per-worker (§7) — one project each, do not collapse.
- Cron triggers cost Worker invocations (cost-projection §6). Keep handlers lean; offload to Queues.
- TF outputs IDs but does not auto-write
wrangler.jsonc(G4–G5). First-apply requires manual paste; subsequent runs stable. - Mimic verbatim:
world-forest/infra/terraform/main.tf(backend + module),.github/workflows/deploy.yml(env + tf steps),DEPLOY.md§7 (tf init under cloud-env),docs/cloud-environment.md(cloud-agent setup script).