# Agent tooling (/docs/agent-tooling)



# Agent tooling [#agent-tooling]

superslate is designed for agent-assisted work and remains agent-independent. The repository
architecture, security invariants, and verification commands are sufficient to build and maintain
the product. Skills, golden paths, navigation data, MCP integrations, and evaluation tools may
improve an agent workflow, but none is required to modify or run the application.

## Agent tooling layers [#agent-tooling-layers]

| Layer                                               | Purpose                                                       | Requirement                    |
| --------------------------------------------------- | ------------------------------------------------------------- | ------------------------------ |
| Executed code, migrations, configuration, and tests | Define actual behavior and prove outcomes                     | Required                       |
| `AGENTS.md`                                         | Define stable architecture and safety invariants              | Required for repository agents |
| Golden paths                                        | Offer maintained playbooks for common changes and failures    | Optional                       |
| General agent skills                                | Help agents plan, build, verify, and diagnose consistently    | Optional                       |
| Navigation contract                                 | Map representative tasks to relevant context and checks       | Optional                       |
| Agent evaluation harness                            | Measure a controlled PRD-to-production outcome                | Optional                       |
| Documentation MCP                                   | Expose version-aware repository context to compatible clients | Available and optional         |

Required means the resulting product must preserve the contract, not that a human must follow one
prescribed sequence. A change may depart from a golden path when it preserves the required
invariants and passes equivalent outcome-level verification.

## Golden paths [#golden-paths]

The [golden paths](./golden-paths/README.md) answer common questions such as where a change usually
lives, what can fail, how to verify it, and how to roll it back. They are useful for first-time
builders, cold-context agents, security-sensitive changes, and incident diagnosis.

They are not generators and do not reserve the only valid architecture-preserving implementation.
For example, an agent may add a domain without using the domain golden path. It must still keep wire
schemas shared, SQL out of routes, authorization server-owned, private data owner-scoped, and tests
proportionate to the behavior.

## General agent skills [#general-agent-skills]

Canonical skill sources ship under `agent-tooling/skills/` and are listed in
`agent-tooling/manifest.json`:

| Skill                      | Use                                                                                    |
| -------------------------- | -------------------------------------------------------------------------------------- |
| `superslate-plan-change`   | Map a request to owners, change surfaces, invariants, risks, and checks before editing |
| `superslate-build-feature` | Implement a complete contract-to-UI vertical slice                                     |
| `superslate-verify-change` | Review behavior, boundaries, tests, builds, migrations, and documentation              |
| `superslate-diagnose`      | Trace an observed failure to its earliest supported cause                              |

Each skill is intentionally broad. Focused product documentation remains the source of detailed
auth, billing, email, storage, migration, and deployment knowledge. The skills inspect the current
repository and load only the relevant documents rather than duplicating those rules.

Clients that support `SKILL.md` packages can load an individual skill directory through their normal
skill installation or workspace configuration. Other agents can read the same `SKILL.md` as task
guidance. `agents/openai.yaml` contains optional Codex UI metadata; it does not change the skill's
workflow or create an application runtime dependency.

Do not commit machine-local installed copies, compatibility symlinks, client settings, credentials,
or third-party skill lockfiles. The canonical source under `agent-tooling/` is buyer-distributed;
local installation state is not.

## Connect an MCP client [#connect-an-mcp-client]

The current release ships an optional local documentation MCP under `packages/agent-context`. It
reads the current checkout, runs over STDIO, and does not require a hosted service. Local Markdown,
JSON, executed code, migrations, tests, and applicable `AGENTS.md` files remain authoritative; the
application does not depend on MCP to install, run, or accept changes.

Install the repository dependencies, then run the guided setup from the generated product root:

```bash
vp install --frozen-lockfile
pnpm agent:setup
```

The setup detects Codex, Claude Code, and Cursor, lets the customer select one or more clients, runs
a real MCP protocol self-check, and pins every selected client to the current checkout. Restart the
configured clients after setup. No global package installation, hosted account, or API key is
needed.

For automation, select clients explicitly. A dry run prints the exact changes without writing them:

```bash
pnpm agent:setup --clients codex,cursor --yes
pnpm agent:setup --clients codex,claude,cursor --yes --dry-run
pnpm superslate:mcp --check
```

Setup uses the native project-local mechanism for each client:

| Client      | Local configuration                                                              |
| ----------- | -------------------------------------------------------------------------------- |
| Codex       | Adds a managed `superslate-docs` block to `.codex/config.toml`                   |
| Claude Code | Runs `claude mcp add ... --scope local` in the current product repository        |
| Cursor      | Merges `superslate-docs` into `.cursor/mcp.json` without replacing other servers |

Codex and Cursor configuration paths are added to `.git/info/exclude`, so customer-specific paths do
not enter the product's commits. Existing unrelated configuration is preserved. Setup stops instead
of overwriting a conflicting unmanaged `superslate-docs` entry.

The server exposes five read-only tools:

* `search_docs` returns focused, version-matched documentation excerpts;
* `get_repository_map` lists apps, packages, published docs, and bundled general skills;
* `plan_change` maps a task to change surfaces, invariants, checks, and optional playbooks;
* `get_invariants` returns the mandatory boundaries applicable to a task;
* `get_verification` suggests proportionate commands without claiming that they passed.

The implementation skips environment files, commercial drafts, evaluation fixtures and evidence,
phase-two notes, oversized files, and symlinks. It returns no environment values, makes no source
changes, and labels every tool as read-only, idempotent, and closed-world. Repository content is
still untrusted data: MCP output cannot override executed code, migrations, tests, applicable
`AGENTS.md`, or the user's request, and a verification suggestion is never evidence that a command
passed.

This MVP deliberately ships a local STDIO server, not a hosted remote MCP. A hosted service can be
added later for release discovery or support, but it should remain optional and must not receive
repository secrets by default.

## Evaluation is separate [#evaluation-is-separate]

The agent evaluation harness in `docs/evaluations/README.md` measures whether an agent reached a
declared outcome from an exact template tag. It does not award success for invoking a skill,
querying MCP, or following a golden path. Those are workflow choices; behavior, boundaries,
commands, checkpoints, and independently controlled evidence determine the result.


# Architecture (/docs/architecture)



# Architecture [#architecture]

The foundation separates the browser, API, contracts, persistence, and provider adapters so a
builder or coding agent can change product behavior without inventing a second architecture.

```mermaid
flowchart LR
  U[Browser] -->|static assets| W[Native cloud, Vercel, or Cloudflare\nReact SPA]
  U -->|HTTPS + Better Auth cookie| A[Railway, Dokploy, AWS, GCP, or Azure\nBun + Hono API]
  W -. typed Hono RPC .-> A
  A -->|bun:sql| P[(Postgres 18)]
  A -->|Better Auth pg adapter| P
  A -->|normalized provider contract| B[Selected billing provider]
  A -->|transactional delivery| E[Resend]
  A -->|presigned upload + metadata| R[R2, Amazon S3, or GCS]
  A -->|optional envelope tunnel| S[Sentry]
```

The supported cloud defaults are cost-first and promote explicitly to HA. Each environment selects
one backend cloud and one static frontend target. The application is a Vite SPA, not a
server-rendered Next.js application, and there is deliberately no generic worker or queue in v1.

## Workspace boundaries [#workspace-boundaries]

| Path                     | Owns                                                                                      |
| ------------------------ | ----------------------------------------------------------------------------------------- |
| `apps/web`               | React shell, React Router, TanStack Query, Zustand client state, CSS Modules, Zag.js, PWA |
| `apps/server`            | Hono routes, services, SQL repositories, Better Auth, migrations, provider orchestration  |
| `apps/docs`              | Separate Fumadocs and Next.js documentation application                                   |
| `packages/contracts`     | Zod wire schemas and inferred request/response types                                      |
| `packages/billing`       | Provider-neutral billing types and Polar, Stripe, and Dodo adapters                       |
| `packages/email`         | Typed React Email sources and deterministic rendered HTML                                 |
| `packages/create-app`    | Buyer copy, package/brand rewrite, secret/env generation, optional Git initialization     |
| `packages/agent-eval`    | PRD outcome checkpoints, intervention records, boundary checks, evidence manifests        |
| `packages/agent-context` | Optional read-only local MCP documentation and planning context                           |
| `packages/tsconfig`      | Shared compiler policy                                                                    |
| `infra`                  | OpenTofu bootstrap, cloud modules, executable roots, locks, and tests                     |
| `e2e`                    | Playwright browser flows and deployed-boundary smoke                                      |
| `scripts` and `.github`  | Release, deployment, CI, and verification orchestration                                   |

`landing/` is the source seller's commercial site. It is outside the root workspace and is never
copied into a generated buyer product. `operations/` is also seller-only and excluded from buyer
output.

## Server domain shape [#server-domain-shape]

Every product domain uses explicit layers:

```text
contracts schema
      ↓
routes.ts        HTTP mapping, session, validation, response status
      ↓
service.ts       policy, provider-neutral orchestration, transactions
      ↓
repository.ts    parameterized SQL and persistence mapping
      ↓
Postgres
```

Routes do not contain SQL. Repositories do not know Hono. Services do not accept raw provider
payloads after adapter normalization. Wire schemas live once in `packages/contracts`; database rows
are mapped rather than exposed accidentally.

The optional [Add a domain end-to-end](./golden-paths/add-domain-end-to-end.md) playbook provides a
tested implementation route. You may use another approach, but preserve the ownership, failure,
migration, contract, and test rules above rather than copying a nearby file mechanically.

## Identity and authorization [#identity-and-authorization]

Better Auth owns `/auth/*`, password hashing, verification, password reset, magic links, Google
OAuth, cookies, and sessions. `sessionAuth` resolves the cookie through `auth.api.getSession()` and
places only `session.user.id` into Hono context.

Product authorization is separate from authentication:

1. the route requires a verified session;
2. the repository receives the current `userId`;
3. private reads and writes include the ownership predicate in SQL;
4. a missing or cross-owner private record returns the same 404 shape when existence must remain
   private.

There is no application access JWT, refresh token, local-storage token, parallel user key, or
generic 403 authorization helper.

See [Authentication](./authentication.md) and
[Add an authenticated route](./golden-paths/add-authenticated-route.md).

## Billing and entitlement flow [#billing-and-entitlement-flow]

The browser may request checkout, but it never grants access:

```mermaid
sequenceDiagram
  participant Browser
  participant API
  participant Provider
  participant DB as Postgres

  Browser->>API: create checkout (session cookie + plan)
  API->>Provider: product/price ID + external user ID
  Provider-->>Browser: hosted checkout
  Provider->>API: signed webhook
  API->>API: verify and normalize in adapter
  API->>DB: claim event + mutate subscription in one transaction
  API->>DB: derive paid entitlement
  Browser->>API: fetch billing status
  API-->>Browser: server-derived access
```

Subscription/customer rows are provider-neutral. The event claim and state mutation are one
transaction; duplicate events are ignored and older provider timestamps cannot restore stale access.
Trial, active, past-due, canceled-with-time-remaining, and expired states have explicit policy in
`domains/billing/entitlements.ts`.

Account deletion is blocked while the local provider subscription can renew or an already-paid
period remains. Deleting application data does not cancel a remote provider subscription.

## Email and local development [#email-and-local-development]

React Email source templates are rendered into the server's required HTML inventory. The server
preloads all templates before serving traffic. Resend handles production delivery; when Resend is
unset in development/test, the complete email and action URL are logged.

The fallback makes authentication deterministic without vendor keys. Production environment
validation requires Resend so reset, verification, and magic-link URLs cannot leak through the local
log path.

## Upload flow [#upload-flow]

The only included upload purpose is an owner-bound profile image:

1. validate purpose, MIME allowlist, declared size, target ownership, and entitlement;
2. persist a pending claim and issue a 15-minute provider presigned `PUT`;
3. confirm against actual provider metadata and the pinned object version;
4. atomically consume the claim or mark it failed and best-effort delete the object.

The path prevents URL-only ownership, replayed confirmation, and trusting the browser's claimed
size. Storage remains optional locally. See [Add an upload type](./golden-paths/add-upload-type.md).

## Configuration boundary [#configuration-boundary]

`apps/server/src/config/env.ts` is the sole server environment reader. Zod validates deployment
strings, converts them into the typed `Config`, and enforces atomic optional integrations. Domain
code consumes `config()`; it must not read `process.env` ad hoc.

Only explicitly public web values use `VITE_*`. Never place provider tokens, auth secrets, webhook
secrets, database URLs, or private DSNs in the web build.

## Runtime and deployment lifecycle [#runtime-and-deployment-lifecycle]

The server startup sequence is:

1. validate environment;
2. connect to Postgres;
3. apply pending dbmate migrations;
4. register application events;
5. locate and preload every required email template;
6. bind the Bun server.

The compiled binary still requires external migration SQL and rendered templates. The Docker image
copies both. Missing required resources fail startup rather than producing a delayed auth or billing
failure.

`/health` is liveness. `/ready` queries the initialized database and gates Railway or Dokploy
traffic. See [Deployment](./deployment.md).

## State ownership [#state-ownership]

* TanStack Query owns browser server-state caching and invalidation.
* Zustand owns deliberate client-only state, not a copy of the API cache.
* React Router owns route composition and protected navigation.
* Postgres is the application system of record.
* dbmate is the sole schema/migration history.
* Better Auth is the sole session authority.
* Provider adapters own raw external payloads and SDK types.
* Zod contracts own public request and response shapes.

## Deliberate limits [#deliberate-limits]

The v1 contract excludes multiple frontend frameworks, mobile, enterprise multi-tenancy, seat
billing, admin, i18n, a generic job queue, a second billing provider, and a broad infrastructure
matrix. These are not missing toggles; adding one requires a validated buyer job and a complete
support contract.

Vite+ 0.x and TypeScript 7 are pinned early-adopter dependencies. Their operational rules and
package-native escape hatches are in [Setup](./setup.md).


# Authentication (/docs/authentication)



# Authentication [#authentication]

Better Auth 1.6 is the only authentication and session implementation. The pre-release schema has
one canonical identity, `users.id`; domain and wire code may call it `user_id`, but there is no
second stored application-user key.

## Included flows [#included-flows]

| Flow                  | Contract                                                                                 |
| --------------------- | ---------------------------------------------------------------------------------------- |
| Email/password signup | 8–128 characters, Bun password hashing, verification required, no immediate auto sign-in |
| Email verification    | sent on signup/sign-in, successful verification signs the user in                        |
| Password sign-in      | only a valid verified identity receives a session                                        |
| Password reset        | reset email through the typed mailer; successful reset revokes existing sessions         |
| Magic link            | 15-minute expiry, token stored hashed, successful token is single-use                    |
| Google OAuth          | enabled only when both server credentials exist; missing config fails closed             |
| Account linking       | explicit linking only; implicit email-based linking is disabled                          |
| Session               | secure Better Auth cookie, 7-day expiry, 1-day update age                                |
| Sign-out              | Better Auth session revocation plus browser query/client-state clearing                  |

Google is the only included social provider. Adding providers to match a competitor is not a release
requirement.

## Request path [#request-path]

The Hono app mounts Better Auth directly:

```text
browser authClient
  → /auth/*
  → Better Auth handler
  → Better Auth tables in Postgres
```

Protected product routes use:

```text
HttpOnly session cookie
  → sessionAuth
  → auth.api.getSession(headers)
  → c.var.user_id = session.user.id
  → ownership-scoped repository query
```

The browser uses `credentials: 'include'` for both Better Auth and the typed Hono RPC client. It
does not store an access token in local storage. Do not add custom access/refresh JWTs, a second
cookie, a second password implementation, or another session table.

## Database ownership [#database-ownership]

dbmate owns these Better Auth-compatible tables:

* `users`;
* `auth_sessions`;
* `auth_accounts`;
* `auth_verifications`;
* `auth_rate_limits`.

Better Auth uses its supported `pg` adapter while application repositories use `bun:sql`. Better
Auth CLI output may be inspected to discover a schema change, but the reviewed change must become a
dbmate migration. Never run a second migration authority against a buyer database.

Sessions and accounts reference `users.id` with cascading database deletes. The application
soft-deletes the user, then removes live sessions and reusable account credentials transactionally.

## Authorization contract [#authorization-contract]

Authentication proves identity; it does not prove resource ownership, role, or billing entitlement.

For a private resource:

1. mount `sessionAuth`;
2. read identity only through `requireUserId(c)`;
3. pass that ID into service and repository methods;
4. include the ownership predicate in the read/write SQL;
5. use a non-disclosing 404 for absent and wrong-owner private records;
6. apply billing entitlement separately when the feature requires it.

Do not accept a `user_id`, owner ID, role, or entitlement from request input. Consult the optional
[Add an authenticated route](./golden-paths/add-authenticated-route.md) playbook for a maintained
implementation and verification route.

## Email behavior [#email-behavior]

Verification, reset, and magic-link callbacks use the typed mailer. In development/test without
Resend, the complete message and action URL are logged so the flow is deterministic. In production,
`RESEND_API_KEY` is required and the log fallback cannot start.

Treat local logs as sensitive: they contain short-lived authentication URLs. Do not send console
breadcrumbs to error monitoring, publish logs, or enable the fallback in a shared/production
environment.

## Origins, cookies, and proxy IPs [#origins-cookies-and-proxy-ips]

`FRONTEND_URL`, optional `MARKETING_SITE_URL`, and `CORS_ORIGIN` define trusted browser origins.
Hono permits credentials only for that configured set. Better Auth uses the same trusted origins.

Set `TRUSTED_PROXY_PROFILE` to `direct`, `railway`, `dokploy`, `aws-alb`, `gcp-cloud-run`, or
`azure-container-apps`. The Bun boundary ignores forwarding headers in direct mode. For managed
ingress profiles it overwrites the internal client-IP header from the provider's expected hop shape.
Prevent direct access around the selected ingress.

Better Auth and application endpoint rate limits are database-backed. Atomic fixed-window updates
share each application budget across replicas.

## Account deletion and billing [#account-deletion-and-billing]

Deleting an application account cannot silently cancel a remote provider subscription. The server
therefore returns `409 CONFLICT` while the latest local subscription is active, trialing, past due,
or canceled with paid time remaining.

The buyer must cancel through the selected provider's Billing portal and wait until the paid period
ends. Only then can the application soft-delete the user and remove sessions/accounts in one
transaction. Deletion is a terminal authentication state: Better Auth rejects every later session
creation for that user, and a database trigger closes the status-check/session-insert race. A fresh
magic link therefore cannot restore a deleted account. Product-specific data retention or immediate
statutory erasure requirements may require a separate reviewed cancellation-and-deletion workflow
before launch.

## Verification [#verification]

The real Postgres HTTP suite proves:

* password signup, verification, authenticated session resolution, and logout;
* password reset and revocation of existing sessions;
* magic-link signup and single-use replay rejection;
* a fresh magic link cannot recreate a deleted user's session;
* optional Google behavior fails closed when credentials are absent;
* unauthenticated requests return 401;
* cross-owner private records use non-disclosing 404 behavior;
* account deletion cannot orphan an active provider subscription.

Run:

```sh
RUN_DB_INTEGRATION_TESTS=1 \
DATABASE_URL=postgresql://app:app@localhost:5432/app?sslmode=disable \
bun test --cwd apps/server
```

Auth changes require this suite plus a production-like browser test of cookies, redirects, email
delivery, and exact deployed origins. Unit tests alone are insufficient.

## Common failures [#common-failures]

| Failure                            | Inspect                                                                    |
| ---------------------------------- | -------------------------------------------------------------------------- |
| Sign-in loops to login             | web/API origins, cookie, `credentials: include`, and Better Auth base URL  |
| Verification/reset email absent    | local server log or Resend sender/domain/provider error                    |
| Magic link already invalid         | expiry, exact origin, or expected single-use replay protection             |
| Google button/action fails         | both Google credentials and `SERVER_URL/auth/callback/google` registration |
| Requests all share one rate bucket | selected `TRUSTED_PROXY_PROFILE` and provider forwarding shape             |
| Delete account returns 409         | cancel Billing, then wait for the recorded paid period to end              |

Do not weaken verification, cookie policy, trusted origins, account-linking policy, proxy trust, or
rate limits to make these failures disappear.


# Background work (/docs/background-work)



# Background work decision [#background-work-decision]

The v1 buyer product has no generic worker, queue, scheduler, or durable job system. This is a
deliberate support and security boundary, not an unfinished Cloudflare example.

The historical Cloudflare/Gemini document processor was removed because it had no Better Auth
authorization handoff, application-owned job state, replay/idempotency contract, cost limit,
complete failure recovery, zero-key local path, or end-to-end deployment proof.

## What exists [#what-exists]

* synchronous Bun/Hono request handling with a 30-second application timeout;
* transactional Postgres writes;
* signed provider webhooks processed synchronously;
* best-effort post-commit billing notification dispatch;
* deployment/platform operations outside the application, such as Postgres backups.

An unawaited notification promise is not durable background infrastructure. It is explicitly
non-critical and can be lost if the process exits.

## What does not exist [#what-does-not-exist]

* a queue client or broker;
* worker deployment/package;
* job, attempt, retry, or dead-letter tables;
* a shared worker JWT/secret;
* scheduled application cleanup;
* durable AI processing;
* a queue dashboard or operational purge/replay path.

Do not restore removed code from Git history or describe provider-side asynchronous APIs as an
application job system.

## Decision matrix [#decision-matrix]

| Job requirement                                              | v1 decision                                     |
| ------------------------------------------------------------ | ----------------------------------------------- |
| bounded work required to form the HTTP response              | perform synchronously with timeout/idempotency  |
| optional notification after committed state                  | best effort only; document possible loss        |
| can be deferred without breaking validated buyer outcome     | defer or remove from v1                         |
| long-running/retryable/costly work required for user outcome | unsupported until product-specific design proof |
| repeated validated job across real products                  | investigate the smallest durable executor       |

The word “AI,” “cron,” or “background” in a PRD is not sufficient evidence for infrastructure.

## Re-entry bar [#re-entry-bar]

A product-specific executor may be investigated only after a real job defines:

1. Better Auth-to-executor authorization with purpose, audience, owner/subject, short expiry or
   server-side claim, and replay behavior;
2. application-owned job identity and persisted state machine;
3. idempotency key and duplicate-provider-effect handling;
4. bounded attempts, retry/backoff, terminal failure, dead letter, and user-visible recovery;
5. input type/size, concurrency, provider spend, and abuse limits;
6. retention, deletion, cleanup, and product-data ownership;
7. zero-key local disabled/fake behavior;
8. deployment owner, health/readiness, logs/metrics/alerts, rollback, and incident procedure;
9. one complete deployed smoke from authenticated request through terminal success/failure;
10. measured support and infrastructure cost that the validated outcome justifies.

Write a new ADR before implementation. One approved job does not justify selling a generic queue.

## Security constraints [#security-constraints]

* Never pass a Better Auth cookie or long-lived universal secret to an executor.
* Never use a provider task ID as application ownership.
* Persist authorization and intended state before external work.
* Never put customer payloads, credentials, or signed URLs into queue names or evidence.
* Required work cannot rely on an in-memory map, timer, or unawaited promise.
* Cleanup must be bounded, observable, retryable, and owner-safe.
* A queue purge is destructive incident action, not routine recovery.

## Buyer response to a background-work PRD [#buyer-response-to-a-background-work-prd]

Consult the optional [Decide background work](./golden-paths/decide-background-work.md) playbook.
The valid outcomes are:

* supported synchronous work;
* defer/re-scope;
* unsupported in v1;
* approved product-specific design investigation.

For the first three outcomes, do not add dependencies, environment variables, deployments, or
commercial claims. For an approved investigation, the ADR and evidence above precede code.

The authoritative removal decision is `docs/decisions/0001-exclude-background-worker-from-v1.md`.


# Billing (/docs/billing)



# Billing [#billing]

The application supports Polar, Stripe, and Dodo Payments behind one provider-neutral application
boundary. A deployment selects exactly one adapter with `BILLING_PROVIDER`; providers are not
combined, and only the selected provider can change local entitlements.

This is billing for the SaaS built from the foundation. It is separate from the seller's
source-product purchase and fulfillment design in `docs/commercial/fulfillment.md`.

## Modes and provider selection [#modes-and-provider-selection]

| Mode           | Configuration                                     | Expected behavior                                                                 |
| -------------- | ------------------------------------------------- | --------------------------------------------------------------------------------- |
| Local zero-key | selected provider credentials/products all unset  | app trial works; plans are empty; checkout/portal are unavailable; no provider IO |
| Automated test | normalized fake provider plus disposable Postgres | ordering, replay, rollback, and entitlement run without external calls            |
| Sandbox/test   | complete selected-provider test configuration     | hosted checkout, portal, and signed lifecycle events without a real charge        |
| Production     | complete selected-provider live configuration     | real lifecycle; release smoke and incident ownership are required                 |

Choose one:

```dotenv
BILLING_PROVIDER=polar # or stripe or dodo
```

Configuration is atomic for the selected provider. Leave every selected-provider credential and
product/price ID unset for zero-key development. Once any is set, provide its API credential,
webhook secret, and at least one unique plan mapping:

| Provider      | Credentials                                                           | Plan identifiers             | Environment                                      |
| ------------- | --------------------------------------------------------------------- | ---------------------------- | ------------------------------------------------ |
| Polar         | `POLAR_ACCESS_TOKEN`, `POLAR_WEBHOOK_SECRET`, `POLAR_ORGANIZATION_ID` | `POLAR_*_PRODUCT_ID`         | `POLAR_ENVIRONMENT=sandbox\|production`          |
| Stripe        | `STRIPE_SECRET_KEY`, `STRIPE_WEBHOOK_SECRET`                          | `STRIPE_*_PRICE_ID`          | determined by the Stripe key                     |
| Dodo Payments | `DODO_PAYMENTS_API_KEY`, `DODO_PAYMENTS_WEBHOOK_SECRET`               | `DODO_PAYMENTS_*_PRODUCT_ID` | `DODO_PAYMENTS_ENVIRONMENT=test_mode\|live_mode` |

Each provider has monthly and yearly display-price variables matching its identifier prefix.
Displayed price is application configuration, not provider authority. Compare identifier, interval,
currency, and charged amount during the release smoke.

Inactive-provider variables are ignored. This permits deliberate provider switching without allowing
two providers to mutate the same entitlement. Switching an application that already has customers
requires a migration and reconciliation plan; changing `BILLING_PROVIDER` alone is not a
live-customer migration.

## Boundaries [#boundaries]

```text
packages/billing
  provider.ts             provider-neutral methods
  types.ts                normalized customers/subscriptions/payments/events
  providers/polar.ts      Polar SDK and normalization
  providers/stripe.ts     Stripe SDK and normalization
  providers/dodo.ts       Dodo SDK and normalization

apps/server/src/domains/billing
  routes.ts               session endpoints and selected-provider webhook route
  service.ts              plan mapping, event transaction, notifications, app trial
  repository.ts           provider-neutral customers/subscriptions/event ledger
  entitlements.ts         sole paid-access policy
```

Raw SDK payloads stop inside their provider adapter. Hono routes, repositories, web code, and public
contracts never accept a provider SDK object.

## Checkout and portal [#checkout-and-portal]

Authenticated users can list configured plans, create a hosted checkout for a configured plan, open
the selected provider's portal after a provider customer exists, and fetch server-derived
subscription/trial/entitlement state.

Checkout derives the user and email from Better Auth. It sends `user_id` and `plan_type` as
provider-side metadata. The return URL is UX/recovery information only and never grants access.
Stripe receives its own checkout-session placeholder; Dodo receives a plain return URL; Polar
receives its native checkout placeholder. Polar also receives the buyer address resolved by the
selected `TRUSTED_PROXY_PROFILE` so its hosted checkout can localize currency and calculate tax from
the buyer rather than from the API server. Keep the application origin inaccessible around that
trusted ingress.

Portal creation uses the provider customer ID stored from a verified webhook. Stripe and Dodo
require that provider-side ID; Polar uses the application external customer ID. Portal-session
creation remains rate-limited.

## Trials and entitlements [#trials-and-entitlements]

The application has a 14-day local product trial starting at `users.created_at`. This is separate
from any provider-side trial.

Paid access is decided only in `entitlements.ts`:

| Local status                                 | Paid access | New checkout blocked | Recovery behavior                        |
| -------------------------------------------- | ----------- | -------------------- | ---------------------------------------- |
| `active`                                     | yes         | yes                  | portal available                         |
| `trialing`                                   | yes         | yes                  | provider trial                           |
| `canceled`, period end still in future       | yes         | yes                  | access until exact period boundary       |
| `past_due`                                   | no          | yes                  | update payment method; do not double-buy |
| `unpaid`                                     | no          | no                   | provider state is inactive               |
| `paused`, `incomplete`, `incomplete_expired` | no          | no                   | no paid entitlement                      |
| absent or expired cancellation               | no          | no                   | app trial may still grant product access |

Provider statuses are normalized fail-closed. For example, Dodo `on_hold` maps to `past_due` and
`failed` maps to `unpaid`; Stripe and Polar lifecycle states map to the same application policy. A
scheduled cancellation remains entitled only before its provider period end.

## Webhook contract [#webhook-contract]

Register the endpoint matching the selected provider:

```text
https://<api-origin>/webhook/<polar|stripe|dodo>
```

The route returns 404 when the path provider is not the selected provider. It returns 503 when the
selected webhook integration is intentionally unset and 403 for a missing or invalid signature.

Each adapter verifies the raw body before normalization:

* Polar uses Standard Webhooks headers and its SDK verifier.
* Stripe requires `Stripe-Signature` and constructs the event with Stripe's verifier.
* Dodo requires its Standard Webhooks ID/timestamp/signature headers and official verifier.

After verification, the application:

1. normalizes the provider event;
2. claims `(provider, event_id)` and mutates subscription/customer/access state in one transaction;
3. orders state using provider event time in `provider_modified_at`;
4. commits before best-effort email/in-app notification.

Duplicate committed events are no-ops. A failed mutation rolls back the claim so the provider can
retry. An older event may be claimed without overwriting newer state. Unknown products,
contradictory plan metadata, and missing user correlation fail the transaction instead of defaulting
to a paid plan.

Subscribe only to lifecycle events handled by the selected adapter:

* Polar: subscription created/active/updated/canceled/uncanceled/revoked/past-due and order paid.
* Stripe: customer subscription created/updated/deleted and invoice payment succeeded/failed.
* Dodo: payment succeeded/failed and subscription active/renewed/plan-changed/updated,
  update-payment-method, cancelled/expired/on-hold/failed/paused.

Provider-valid unrelated events are acknowledged and recorded as unhandled. Treat a newly required
event as an adapter/version change with a regression fixture.

## Notifications, deletion, and failure semantics [#notifications-deletion-and-failure-semantics]

Billing state commits before lifecycle notification delivery. A notification failure must not make
the provider retry an already-committed financial mutation. The current implementation is not backed
by an outbox, so an operator resend must not replay billing state.

Deleting local application data does not cancel a remote subscription. Account deletion returns
`409 CONFLICT` while billing can renew or paid time remains. The customer cancels through Billing
and waits for the period boundary before local account deletion.

Never manually toggle `users.is_premium`, delete event claims, or grant access from a checkout
redirect. Use [Diagnose a failed webhook](./golden-paths/diagnose-failed-webhook.md).

## Provider verification [#provider-verification]

Before production, run the same lifecycle in the selected provider's sandbox/test mode:

1. create every enabled recurring product/price;
2. configure the complete selected provider;
3. register the exact webhook route and events;
4. run checkout with a fresh verified application user and a deliverable email address accepted by
   the provider sandbox;
5. confirm customer correlation, subscription, event claim, entitlement, and portal;
6. replay the same event and deliver an older lifecycle event;
7. fail and recover a payment;
8. schedule cancellation and verify the exact access boundary;
9. reject an invalid signature;
10. verify account deletion remains blocked until billing is resolved.

The optional [Add a billing plan](./golden-paths/add-billing-plan.md),
[Add a billing-gated feature](./golden-paths/add-billing-gated-feature.md), and
[Diagnose a failed webhook](./golden-paths/diagnose-failed-webhook.md) playbooks provide maintained
implementation and recovery sequences.

## Reconciliation and alerting [#reconciliation-and-alerting]

Run a bounded dry-run against the selected provider:

```sh
vp run --filter @app/server billing:reconcile -- --limit=100
```

Apply remote subscription state only after reviewing that report:

```sh
vp run --filter @app/server billing:reconcile -- --apply --limit=100
```

The compiled deployment artifact supports the same operator command:

```sh
./server billing:reconcile --apply --limit=100
```

The command exits non-zero for unresolved drift, provider errors, or a locally stored subscription
that no longer exists remotely. Configure Railway or Dokploy scheduled-job failure alerts around
that exit status. Missing remote subscriptions are never silently deleted or revoked; investigate
them in the provider dashboard first. This explicit bounded command is the v1 recovery mechanism,
not a generic worker or queue.

## Known limits [#known-limits]

* Only one provider can be active per deployment.
* Plan currency and amount are not reconciled into the application plan response.
* There is no generic coupon, tax display, usage, seat, or refund UI.
* Reconciliation is bounded and operator-triggered; each deployment must schedule it and route a
  non-zero exit to its platform alerting channel.
* Adapter unit/integration tests do not replace a real Polar sandbox, Stripe test-mode, or Dodo
  test-mode checkout-to-webhook proof.


# Configuration (/docs/configuration)



# Configuration [#configuration]

Configuration is owned by the layer that consumes it. The server validates private and deployment
values once; the browser receives only intentionally public `VITE_*` values; deployment systems own
their secret stores; provider dashboards own provider resources. Do not turn a local `.env` file
into a second configuration authority.

The exact variables and defaults live in `apps/server/.env.example` and `apps/web/.env.example`.
They are executable examples, while this page explains when a group is required and where it
belongs.

## Configuration map [#configuration-map]

| Concern                       | Canonical owner                                                 | Required when                    | Key examples                                                                                    |
| ----------------------------- | --------------------------------------------------------------- | -------------------------------- | ----------------------------------------------------------------------------------------------- |
| Database and runtime          | Server environment validated in `apps/server/src/config/env.ts` | Always                           | `DATABASE_URL`, pool limits, `ENVIRONMENT`, `PORT`                                              |
| Identity and browser origin   | Server environment and browser public environment               | Always outside local development | `BETTER_AUTH_SECRET`, `FRONTEND_URL`, `SERVER_URL`, `VITE_API_URL`, `TRUSTED_PROXY_PROFILE`     |
| Deployment identity           | Deployment environment and immutable release artifact           | A deployed API exists            | `DEPLOYMENT_CLOUD`, `DEPLOYMENT_ENVIRONMENT`, `RELEASE_ID`, `IMAGE_DIGEST`                      |
| Transactional email           | Server environment and Resend account                           | Production email is sent         | `RESEND_API_KEY`, `EMAIL_FROM`, `EMAIL_TEMPLATES_DIR`                                           |
| Billing                       | Server environment and exactly one provider dashboard           | Paid access is enabled           | `BILLING_PROVIDER`, selected provider credential, webhook secret, product IDs, display prices   |
| Object storage                | Server environment and selected storage account                 | Uploads are enabled              | `STORAGE_PROVIDER`, bucket values, R2/S3/GCS credentials                                        |
| OAuth                         | Server environment and provider dashboard                       | Google sign-in is enabled        | `GOOGLE_CLIENT_ID`, `GOOGLE_CLIENT_SECRET`, `VITE_GOOGLE_CLIENT_ID`                             |
| Observability and alerts      | Server or browser environment and selected service              | Sentry or Slack is enabled       | `SENTRY_PROJECT_IDS`, browser `VITE_SENTRY`, build-only Sentry upload values, `SLACK_BOT_TOKEN` |
| Product identity and policies | Source-owned browser configuration and public environment       | Before real users                | `BRAND_NAME`, `VITE_TERMS_URL`, `VITE_PRIVACY_URL`                                              |
| Local-only demo sign-in       | Local web environment and a local seeded account                | Development convenience only     | `VITE_DEV_LOGIN_EMAIL`, `VITE_DEV_LOGIN_PASSWORD`                                               |

`VITE_*` values are compiled into browser assets. They must never contain server credentials,
webhook secrets, database URLs, private origins, or provider API keys.

## Environment by purpose [#environment-by-purpose]

| Environment       | Purpose                                                | Required behavior                                                                                                                                              |
| ----------------- | ------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Local development | Build product behavior without third-party accounts    | Postgres and Better Auth work. Email uses the local log; billing, storage, Slack, Sentry, and Google remain visibly disabled unless configured completely.     |
| Test              | Prove isolated behavior in CI or a disposable database | Test-only values, no external delivery, no shared database, and no production credentials.                                                                     |
| Staging           | Rehearse the exact deployment and provider boundary    | Separate origins, database, secret store, and provider test/sandbox resources from production.                                                                 |
| Production        | Accept real users and durable data                     | Exact HTTPS origins, unique secret, selected proxy profile, verified sender, backup/restore proof, and complete live configuration for every enabled provider. |

Do not point a preview deployment at the production API. Use a stable staging browser/API pair under
the same registrable site when testing cookies.

## Configure in dependency order [#configure-in-dependency-order]

1. Copy the two example environment files to ignored local files. Set only Postgres, the local
   Better Auth secret, and local origins first.
2. Run the zero-key path from [Setup](./setup.md). A local account, verification, reset, and magic
   link must work before adding provider credentials.
3. Choose the production topology and set exact origins, proxy profile, deployment identity, and
   secret ownership as described in [Deployment](./deployment.md).
4. Enable one optional capability at a time. Supply every value in its group, start the server, and
   run its failure and success smoke before enabling the next group.
5. Record the variable owner, rotation owner, source dashboard, and last verified date in the
   deployment's private operations record. Record names and owners, never values, in tickets or
   release notes.

The [add an environment variable](./golden-paths/add-environment-variable.md) path is required when
you change the application configuration contract. It covers validation, examples, generation,
deployment, documentation, and verification together.

## Optional capability groups [#optional-capability-groups]

| Capability | All-or-nothing contract                                                                                                                                                                                                          | Where to continue                                                     |
| ---------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------- |
| Billing    | Select `polar`, `stripe`, or `dodo`; provide that provider's credential, webhook secret, and at least one monthly or yearly product mapping. Leave the selected provider's integration values all unset for zero-key local work. | [Billing](./billing.md)                                               |
| Storage    | Select one provider and provide the complete bucket, public URL, region, and matching credentials required by that provider.                                                                                                     | [Object storage](./storage.md)                                        |
| Google     | Provide server client ID/secret, browser client ID, exact provider callback, and allowed origins together.                                                                                                                       | [Authentication](./authentication.md)                                 |
| Sentry     | Provide the public browser DSN and server allowlist together; build upload credentials belong only in CI or the protected build environment.                                                                                     | [Security](./security.md)                                             |
| Slack      | Provide the server token only when sanitized operational alerts have a defined owner. Its absence is a valid disabled state.                                                                                                     | [Notifications and marketing endpoints](./notifications-marketing.md) |

Changing `BILLING_PROVIDER` after customers exist is not ordinary configuration. It requires an
explicit entitlement migration and reconciliation plan; one deployment must not combine provider
entitlements.

## Safe configuration checks [#safe-configuration-checks]

The server fails at startup for invalid required values and partial selected billing or storage
configuration. Validate the intended environment without printing secrets:

```sh
vp run --filter @app/server typecheck
vp run --filter @app/server test:unit
vp run --filter @app/server build
```

Then exercise the selected deployment's `/ready` endpoint and the enabled provider's real test or
live smoke. Use [Troubleshooting](./troubleshooting.md) for the first observable failure instead of
loosening origin, cookie, validation, or signature policy.

## Values that must stay out of source control [#values-that-must-stay-out-of-source-control]

Never commit real `.env` files, `backend.hcl`, `.tfvars`, state, cloud credentials, database URLs,
webhook secrets, Better Auth secrets, OAuth secrets, provider API keys, signed URLs, or customer
data. A name or public origin can be documented; a credential value cannot.


# Customization (/docs/customization)



# Customization contract [#customization-contract]

Customize the generated product in layers. Preserve the architecture and verification boundaries
that make later agent work predictable; replace the product decisions that are intentionally
neutral.

## Start from a generated repository [#start-from-a-generated-repository]

Use `create-app` once to establish the product name, npm scope, display brand, unique Better Auth
secret, and independent Git history. Do not build a product inside the delivered foundation checkout
and do not run the scaffolder over an existing repository.

The initial brand replacement is deliberately narrow. It does not choose a commercial name, domain,
logo, customer promise, sender, legal entity, support channel, or accessible palette. The optional
[Customize branding and design tokens](./golden-paths/customize-branding-design-tokens.md) playbook
provides a maintained route for those changes. No domain, provider account, package registry, or
permanent organization may be reserved without an explicit product decision.

## Customization map [#customization-map]

| Product decision             | Primary surfaces                                                                                                            | Preserve                                                                                  |
| ---------------------------- | --------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------- |
| Name and runtime identity    | `BRAND_NAME` in `apps/web/src/configs/config.ts` and `apps/docs/lib/shared.ts`, static metadata, PWA manifest, email source | counted brand manifest, context-safe replacement, text-free mark                          |
| Customer outcome and copy    | web metadata, auth/settings states, empty/error states, email copy                                                          | accurate capability claims, actionable failures, no inherited example behavior            |
| Visual system                | `apps/web/src/styles/_variables.css`, CSS Modules, local font/image assets                                                  | semantic token names, focus-visible, contrast, reduced motion, responsive states          |
| Durable product capability   | migration, shared Zod contract, server repository/service/routes, web service/module                                        | ownership in SQL, layer boundaries, Hono RPC types, negative-path tests                   |
| Authentication options       | typed server environment, `apps/server/src/lib/auth.ts`, login UI                                                           | Better Auth as sole session authority, atomic provider configuration, cookie/origin rules |
| Legal policy links           | `VITE_TERMS_URL`, `VITE_PRIVACY_URL`, login acknowledgment                                                                  | real public URLs, no placeholder legal claims                                             |
| Plans and paid access        | `packages/contracts/src/billing.ts`, selected provider adapter, entitlement service, settings UI                            | provider normalization, server-side gates, webhook idempotency/order                      |
| Transactional communication  | `packages/email/src`, rendered server templates, mailer/event handlers                                                      | typed template inventory, escaping, local fallback, generated-artifact workflow           |
| Public upload types          | shared upload contract, upload domain, storage adapters, web upload service/UI                                              | owner-scoped claims, metadata confirmation, expiry, size/type enforcement                 |
| Optional integration removal | full inventory in [Removing subsystems](./removing-subsystems.md)                                                           | no dead keys/routes/packages/claims; fresh-buyer proof                                    |
| Production topology          | Railway or Dokploy API/Postgres, Vercel SPA, provider environment, health/readiness                                         | supported target only after equivalent build, proxy, backup, rollback, and smoke evidence |

For product behavior, [Add a domain end to end](./golden-paths/add-domain-end-to-end.md) is an
optional tested playbook. A different implementation is valid when it preserves the architecture and
passes verification. A nearby component or route is an example of shape, not permission to skip
ownership, validation, loading, error, migration, or security decisions.

## Generated and source-owned files [#generated-and-source-owned-files]

Do not edit generated files manually:

* `apps/server/email-templates/*.html` comes from `packages/email/src`;
* `pnpm-lock.yaml` comes from the package manager;
* database migration history comes from dbmate migration files, not direct production edits;
* production bundles, PWA service workers, source maps, and the compiled server come from builds;
* favicon raster variants come from a reviewed source asset and image-generation workflow.

Source ownership means a buyer may fork any code. It does not make every fork part of the supported
foundation contract. Record deliberate architecture departures in a local decision file so a later
agent knows which upstream assumptions no longer apply.

## Product-specific files [#product-specific-files]

Keep product behavior easy to distinguish from the foundation:

* create a domain rather than adding business SQL to a generic route;
* keep product-specific constants, copy, events, and policies in the owning domain/module;
* add a migration instead of editing the released baseline;
* add local documentation for non-obvious invariants and external resources;
* keep provider-specific types inside the provider adapter;
* add tests that describe the product's owner, lifecycle, limits, and failure behavior.

Do not rename every architecture term to match a brand. Stable names such as `service`,
`repository`, `contracts`, `sessionAuth`, and semantic design tokens are navigation aids for people
and agents.

## Verification [#verification]

For every material customization:

```sh
vp check
vp run -r test
vp run -r build
pnpm audit --audit-level high
git diff --check
```

Add real Postgres integration tests for persistence/auth/billing/upload changes. Regenerate and
inspect email HTML after email changes. Inspect the production PWA and browser states after identity
or design changes. Exercise the selected supported Railway/Vercel or Dokploy/Vercel path after
topology, origin, cookie, or environment changes.

Before release, generate a second custom-scope/custom-brand reference app from the exact foundation
version. A foundation defect reproduces there; a product-only defect does not. Preserve that
distinction when requesting support.

## Completion criteria [#completion-criteria]

* The application has one accurate identity and no neutral placeholder or source-product behavior.
* Product domains preserve contract, route, service, repository, ownership, and migration
  boundaries.
* Generated artifacts match their source and the lockfile is frozen.
* Optional integrations are either configured atomically, visibly unavailable, or removed as a
  complete vertical slice.
* Checks, tests, builds, dependency audit, relevant provider flows, and rendered states pass.
* Local decision records explain intentional departures that will affect future upgrades.


# Deployment (/docs/deployment)



# Deployment [#deployment]

The foundation supports Railway, Dokploy, and three OpenTofu-managed backend clouds. Vercel and
Cloudflare remain static frontend hosts only.

Start with [Go live](./go-live.md) when coordinating product, provider, backup, and browser proof.
Use this page for the supported infrastructure and release mechanics.

| Backend | Native frontend | External frontend                              | API                     | PostgreSQL                    |
| ------- | --------------- | ---------------------------------------------- | ----------------------- | ----------------------------- |
| AWS     | S3 + CloudFront | Vercel SPA or Cloudflare Workers Static Assets | ECS Fargate + HTTPS ALB | RDS PostgreSQL 18             |
| GCP     | GCS + Cloud CDN | Vercel SPA or Cloudflare Workers Static Assets | Cloud Run               | Cloud SQL PostgreSQL 18       |
| Azure   | Static Web Apps | Vercel SPA or Cloudflare Workers Static Assets | Container Apps          | Flexible Server PostgreSQL 18 |

OpenTofu is included for every buyer and becomes more useful as environments and teams grow. The
repository supports OpenTofu 1.12.5 only. It does not claim Terraform compatibility.

## Choose a target [#choose-a-target]

Use Railway for the shortest managed deployment path. Use Dokploy when an operator owns the server,
database backups, updates, and monitoring. Choose AWS, GCP, or Azure when provider-native identity,
networking, observability, and managed PostgreSQL are required.

Each environment has one backend cloud, one object-storage provider, and one frontend target. AWS
defaults to S3 and may select R2. GCP defaults to GCS and may select R2. Azure uses R2; Azure Blob
storage is not supported.

* `native` creates the cloud-native static host.
* `vercel` accepts an existing exact production origin and creates no Vercel resources.
* `cloudflare` accepts an existing exact production origin and creates no Cloudflare resources.

Multi-cloud means portability between independent environments. It does not mean active-active
operation, cross-cloud replication, or shared state.

The application creator asks `Set up cloud deployment now?`. Choose yes to select the cloud, storage
provider, frontend host, environment, region, sibling production domains, and cost-first or HA
profile. It writes `deployment/config.json`, generates an exact protected-environment checklist, and
removes unused provider roots and frontend adapters. The selection is deliberately locked so a later
agent cannot silently turn one environment into a different cloud.

Choose no to keep every provider while the product is still local. Configure and prune them later
from the generated repository with:

```sh
pnpm deploy:configure
```

Restore removed paths from a fresh licensed release before changing a locked provider.

## Delivery files [#delivery-files]

* `apps/server/Dockerfile` builds the shared production image.
* `apps/server/railway.toml` defines the Railway release and service commands.
* `deploy/dokploy/compose.yml` defines the one-shot migration and API services.
* `apps/web` contains the static SPA and its native, Vercel, and Cloudflare adapters.
* `.github/workflows/deploy-cloud.yml` implements the cloud OIDC release sequence.

## Runtime contract [#runtime-contract]

The compiled image exposes two deployment commands:

```sh
./server migrate
./server serve
```

`migrate` resolves the external migration directory, takes a PostgreSQL advisory lock, applies each
pending dbmate up section transactionally, and exits. Concurrent invocations serialize. `serve`
validates its external email templates, connects to PostgreSQL, and starts HTTP without changing the
schema.

`/health` is process liveness and never queries PostgreSQL. `/ready` returns `200` only after
startup resources are loaded and PostgreSQL answers a probe. Managed load balancers and container
health checks use `/ready` for traffic admission.

Application endpoint limits use PostgreSQL atomic fixed windows, so replicas share one budget.
Better Auth keeps its own database limiter. Set pool limits so:

```text
maximum replicas × (APP_DB_POOL_MAX + AUTH_DB_POOL_MAX) ≤ 80% of database connections
```

Every cloud module enforces this calculation from its declared connection capacity.

## OpenTofu layout [#opentofu-layout]

```text
infra/
  bootstrap/{aws,gcp,azure}/
  modules/{aws,gcp,azure}/
  roots/{aws,gcp,azure}/
  tests/verify.sh
deployment/
  config.json
  README.md
```

Each root has an independent backend, provider constraints, committed provider lock, example
environment values, and mocked cost-first and HA tests. Environments use separate state paths or
state buckets, never OpenTofu workspaces.

`deployment/config.json` is the committed, non-secret source for common inputs.
`deployment/README.md` lists the state, OIDC, DNS, secret-manager, and GitHub environment work that
requires the buyer's cloud authorization.

Verify all roots with:

```sh
pnpm verify:infra
```

This runs `tofu fmt -check -recursive`, backend-free initialization with a read-only lock,
validation, and mocked `tofu test` plans for every root.

## State bootstrap [#state-bootstrap]

Run the matching bootstrap once with a tightly controlled administrator identity:

* `infra/bootstrap/aws/README.md` creates a private, encrypted, versioned S3 bucket using native
  `use_lockfile` locking.
* `infra/bootstrap/gcp/README.md` creates a uniform-access, versioned GCS bucket dedicated to one
  environment. GCS provides native state locking.
* `infra/bootstrap/azure/README.md` creates a private, versioned Blob container. The backend uses
  native blob leases.

Copy the output into `backend.hcl` outside version control. Real `backend.hcl`, `.tfvars`,
`.tfstate`, `.terraform/`, and generated cloud credentials are excluded from buyer scaffolds. Only
examples and provider locks ship.

## Common inputs and outputs [#common-inputs-and-outputs]

All roots accept `project_name`, `environment`, `region`, `storage_provider`, `frontend_target`,
`external_frontend_origin`, `api_domain`, `frontend_domain`, `image_digest`, `traffic_enabled`,
`ha_enabled`, API sizing, replica limits, pool limits, database sizing, backup retention, log
retention, deletion protection, and an optional alert email. The DNS zone input is provider-specific
because provider zone identifiers are not interchangeable.

The AWS and GCP roots provision native application storage only when `storage_provider` is `s3` or
`gcs`. They create separate staging and public buckets, exact-origin CORS, one-day staging expiry,
public profile-image reads, and workload-identity permissions. When `storage_provider` is `r2`, the
operator supplies R2 configuration through the cloud's runtime secret container. See
[Storage](./storage.md).

External frontends require `external_frontend_origin`. It must be the exact stable production
origin, not a preview URL. Guided setup derives it from the selected frontend domain. The workflow
supplies the immutable `sha256:` OCI `image_digest`; buyers do not guess the first digest.
`traffic_enabled` remains false until the first migration succeeds. Provider-specific SKU overrides
fail at provider planning or apply with the selected region and requested SKU visible; if a default
database SKU is unavailable, set the documented `db_instance_class`, `db_tier`, or `db_sku_name`
override instead of silently accepting a larger tier.

Every root returns:

* `api_origin` and `frontend_origin`;
* `container_repository`;
* `api_service_id`, `migration_job_id`, and the provider-specific `release_target`;
* `database_secret_id`, never its value;
* `dns_validation_records`;
* `native_frontend_target` and `native_frontend_deployment`;
* `observability_url`.

## AWS quickstart [#aws-quickstart]

Guided setup keeps the AWS root and removes the others. Bootstrap state once, put the Route 53 zone
or existing certificate identifiers in the protected `TFVARS_JSON` overlay, then use the deployment
workflow. For direct OpenTofu inspection:

```sh
cd infra/roots/aws
tofu init -backend-config=/protected/aws-backend.hcl
tofu plan -var-file=/protected/aws.tfvars
```

Cost-first runs one public-subnet Fargate task reachable only from the ALB security group and a
private, encrypted, single-AZ `db.t4g.micro` RDS instance. It creates no NAT gateway. HA runs two
API tasks in private subnets, one NAT gateway per availability zone, Multi-AZ RDS, and an ECS
deployment circuit breaker. The native frontend is a private S3 bucket read only through CloudFront
Origin Access Control, with immutable asset caching and no-cache HTML.

## GCP quickstart [#gcp-quickstart]

Guided setup keeps the GCP root and removes the others. Bootstrap state once, put the project and
Cloud DNS inputs in the protected `TFVARS_JSON` overlay, then use the deployment workflow. For
direct OpenTofu inspection:

```sh
cd infra/roots/gcp
tofu init -backend-config=/protected/gcp-backend.hcl
tofu plan -var-file=/protected/gcp.tfvars
```

Cost-first runs Cloud Run from zero to three instances and a private-IP zonal `db-f1-micro` Cloud
SQL instance. HA keeps at least one Cloud Run instance warm, uses regional Cloud SQL HA, and enables
point-in-time recovery. Cloud Run uses Direct VPC egress. The API sits behind the external HTTPS
load balancer so the `gcp-cloud-run` proxy profile can validate the provider-appended forwarding
shape. The native frontend uses a versioned GCS bucket, a backend bucket, Cloud CDN, and managed
TLS.

## Azure quickstart [#azure-quickstart]

Guided setup keeps the Azure root and removes the others. Bootstrap state once, put Azure DNS and
Key Vault identifiers in the protected `TFVARS_JSON` overlay, then use the deployment workflow. For
direct OpenTofu inspection:

```sh
cd infra/roots/azure
tofu init -backend-config=/protected/azure-backend.hcl
tofu plan -var-file=/protected/azure.tfvars
```

Cost-first runs Container Apps from zero to three replicas and a private `B_Standard_B1ms` Flexible
Server. HA keeps two API replicas warm and requests zone redundancy for the Container Apps
environment and PostgreSQL. Regions without the requested zone or SKU support fail visibly and
require an explicit region or tier override. The native frontend uses Static Web Apps and the
included `staticwebapp.config.json` for React Router fallback and hashed-asset caching.

## Frontend selection [#frontend-selection]

The browser has one provider-neutral backend variable:

```text
VITE_APP_ENV=production
VITE_API_URL=https://api.example.com
VITE_TERMS_URL=https://example.com/terms
VITE_PRIVACY_URL=https://example.com/privacy
```

### Native [#native]

The protected deployment workflow builds `apps/web` after the API is ready. AWS syncs hashed assets
with immutable caching and invalidates only `index.html`. GCP synchronizes the bucket, sets asset
and HTML cache metadata, and refreshes the HTML CDN entry. Azure retrieves the Static Web Apps
deployment credential at runtime through Azure OIDC and does not store it as a repository secret.

### Vercel SPA [#vercel-spa]

Create a Vercel project rooted at `apps/web`, keep source files outside the root available to the
build, and use the included `apps/web/vercel.json`:

* install: `npm install --global pnpm@11.18.0 && pnpm install --frozen-lockfile`;
* build: `pnpm run build:prod`;
* output: `dist`;
* production domain: the exact `frontend_domain` supplied to OpenTofu;
* `VITE_API_URL`: the `api_origin` output.

Prefer Vercel Git integration. Vercel Functions and Vercel databases are unsupported.

### Cloudflare Workers Static Assets [#cloudflare-workers-static-assets]

Create a Workers project rooted at `apps/web` and use `apps/web/wrangler.toml`. It contains only the
`dist` asset directory and `not_found_handling = "single-page-application"`; there is no Worker API
entry point. Prefer Cloudflare Git integration. An API-token deployment is optional, but the token
must live in a protected deployment environment. Cloudflare API Workers, D1, Hyperdrive, Containers,
Queues, and Durable Objects are unsupported.

## Documentation site [#documentation-site]

`apps/docs` is a separate Next.js application and is not deployed by the product frontend or
OpenTofu paths. It requires its own Node-compatible host and public-origin verification when a buyer
chooses to publish it. Follow [Operate the documentation site](./docs-site.md); do not point the SPA
deployment workflow at the docs workspace.

## DNS, CORS, and same-site auth [#dns-cors-and-same-site-auth]

Production uses stable HTTPS names such as `app.example.com` and `api.example.com`. Keep them under
the same registrable site so Better Auth cookies do not depend on third-party-cookie exceptions.
OpenTofu configures the exact frontend origin in `FRONTEND_URL`; Hono CORS and Better Auth trusted
origins reject any other origin.

Preview domains never use the production API. Give previews a stable staging alias and staging API
under the same registrable site.

`TRUSTED_PROXY_PROFILE` is one of `direct`, `railway`, `dokploy`, `aws-alb`, `gcp-cloud-run`, or
`azure-container-apps`. The runtime discards a user-supplied internal client-IP header and rebuilds
it from the selected ingress shape. Do not expose the container through another public path.

## Secrets [#secrets]

Never put Better Auth, email, billing, Sentry, R2 credentials, OAuth, or secret provider values in
committed `.tfvars`. OpenTofu creates the cloud secret container and runtime identity. A protected
workflow writes values directly to Secrets Manager, Secret Manager, or Key Vault before the first
application release.

Database credentials are generated by OpenTofu because the database resource requires them. They
remain sensitive in encrypted state and are written to a database URL secret. No root outputs the
credential value.

Required production application values remain documented in `apps/server/.env.example`, including
`BETTER_AUTH_SECRET`, `RESEND_API_KEY`, exact `FRONTEND_URL`, exact `SERVER_URL`, pool limits,
deployment metadata, the proxy profile, and `SENTRY_PROJECT_IDS` when the browser error tunnel is
enabled.

Provider-specific application contracts remain in [Billing](./billing.md), [Storage](./storage.md),
and the [background-work decision](./background-work.md).

## GitHub OIDC and release workflow [#github-oidc-and-release-workflow]

`.github/workflows/deploy-cloud.yml` uses a protected GitHub environment and OIDC. Configure only
the matching identity inputs:

* AWS: `AWS_ROLE_ARN`;
* GCP: `GCP_WORKLOAD_IDENTITY_PROVIDER` and `GCP_SERVICE_ACCOUNT`;
* Azure: `AZURE_CLIENT_ID`, `AZURE_TENANT_ID`, and `AZURE_SUBSCRIPTION_ID`.

Each environment also supplies protected `BACKEND_HCL` and `TFVARS_JSON` values plus public frontend
and legal URL variables. `TFVARS_JSON` is only the provider-specific overlay; the workflow merges it
with the locked non-secret `deployment/config.json`. Long-lived AWS keys, service-account JSON, and
Azure client secrets are unsupported.

The workflow rejects cloud, environment, or frontend dispatch inputs that disagree with the locked
configuration. On the first release it creates only the selected container repository, builds and
pushes one linux/amd64 image, provisions runtime infrastructure with public API traffic disabled,
and runs the migration primitive. A successful migration admits traffic; a failed migration leaves
the new service private or scaled to zero. Later releases preserve existing traffic until migration
succeeds, then update the API to the exact digest. API resources ignore image drift from OpenTofu so
an infrastructure apply cannot bypass the release primitive.

After readiness, the workflow builds the native frontend and runs:

```sh
API_ORIGIN=https://api.example.com \
WEB_ORIGIN=https://app.example.com \
bash scripts/verify-public-deployment.sh
```

Vercel and Cloudflare Git integrations deploy their own static build after `VITE_API_URL` is updated
to the stable API origin.

## Railway and Dokploy [#railway-and-dokploy]

Railway uses `apps/server/railway.toml`. Its `preDeployCommand` runs `./server migrate`, its start
command runs `./server serve`, and traffic waits for `/ready`.

Dokploy uses `deploy/dokploy/compose.yml`. A one-shot `migrate` service runs the same image and must
complete successfully before the API service starts. PostgreSQL remains private and persists under
`/var/lib/postgresql`. Verify the path with:

```sh
bash scripts/verify-dokploy-compose.sh
```

## Rollback and failed release recovery [#rollback-and-failed-release-recovery]

Deploy additive expand/contract migrations once more than one API revision can overlap. Never
automatically run database downs. On failure:

1. preserve the migration execution logs and image digest;
2. leave the previous API revision receiving traffic;
3. fix forward unless a reviewed down is demonstrably data-safe;
4. rerun the migration job with a new immutable image;
5. release the API only after the job exits zero and readiness passes.

Application rollback selects the prior image digest. Frontend rollback redeploys the prior static
artifact. If wire contracts changed, restore a compatible API/frontend pair.

## Promote cost-first to HA [#promote-cost-first-to-ha]

Set `ha_enabled=true`, review the cost and topology plan, confirm the selected region supports every
zone-redundant feature, and apply through the protected environment. Promotion changes availability;
it does not migrate providers or combine entitlements across deployments.

## Troubleshooting [#troubleshooting]

* Pool-capacity check failure: lower replicas or pool limits, or explicitly select a larger database
  tier and its documented connection capacity.
* Database SKU unavailable: set the provider-specific tier override or choose a supported region.
* API never ready: inspect database reachability, external migrations/templates, and the release
  digest metadata in structured logs.
* CORS failure: compare the literal browser `Origin` with `frontend_origin`; wildcards are not used.
* Incorrect client IP: confirm only the supported ingress reaches the service and select its exact
  proxy profile.
* Managed certificate pending: publish every returned DNS validation record and wait for provider
  issuance before retrying.
* OpenTofu lock mismatch: run the pinned OpenTofu 1.12.5 release, review provider changes,
  regenerate the lock intentionally, and commit it.

## Teardown [#teardown]

Teardown is destructive and never part of the deployment workflow. In a disposable environment, set
`deletion_protection=false`, apply that reviewed change, verify the selected state and cloud
account, then run `tofu destroy`. Delete state backends, backups, registries, or customer data only
with separate explicit authority. Production teardown requires a retained and restore-tested
database backup.

## Proof ledger [#proof-ledger]

Mocked cost-first and HA plans are repository evidence, not live-cloud acceptance. The landing page
must not advertise the nine frontend/backend combinations until disposable-environment records prove
custom-domain auth, CORS, CRUD ownership denial, migration failure, rollback, caching, logs, alarms,
no-drift plans, and apply/destroy for every combination.


# Operate the documentation site (/docs/docs-site)



# Operate the documentation site [#operate-the-documentation-site]

`apps/docs` is a separate Next.js documentation application. It renders the canonical Markdown and
MDX files from the repository-level `docs/` directory; there is no second `content/docs` copy.
Changing a guide changes the repository contract and the rendered site together.

The docs site is not the React product SPA and is not deployed by the application OpenTofu, Railway,
Dokploy, Vercel-SPA, or Cloudflare static-assets paths. It is optional buyer-owned documentation and
needs an independently selected Next.js host when published.

## Local development [#local-development]

The scaffold creates ignored `apps/docs/.env` with a localhost origin so the full zero-key workspace
build remains deterministic.

```sh
pnpm --filter @app/docs dev
```

Open `http://localhost:3002/docs`. Search is served by the Next route handler. Markdown negotiation,
`llms.txt`, `llms-full.txt`, per-page Markdown, sitemap, robots, and Open Graph image routes are
part of the application.

## Offline Markdown bundle [#offline-markdown-bundle]

The documentation navigation includes **Download Markdown**. It downloads the current published
documentation as one Markdown file from `llms-full.txt`; save that file for offline reading or give
it to an agent alongside the exact product release. It contains only the published documentation,
not private environment files, seller-only surfaces, evaluation evidence, or historical Phase 2
material.

<a href="/llms-full.txt" download>Download the current Markdown bundle</a>

## Content and navigation [#content-and-navigation]

For every new published page:

1. add the `.md` or `.mdx` file under `docs/`;
2. add it to `docs/meta.json` at the intended sidebar position;
3. add it to the `files` list in `apps/docs/lib/source.ts`;
4. use links to other published docs pages or external HTTPS pages;
5. render and verify the route, Markdown representation, search, sitemap, and adjacent navigation.

Repository-only files such as ADRs, commercial policies, evaluation evidence, and infrastructure
READMEs should be referenced as code paths unless they are deliberately added to the published
source list. A filesystem-valid relative Markdown link can still be a 404 in the docs application.

## Verification [#verification]

```sh
pnpm --filter @app/docs lint
pnpm --filter @app/docs typecheck
pnpm --filter @app/docs test
NEXT_PUBLIC_SITE_URL=https://docs.example.com \
  pnpm --filter @app/docs build:public
```

The ordinary `build` command accepts the generated localhost origin for repository-wide local and CI
verification. `build:public` rejects missing, HTTP, localhost, credentialed, query, fragment, or
path-bearing values before Next builds.

## Publication [#publication]

Set `NEXT_PUBLIC_SITE_URL` to the exact public HTTPS origin in the deployment environment. It is
inlined into canonical URLs, sitemap entries, robots, and Open Graph metadata at build time. Rebuild
after changing the origin.

Use a Node-compatible Next.js deployment that can run the search route and proxy. Build with
`build:public`, then start the produced application with:

```sh
pnpm --filter @app/docs start
```

No production host automation is included. The selected host must preserve Next route handlers,
static assets, proxy behavior, HTTPS, immutable release identification, logs, rollback, and a smoke
covering `/docs`, `/api/search`, `/sitemap.xml`, `/robots.txt`, `/llms.txt`, and one Open Graph
image.

Do not publish the generated local `.env`, rely on localhost metadata, or assume the product SPA
deployment also serves this application.


# End-to-end testing (/docs/e2e-testing)



# End-to-end testing [#end-to-end-testing]

Playwright verifies the browser-to-API boundary that component and server tests cannot: actual
routes, cookies, redirects, form interaction, browser state, and cross-origin policy. The suite is
run in CI on Chromium after database-backed tests.

The suite is not a substitute for real provider checkout, mailbox, cloud, backup, or production
browser verification. Those remain release smoke tests under [Go live](./go-live.md).

## Run the suite locally [#run-the-suite-locally]

Install Chromium once for the pinned Playwright version:

```sh
pnpm exec playwright install chromium
```

Start Postgres, create a disposable database, and run the root command with that exact database:

```sh
docker compose up -d postgres
createdb app_e2e
DATABASE_URL='postgresql://app:app@localhost:5432/app_e2e?sslmode=disable' \
  pnpm test:e2e
```

The Playwright configuration migrates that database, starts the API on `127.0.0.1:8000`, starts the
web app on `127.0.0.1:5173`, and uses test-only local origins. It disables external email and Slack
delivery. The suite creates test users and data, so never point it at staging, production, or a
shared developer database.

Remove the disposable database when finished:

```sh
dropdb --if-exists --force app_e2e
```

## What the current suite proves [#what-the-current-suite-proves]

`e2e/auth.spec.ts` covers public and protected authentication behavior. `e2e/product-smoke.spec.ts`
covers the authenticated application shell and key browser boundaries. CI installs Chromium and runs
the same `pnpm test:e2e` command with its disposable PostgreSQL service.

Add browser coverage when a change affects:

* login, sign-out, verification, reset, magic link, OAuth, session expiry, cookies, or redirects;
* a new protected page, form, user-visible mutation, loading/empty/error state, or route fallback;
* billing checkout entry points, provider return handling, entitlement presentation, or portal
  entry;
* upload selection/confirmation, PWA update/offline behavior, or an origin-sensitive integration.

Keep provider-side payment completion, external email delivery, DNS, and cloud ingress in their
provider or deployment smoke instead of putting real credentials into Playwright.

## Add a browser test [#add-a-browser-test]

Create `e2e/<feature>.spec.ts`. Start from user-visible behavior, use accessible role/label queries,
and make the result observable. Prefer a short independent flow over a sequence coupled to another
test's data.

```ts
import {expect, test} from '@playwright/test'

test('a signed-in user can create a saved link', async ({page}) => {
  await page.goto('/saved-links')
  await page.getByRole('button', {name: 'Add saved link'}).click()
  await page.getByLabel('URL').fill('https://example.com')
  await page.getByLabel('Label').fill('Example')
  await page.getByRole('button', {name: 'Save link'}).click()

  await expect(page.getByRole('link', {name: 'Example'})).toBeVisible()
})
```

The example assumes its domain, route, labels, and browser flow have already been built. Do not add
test-only product endpoints or bypass Better Auth to make a test convenient. For a durable feature,
first follow the [worked feature recipe](./worked-feature-bookmarks.md) and the
[domain golden path](./golden-paths/add-domain-end-to-end.md).

## Failure evidence [#failure-evidence]

Playwright retains traces, screenshots, and video on failure. The API process output is written to
`.e2e/server.log`. Inspect the first browser assertion, browser console/network evidence, and server
request ID before changing selectors, environment, auth, or application code.

When a browser test fails only in CI, record the exact command, database name, Playwright version,
trace, and redacted server log. Do not paste cookies, passwords, auth links, provider payloads, or
environment files into an issue or support request.

## Keep the suite useful [#keep-the-suite-useful]

* Test the product outcome, not CSS class names, implementation state, or a timing guess.
* Use deterministic fixtures and independently created users for ownership boundaries.
* Assert unauthenticated redirects, invalid input, failures, and success for a changed critical
  flow.
* Keep browser data isolated to the named disposable database.
* Run [Testing](./testing.md) as well: browser E2E complements contracts, real Postgres integration,
  server tests, and the production build.


# Transactional email (/docs/email)



# Transactional email [#transactional-email]

React Email source templates, deterministic rendered HTML, a typed server renderer, and Resend form
one transactional-email path. Authentication can be developed locally without a provider account;
production cannot.

## Modes [#modes]

| Mode          | `RESEND_API_KEY` | Behavior                                                                        |
| ------------- | ---------------- | ------------------------------------------------------------------------------- |
| Local/test    | unset            | no provider call; recipient, template, subject, and props/action URL are logged |
| Provider test | set to test key  | rendered HTML sent through Resend; provider errors fail the calling operation   |
| Production    | required         | startup environment validation fails when absent                                |

The local fallback is intentional for a single trusted developer machine. It is not a fake inbox:
auth URLs appear in structured logs and are sensitive bearer links.

## Source and generated artifacts [#source-and-generated-artifacts]

The editable source lives in `packages/email/src`. The deploy command exports, verifies, and copies
HTML into `apps/server/email-templates`:

```sh
vp run --filter @app/email dev
vp run --filter @app/email deploy
```

Never edit `apps/server/email-templates/*.html` manually. The server requires and preloads all nine
files before accepting traffic:

* `login-email`;
* `verify-email`;
* `reset-password`;
* `welcome-email`;
* `billing-subscription-created`;
* `billing-payment-successful`;
* `billing-payment-failed`;
* `billing-subscription-canceled`;
* `security-alert`.

The source inventory, deploy manifest, typed renderer names/props/subjects, and generated HTML must
remain exactly aligned. Missing templates fail deployment or startup instead of failing only when a
buyer requests a password reset.

## Rendering and delivery [#rendering-and-delivery]

Callers select a typed template and provide its exact props. The server:

1. retrieves the preloaded HTML;
2. applies conditional blocks;
3. HTML-escapes every inserted value;
4. resolves the static or typed subject;
5. logs locally or sends `from`, recipients, subject, HTML, and optional reply-to through Resend.

Set:

```text
RESEND_API_KEY=<API runtime secret>
EMAIL_FROM=Product <noreply@mail.example.com>
```

`EMAIL_FROM` must use a sender/domain verified in the same Resend account as the key. Keep the key
only in the API environment; never expose it through `VITE_*`.

The compiled server still reads external rendered templates. The included Docker image copies them
to its detected layout. Use `EMAIL_TEMPLATES_DIR` only for a proven custom filesystem layout; an
explicit missing directory is a fatal startup error.

## Delivery semantics [#delivery-semantics]

Provider rejection is returned as a stable server error to an awaited calling flow. Better Auth
email calls are awaited, so signup/reset/magic-link failure remains visible to that auth operation.

Billing lifecycle notifications run after the financial transaction commits. A delivery failure is
logged and cannot roll back billing state or force a webhook retry.

The v1 mailer does not include:

* a durable outbox;
* scheduled retries or dead letters;
* delivery/bounce/complaint webhook processing;
* application-owned send receipts;
* provider idempotency keys.

Resend supports 24-hour idempotency keys, but adding a header alone would not fix a process crash
between database commit and send. Add a product-owned outbox/state transition when an email is a
business-critical deliverable rather than claiming exactly-once delivery.

References: [Resend send API](https://resend.com/docs/api-reference/emails/send-email),
[idempotency keys](https://resend.com/docs/dashboard/emails/idempotency-keys), and
[errors](https://resend.com/docs/api-reference/errors).

## Security and privacy [#security-and-privacy]

* Production must never use the log fallback.
* Local logs can contain verification, reset, and magic-link URLs; do not forward them to Sentry,
  analytics, shared chat, or public CI.
* Sentry console breadcrumbs are disabled for this reason.
* Do not log provider keys, complete provider responses, cookies, or unrelated template data.
* Keep email props minimal; transactional email is not an analytics warehouse.
* Product-specific unsubscribe, retention, consent, and marketing rules are buyer responsibilities.
* A rendered link must use the exact trusted application origin and HTTPS in production.

## Verification [#verification]

For every template change:

```sh
vp run --filter @app/email deploy
bun test --cwd apps/server src/infra/email/renderer.test.ts src/infra/mailer/client.test.ts
vp run --filter @app/server build
git diff --check
```

Then send through a verified test domain and inspect subject, sender, links, mobile layout, dark
mode, spam result, and provider error/log behavior. A source preview alone is not production proof.

Consult [Add a transactional email](./golden-paths/add-transactional-email.md) and
[Deploy a fresh application](./golden-paths/deploy-fresh-application.md).

## Failure guide [#failure-guide]

| Failure                                | Inspect                                                               |
| -------------------------------------- | --------------------------------------------------------------------- |
| Local message “missing”                | structured server log and correct local fallback mode                 |
| Resend sender rejected                 | verified domain, `EMAIL_FROM`, key/account/environment                |
| Link points to localhost/wrong host    | `FRONTEND_URL`, `SERVER_URL`, Better Auth trusted origins             |
| Placeholder remains in message         | source/deploy/renderer inventory and the deploy command above         |
| Server exits before ready              | rendered directory, all nine files, `EMAIL_TEMPLATES_DIR`             |
| Billing state correct but email absent | post-commit notification log; do not replay the financial event       |
| Duplicate critical message             | no v1 outbox/idempotency contract; design domain-owned delivery state |


# Build your first feature (/docs/first-feature)



Use a new domain for durable product behavior. A feature is complete only when its data ownership,
validation, server policy, browser states, and verification all agree.

## A maintained path [#a-maintained-path]

<Steps>
  <Step>
    ### Define the behavior [#define-the-behavior]

    Specify the owner, lifecycle, limits, and failure states before choosing files or UI.
  </Step>

  <Step>
    ### Make persistence explicit [#make-persistence-explicit]

    Add a dbmate migration and shared Zod request/response schemas in `packages/contracts`.
  </Step>

  <Step>
    ### Build the vertical slice [#build-the-vertical-slice]

    Create server route, service, and repository layers, then the typed web service and product module.
  </Step>

  <Step>
    ### Prove it works [#prove-it-works]

    Cover authorization, validation, durable behavior, and relevant failure paths before release.
  </Step>
</Steps>

<Cards>
  <Card title="Add a domain end to end" description="A maintained optional implementation playbook." href="/docs/golden-paths/add-domain-end-to-end" />

  <Card title="Worked saved-links feature" description="Follow one bounded product decision from schema to browser proof." href="/docs/worked-feature-bookmarks" />

  <Card title="Add a database migration" description="Evolve schema safely with dbmate." href="/docs/golden-paths/add-database-migration" />

  <Card title="Add a protected route" description="Use Better Auth session identity correctly." href="/docs/golden-paths/add-authenticated-route" />

  <Card title="Customize safely" description="Replace neutral product choices without losing boundaries." href="/docs/customization" />
</Cards>

## Do not skip the boundaries [#do-not-skip-the-boundaries]

Do not place product SQL in routes, use client state as the authorization source, or gate paid
access solely in the browser. These shortcuts are fast only until the first policy, billing, or
data-loss edge case appears.

<Callout title="Golden paths are optional playbooks" type="info">
  Each guide provides a tested route, common change surfaces, ownership rules, acceptance criteria,
  and useful checks. You may use another implementation approach when it preserves the required
  invariants and proves the same product outcome.
</Callout>


# Go live (/docs/go-live)



# Go live [#go-live]

This is the launch sequence for the SaaS application built from the foundation. It is not a claim
that the foundation's own pre-release source distribution is ready to sell. Start only from a clean,
tagged application commit with a named release owner and a rollback decision.

Use [Deployment](./deployment.md) to select and operate infrastructure. This page orders the work
that crosses infrastructure, the product, and the enabled providers.

## Launch outcome [#launch-outcome]

A launch is ready when a real user can reach the intended HTTPS origin, create and recover an
account, receive transactional email, use the product's critical path, pay when billing is enabled,
and receive a safe failure when an optional capability is absent. The release owner can observe the
service, restore data, and roll back the application without guessing.

The foundation ships an authenticated product application, not a marketing site, legal copy, or
product domain. Read [Product scope](./product-scope.md) before treating a foundation capability as
a customer promise.

## 1. Make the product identifiable [#1-make-the-product-identifiable]

Complete these decisions before creating public accounts:

* Generate the application with its final working name and scope, then complete the maintained
  [branding and design-token path](./golden-paths/customize-branding-design-tokens.md).
* Replace the starter dashboard with the product's real first-use outcome. Do not launch a neutral
  screen as though it were a customer feature.
* Publish product-owned terms, privacy, retention, deletion, support, and incident-contact pages,
  then set `VITE_TERMS_URL` and `VITE_PRIVACY_URL` to their public HTTPS URLs.
* Decide the support channel, on-call or incident owner, data-retention policy, and the first
  customer-facing recovery path.

The source product's commercial policies are not the legal or support documents for the SaaS you
build from it.

## 2. Prove the release candidate [#2-prove-the-release-candidate]

Run the deterministic repository verification from the exact release commit:

```sh
vp install --frozen-lockfile
vp check
vp run -r typecheck
vp run -r test
pnpm test:e2e
vp run -r build
pnpm audit --audit-level high
git diff --check
```

Run database-enabled tests against a disposable database when the release changes persistence,
authentication, billing, uploads, or migrations. A passing zero-key test path does not prove a
configured provider.

Use [Testing](./testing.md) for runner boundaries and [End-to-end testing](./e2e-testing.md) for the
browser harness. Build a custom-scope/custom-brand reference application if the change affects the
foundation or its scaffolder.

## 3. Establish the production boundary [#3-establish-the-production-boundary]

Choose one supported backend, one frontend target, one storage provider when uploads are enabled,
and one billing provider when paid access is enabled. Give production and staging separate database,
provider, and secret boundaries.

Before admitting public traffic:

1. Set exact stable HTTPS `FRONTEND_URL`, `SERVER_URL`, browser `VITE_API_URL`, and allowed CORS
   origins. Production previews must not use the production API.
2. Set the matching `TRUSTED_PROXY_PROFILE`; do not add a generic forwarded-header trust setting.
3. Run the migration primitive once, then confirm `/ready` returns `200`. `/health` alone is not a
   database readiness check.
4. Restore a fresh database from a retained backup in a disposable environment. Record the recovery
   time and accepted data-loss window before launch.
5. Record the exact image digest, deployment identifier, database backup location, and prior
   rollback target.

See [Configuration](./configuration.md) for variable ownership and [Deployment](./deployment.md) for
supported topology, DNS, rollout, and rollback behavior.

## 4. Verify identity and communications [#4-verify-identity-and-communications]

Use the exact production origins and a deliverable external mailbox. Verify:

* signup, verification, password reset, magic link, logout, session revocation, and the product's
  protected critical path;
* Google sign-in and callback only when Google is enabled;
* the Resend sending domain, `EMAIL_FROM`, and every enabled transactional template;
* a rejected cross-origin request, expired/replayed token, and a private-resource request from a
  different user;
* that production logs, browser errors, and support evidence contain no secrets, auth links, or
  customer data.

Read [Authentication](./authentication.md), [Transactional email](./email.md), and
[Security](./security.md) before changing cookie, provider, or logging behavior to make a smoke test
pass.

## 5. Verify each enabled provider [#5-verify-each-enabled-provider]

An optional capability is either fully configured and proven or visibly unavailable. Do not launch
with a partially configured selected provider.

| Capability | Required production proof                                                                                                                                                                                      |
| ---------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Billing    | Live product/price mapping, hosted checkout, verified webhook, subscription state, entitlement, portal, failed-payment recovery, cancellation boundary, invalid-signature rejection, and reconciliation alert. |
| Storage    | Exact CORS, owner-scoped upload, invalid MIME/size rejection, expiry, replay rejection, object confirmation, deletion, and selected-provider lifecycle cleanup.                                                |
| Sentry     | A redacted production-like error reaches the selected project through the tunnel without email, cookies, auth links, or console content.                                                                       |
| Slack      | Disabled state is harmless, or a sanitized alert reaches the intended operational channel without becoming the system of record.                                                                               |

Use the provider-specific verification in [Billing](./billing.md) and
[Object storage](./storage.md). For billing, prove the live flow again after the test/sandbox run; a
checkout return page never grants access by itself.

## 6. Release, observe, and retain evidence [#6-release-observe-and-retain-evidence]

Deploy the immutable application artifact. After traffic is admitted, verify the exact public
origins, an authenticated browser session, the buyer-critical product outcome, PWA update/offline
behavior, and every enabled provider flow.

For the first launch window, monitor readiness, structured errors, provider event failures,
reconciliation output, database capacity, and deployment alerts. Preserve only redacted evidence:

* release tag/commit, artifact digest, and deployment identifier;
* verification command output and browser smoke result;
* migration result, backup/restore result, and rollback target;
* provider event IDs and reconciliation report without payloads or credentials;
* the owner and escalation path for security or service incidents.

Follow [Troubleshooting](./troubleshooting.md) from the first observable failure. A failed
migration, ownership check, provider signature, or recovery test stops the launch; do not hide it
behind a client-side success state.

## Stop conditions [#stop-conditions]

Do not release when any of these are unresolved:

* a customer-facing policy, support contact, or product outcome is still placeholder content;
* migrations, backup restore, health/readiness, or rollback are unproven;
* browser origins, cookie behavior, or trusted proxy profile differ from the deployed path;
* an enabled provider lacks its complete credentials, live configuration, or failure-path proof;
* a test, audit, or browser smoke was skipped without an explicit owner and release decision;
* a release depends on an unrecorded manual production database change or secret value.

Record the deferred item and its owner before choosing to release without a non-critical proof. Do
not relabel a deferred check as passed.


# Introduction (/docs)



SaaS Boilerplate is a source-owned TypeScript foundation for building and operating a
production-ready SaaS. It gives you a real application architecture—not a collection of disconnected
snippets—so you can focus on your product domain.

## Start here [#start-here]

<Cards>
  <Card icon="<BoxesIcon />" title="Tech stack" description="See what powers the browser, API, data, and operations." href="/docs/tech-stack" />

  <Card icon="<CompassIcon />" title="Set up locally" description="Run the complete stack with no third-party keys." href="/docs/setup" />

  <Card title="Configure safely" description="Assign environment and provider settings to the right owner." href="/docs/configuration" />

  <Card icon="<RocketIcon />" title="Build your first feature" description="Follow the contract-to-UI path for a new domain." href="/docs/first-feature" />

  <Card icon="<ShieldCheckIcon />" title="Go to production" description="Prepare deployment, providers, and verification." href="/docs/go-live" />
</Cards>

## How the parts connect [#how-the-parts-connect]

<ArchitectureMap />

## What you get [#what-you-get]

* A React web app, Bun + Hono API, PostgreSQL, and shared Zod contracts.
* Better Auth with password, magic-link, and optional Google sign-in.
* Billing adapters for Polar, Stripe, and Dodo Payments.
* Transactional email, R2/S3/GCS uploads, notifications, error monitoring, and PWA support.
* Source-owned migrations, test coverage, operational runbooks, and optional agent skills.

The boilerplate starts usable with only Postgres. External providers are optional in local
development and become available when configured completely.

See [Included product surfaces](./product-surfaces.md) for every shipped browser route and
[Notifications and marketing endpoints](./notifications-marketing.md) for the communication paths.

<Callout title="A product foundation, not a complete business" type="info">
  The generated product includes the authenticated application and its operational boundaries. You
  supply the product domain, marketing and legal surfaces, commercial policy, analytics choice, and
  any organization or background-work model. See [Product scope](./product-scope.md).
</Callout>

<Callout title="Designed for a clean first run" type="success">
  Password authentication, verification, password reset, and magic-link sign-in work locally without
  third-party API keys. Provider-backed capabilities become available when their complete
  configuration is present.
</Callout>

## How to use these docs [#how-to-use-these-docs]

Read the setup guide first, then the tech stack, project structure, and included product surfaces.
The feature and web UI guides explain what is already built; optional golden paths and agent skills
provide maintained ways to extend it. The architecture and verification requirements apply
regardless of the workflow you choose.

## Common questions [#common-questions]

<Accordions>
  <Accordion title="Can I use a different billing provider?" id="billing-providers">
    Yes. Polar, Stripe, and Dodo Payments are supported behind one normalized billing contract.
    Select exactly one provider per deployment and configure it completely.
  </Accordion>

  <Accordion title="Is this a Next.js product app?" id="product-framework">
    No. The product is a React 19 SPA built with Vite+. Next.js powers only this separate
    documentation site.
  </Accordion>

  <Accordion title="Where should I add product behavior?" id="product-behavior">
    Create a dedicated domain with contracts, routes, service policy, repository, UI module, and
    tests. The first-feature guide walks through that shape.
  </Accordion>

  <Accordion title="Where is the launch checklist?" id="go-live">
    Use Go live to order product, provider, deployment, backup, and browser proof. Deployment
    remains the detailed infrastructure guide.
  </Accordion>
</Accordions>


# Notifications and marketing endpoints (/docs/notifications-marketing)



# Notifications and marketing endpoints [#notifications-and-marketing-endpoints]

The repository contains two distinct communication paths. Authenticated product notifications are
stored per user and respect preferences. Public marketing submissions are rate-limited boundary
endpoints intended to be called by a buyer-owned marketing site.

## Product notifications [#product-notifications]

The supported notification types are:

| Type                  | Default email | Default in-app | Included producer behavior                               |
| --------------------- | ------------- | -------------- | -------------------------------------------------------- |
| `billing_update`      | enabled       | enabled        | billing owns its lifecycle email and emits in-app state  |
| `security_alert`      | enabled       | enabled        | unrecognized-device sign-in and password reset           |
| `system_announcement` | enabled       | enabled        | no included producer; a product emits these deliberately |

Producers live with the domain that owns the event. Billing lifecycle notifications are emitted from
`apps/server/src/domains/billing/service.ts`. Security alerts are emitted from the Better Auth hooks
in `apps/server/src/lib/auth.ts`: a sign-in whose user agent has never been seen for that user, and
a completed password reset. The first session a user ever creates never alerts, so signup is silent.
Device recognition is user-agent based, not a device-identity system; treat it as a heuristic.

There is no email-change alert because the account contract has no email-change flow;
`UpdateProfileRequestSchema` covers name, profile image, and phone number only. Adding one means
adding the flow first.

There is no push-delivery implementation. Do not expose a push preference or claim push support
without adding a provider, permission UX, subscription ownership, revocation, retry, privacy, and
browser proof.

All notification routes require `sessionAuth` and scope reads and writes to the current `users.id`:

| Method   | Route                             | Behavior                                |
| -------- | --------------------------------- | --------------------------------------- |
| `GET`    | `/notifications`                  | paginated list with type/unread filters |
| `GET`    | `/notifications/unread-count`     | current user's unread count             |
| `PATCH`  | `/notifications`                  | mark up to 100 IDs or all rows as read  |
| `GET`    | `/notifications/preferences`      | effective email and in-app preferences  |
| `PUT`    | `/notifications/preferences`      | update deduplicated typed preferences   |
| `GET`    | `/notifications/:notification_id` | owner-scoped detail                     |
| `DELETE` | `/notifications/:notification_id` | owner-scoped deletion                   |

The browser polls unread count, invalidates only notification query keys after mutations, and sends
the HttpOnly Better Auth cookie through the shared Hono client. The service must never accept a user
ID from notification request input.

Notification dispatch is best effort after the owning transaction commits. It can be lost when the
process exits and is not a durable queue or outbox. If delivery is required for a product invariant,
follow [Background work](./background-work.md) and design persisted delivery state.

## Waitlist endpoint [#waitlist-endpoint]

`POST /marketing/waitlist` validates `JoinWaitlistRequestSchema`, stores one row per normalized
email, and returns the same non-disclosing success message for an existing address. The optional
fields are `referral_source`, `source_page`, and `campaign`.

The database row is durable. A newly inserted lead also triggers a best-effort Slack alert. Slack
failure does not roll back the lead and duplicate submissions do not send another alert.

## Contact endpoint [#contact-endpoint]

`POST /marketing/contact` validates `ContactFormRequestSchema` and sends a best-effort Slack alert.
The repository does not persist contact messages and there is no email, retry, inbox, or support
SLA. Do not connect a production form until Slack is configured and this loss policy matches the
product. Add durable storage or a provider-owned ticket ID when a submission must be recoverable.

Both public endpoints share an atomic Postgres limit of five requests per minute for the resolved
client IP. The budget is shared across API replicas and depends on the selected trusted proxy
profile producing the correct client IP.

## Slack behavior [#slack-behavior]

`SLACK_BOT_TOKEN` is optional. When absent, signup, waitlist, and contact alerts are disabled and
the server continues. When present, the bot posts sanitized text to `#alerts-users`. The channel is
a current application constant, not a configurable routing matrix.

Never treat Slack as the source of truth for a durable lead, a contact-support entitlement, or an
incident system. Never include secrets, auth links, cookies, payment data, or unrestricted customer
payloads in alert messages.

## Verification [#verification]

```sh
bun test --cwd apps/server src/domains/marketing
bun test --cwd apps/server src/domains/notifications
RUN_DB_INTEGRATION_TESTS=1 \
  DATABASE_URL=postgresql://app:app@localhost:5432/app?sslmode=disable \
  bun test --cwd apps/server
pnpm test:e2e
```

Before launch, prove duplicate waitlist behavior, invalid and oversized input, limit exhaustion,
Slack disabled/failure behavior, notification preferences, owner isolation, mark-read limits, and
the exact retention policy for leads, contact content, and notifications.


# Product scope (/docs/product-scope)



# Product scope [#product-scope]

This foundation is a source-owned authenticated SaaS application foundation. It is designed to make
the first durable product capability safe to build and deploy; it is not a finished business,
industry-specific application, or an all-features starter kit.

Use this page before estimating product work or making a buyer-facing claim. A visible source file
is not necessarily a shipped buyer feature, and an optional integration is not enabled merely
because its package exists.

## Included foundation capabilities [#included-foundation-capabilities]

| Area                      | What ships                                                                                                                                                    |
| ------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Product application       | React SPA, protected routes, settings, notification inbox, neutral dashboard, responsive UI primitives, PWA, and a typed API client.                          |
| Data and API              | PostgreSQL, dbmate migrations, Hono routes/services/repositories, shared Zod contracts, owner-scoped data patterns, and test scaffolding.                     |
| Authentication            | Better Auth password, verification, reset, magic link, sessions, and optional Google sign-in.                                                                 |
| Billing                   | One selected deployment provider: Polar, Stripe, or Dodo Payments; plans, hosted checkout, portal, entitlement policy, verified webhooks, and reconciliation. |
| Communication             | React Email templates, Resend delivery, development email-log fallback, in-app preferences, and optional Slack operational alerts.                            |
| Storage and observability | Optional R2/S3/GCS owner-scoped upload path, structured logs, optional Sentry, and deployment readiness.                                                      |
| Operations                | Railway, Dokploy, and OpenTofu-managed cloud paths; release, upgrade, troubleshooting, removal, and buyer scaffolding contracts.                              |

Read [Included product surfaces](./product-surfaces.md) for routes and zero-key behavior, rather
than assuming that a provider configuration creates a customer outcome.

## Every buyer supplies [#every-buyer-supplies]

The generated repository intentionally leaves these decisions to the product owner:

* the actual product domain, dashboard outcome, copy, pricing decision, and customer journey;
* commercial name, logo, accessible visual identity, sender identity, and public domain;
* marketing site, blog, SEO program, analytics choice, and acquisition funnel;
* terms, privacy, retention, deletion, tax, compliance, support, and incident policies;
* external provider accounts, production credentials, DNS, monitoring ownership, backups, and
  operational response;
* any authorization model beyond a single user owning personal resources.

Marketing-site code is not generated for a buyer application. Public waitlist and contact endpoints
are available for a buyer-owned marketing site; they are not a shipped marketing website.

## Deliberately not included in version one [#deliberately-not-included-in-version-one]

| Capability                                                                                                            | Why it is absent                                                                                                                                                  |
| --------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Organizations, roles, SSO, SCIM, or admin impersonation                                                               | Multi-tenant authorization needs a product-specific ownership and disclosure model.                                                                               |
| Generic queues, cron jobs, workers, or durable AI processing                                                          | No generic executor is safe without a validated job, authorization handoff, retry/recovery, cost, and support contract.                                           |
| Built-in blog, SEO suite, marketing pages, legal pages, or sales analytics                                            | These are business and brand decisions outside the buyer product application.                                                                                     |
| Product analytics or a feature-flag platform                                                                          | Instrumentation, consent, retention, and event taxonomy must be product-owned.                                                                                    |
| Malware scanning, audit-log UI, compliance export, automatic retention/erasure, or application-level field encryption | These require a specific risk model, operational owner, and evidence; they are not checkbox features.                                                             |
| Generic coupon, tax display, usage, seat, refund, or provider-switching UI                                            | The included billing contract is intentionally limited to one deployment provider and recurring monthly/yearly plans.                                             |
| A public third-party API or generated OpenAPI contract                                                                | The typed Hono RPC boundary is for the shipped browser application. External integrations need their own authentication, versioning, abuse, and support contract. |

Absence is not a prohibition forever. It means the buyer must make a separate product decision and
prove the new boundary before claiming support.

## Choose the next path [#choose-the-next-path]

* Add a product-owned resource with the [worked feature recipe](./worked-feature-bookmarks.md) and
  [domain golden path](./golden-paths/add-domain-end-to-end.md).
* Replace neutral identity and UX with [Customization](./customization.md).
* Add a provider-backed capability only through its billing, email, storage, or authentication
  contract.
* Remove an optional capability completely with [Removing subsystems](./removing-subsystems.md).
* Evaluate a queue, cron, or long-running process with [Background work](./background-work.md)
  before adding infrastructure.


# Included product surfaces (/docs/product-surfaces)



# Included product surfaces [#included-product-surfaces]

The browser application is a neutral authenticated SaaS shell. It includes complete auth and
settings paths plus a starter dashboard that must be replaced with the buyer's product outcome.
React Router owns navigation, TanStack Query owns server state, and Better Auth owns the session.

## Browser routes [#browser-routes]

| Route                     | Access        | Included behavior                                                   |
| ------------------------- | ------------- | ------------------------------------------------------------------- |
| `/auth/login`             | public        | magic link, password sign-in, optional Google                       |
| `/auth/signup`            | public        | password signup and verification                                    |
| `/auth/forgot-password`   | public        | password-reset request                                              |
| `/auth/reset-password`    | public token  | password replacement and session revocation                         |
| `/auth/verify`            | public token  | email verification callback                                         |
| `/dashboard`              | authenticated | neutral starter state; replace it with the product home             |
| `/users`                  | authenticated | paginated, filterable user table with a mobile card list            |
| `/users/:userId`          | authenticated | one user's profile, account state, and linked sign-in providers     |
| `/settings/profile`       | authenticated | name, phone number, and optional public profile image               |
| `/settings/account`       | authenticated | linked provider state, Google linking when configured, and deletion |
| `/settings/billing`       | authenticated | trial, plans, checkout, portal, and provider-disabled states        |
| `/settings/notifications` | authenticated | email and in-app preferences                                        |
| `/notifications`          | authenticated | inbox, unread state, mark-read, and delete                          |

Every protected route passes through `PrivateRoute`. A missing session redirects to login; a session
resolution failure remains visible instead of rendering protected content from stale client state.

## Profile and account ownership [#profile-and-account-ownership]

`GET /users/me` returns the current user's public application profile. `PUT /users/profile` accepts
the shared `UpdateProfileRequestSchema`; the caller cannot choose another user ID. The profile-image
flow is separately entitlement-gated and provider-backed as described in
[Object storage](./storage.md).

`GET /users` lists non-deleted users for the table, and `GET /users/:user_id` returns one user's
detail record for `/users/:userId`. Both require a session, return no credential material, and never
expose a deleted account. Add an authorization rule before exposing either surface to a role that
must not see the whole directory.

Email changes are not included. Add them only through a Better Auth-compatible flow with new-address
verification, existing-session policy, notification, and recovery behavior. Do not mutate the email
column through the generic profile update.

Account deletion uses `DELETE /users/profile/account`. It is blocked while a provider subscription
can renew or paid access remains. See [Authentication](./authentication.md) for the terminal
deletion state and billing boundary.

## Zero-key states [#zero-key-states]

With only Postgres configured:

* password, verification, reset, and magic-link flows work through the local email log;
* Google controls are hidden;
* billing renders an actionable unavailable state without a checkout button;
* profile text fields work, while profile-image upload returns the documented storage-unavailable
  response;
* notifications work in-app when application events create them;
* the dashboard clearly identifies itself as starter content.

An unavailable optional provider is not an empty success. Keep the current disabled state visible or
remove the complete subsystem using [Removing subsystems](./removing-subsystems.md).

## Replacing the dashboard [#replacing-the-dashboard]

Replace `apps/web/src/modules/dashboard/pages/DashboardPage.tsx` with the first product-owned route.
Keep the route lazy, use shared contracts and a service-owned TanStack Query hook, and cover
loading, empty, error, unauthorized, narrow, keyboard, and reduced-motion states in proportion to
the feature. The maintained implementation path is [Build your first feature](./first-feature.mdx).

## Verification [#verification]

```sh
pnpm test:e2e
vp run --filter @app/web test
vp run --filter @app/web typecheck
vp run --filter @app/web build
```

The browser suite verifies every protected route, zero-key auth, the main settings surfaces, 404
handling, protected API behavior, and cross-origin rejection. Provider configuration still requires
the provider-specific smoke paths.


# Project structure (/docs/project-structure)



```text
apps/
  web/          React product application
  server/       Bun + Hono API, migrations, and domains
  docs/         Fumadocs + Next.js documentation site
packages/
  contracts/    Shared Zod schemas and inferred types
  billing/      Provider-neutral billing contract and adapters
  email/        React Email source templates
  create-app/   Buyer scaffold and branding workflow
  agent-eval/   Outcome evaluation and evidence harness
  agent-context/ Local read-only documentation MCP server
  tsconfig/     Shared TypeScript policy
agent-tooling/
  skills/       Optional general coding-agent workflows
docs/           Canonical product and operating documentation
infra/          OpenTofu bootstrap, modules, roots, locks, and tests
e2e/            Playwright browser and API boundary coverage
scripts/        Deployment, release, and repository verification
.github/        CI, cloud release, and source release workflows
```

<Files>
  <Folder name="apps">
    <Folder name="web">
      <File name="src/modules/" />

      <File name="src/services/" />
    </Folder>

    <Folder name="server">
      <Folder name="src/domains">
        <File name="routes.ts" />

        <File name="service.ts" />

        <File name="repository.ts" />
      </Folder>

      <File name="migrations/" />
    </Folder>

    <Folder name="docs">
      <File name="app/docs/" />
    </Folder>
  </Folder>

  <Folder name="packages">
    <File name="contracts/" />

    <File name="billing/" />

    <File name="email/" />

    <File name="agent-context/" />
  </Folder>

  <Folder name="docs">
    <File name="golden-paths/" />
  </Folder>

  <Folder name="agent-tooling">
    <File name="manifest.json" />

    <File name="skills/" />
  </Folder>

  <Folder name="infra">
    <File name="bootstrap/" />

    <File name="modules/" />

    <File name="roots/" />

    <File name="tests/" />
  </Folder>

  <Folder name="e2e" />

  <Folder name="scripts" />

  <Folder name=".github" />
</Files>

The studio source checkout may also contain seller-only `landing/` and `operations/` directories.
They are excluded from generated buyer repositories and are not application runtime dependencies.

## Domain ownership [#domain-ownership]

Each server domain follows the same route → service → repository path. Routes map HTTP and session
state, services own policy and transactions, and repositories own parameterized SQL. The web app
consumes the shared contracts rather than recreating API types by hand.

Use this structure as a map, not a restriction on product language. Add product-specific capability
inside its own domain/module instead of folding it into generic files.

<Callout title="Keep the flow directional" type="idea">
  Contracts describe the wire format. Routes map HTTP. Services own policy. Repositories own SQL.
  That separation makes ownership and failure behavior discoverable for people and coding agents.
</Callout>

Read [Architecture](./architecture.md) for boundaries and production topology, or jump to
[Build your first feature](./first-feature.mdx) for the extension workflow.


# Releases (/docs/releases)



# Release and delivery contract [#release-and-delivery-contract]

Product releases are immutable annotated Git tags and deterministic source artifacts. A mutable
branch, local checkout, package version, deployment, or generated buyer repository is not a product
release.

## Versioning [#versioning]

The first approved commercial baseline is `v1.0.0`. Before it exists, the product remains
unreleased. After `v1.0.0`:

* patch releases preserve the supported product contract and contain compatible fixes;
* minor releases add backward-compatible capability or supported-path improvements;
* major releases require explicit buyer action to preserve an existing supported contract.

Release candidates use `vMAJOR.MINOR.PATCH-rc.N`. Internal `v0.0.0-rehearsal.N` tags may exist only
in isolated rehearsal repositories; they are never customer releases, entitlement events, or public
compatibility claims.

## Required release inputs [#required-release-inputs]

Every commercial tag requires:

* a clean commit that has passed main CI;
* an annotated tag pointing at that exact commit;
* a matching `CHANGELOG.md` heading;
* `docs/releases/<version>.md` completed from the release-note template;
* pinned Node, Bun, pnpm, Vite+, TypeScript, Postgres, and provider assumptions;
* migration forward/backward/irreversible notes;
* environment additions/removals/rotations;
* known issues and support transition window;
* root, real-Postgres, custom-buyer, package-native toolchain, audit, packaging, and
  previous-release upgrade proof.

The first commercial tag has no previous supported release. It still requires a full fresh-buyer
proof. Every later tag requires `release:rehearse-upgrade` from the previous supported tag.

## Verification commands [#verification-commands]

Run only from the clean commit carrying the annotated target tag and against a disposable database:

```sh
RELEASE_DISPOSABLE_DATABASE=1 \
DATABASE_URL='postgresql://app:app@localhost:5432/release_verify?sslmode=disable' \
pnpm release:verify -- v1.0.0-rc.1 v1.0.0-rc.0
```

Omit the second tag only for the first commercial baseline. The verifier refuses a dirty checkout, a
lightweight/mismatched tag, missing notes, or a database not explicitly declared disposable.

The verifier:

1. freezes dependencies and proves migration `up → down → up`;
2. runs DB-enabled tests, checks, supported builds, package-native escape commands, and audit;
3. creates and fully verifies a custom-scope/custom-brand buyer;
4. packages the tagged source through Git attributes;
5. rehearses an upgrade from the previous tag when supplied.

## Artifact contract [#artifact-contract]

`release:package` produces:

* `foundation-source-<tag>.tar.gz`;
* `foundation-source-<tag>.tar.gz.sha256`;
* `foundation-source-<tag>.manifest.json`.

The tarball is `git archive` output compressed with timestamp-free gzip. Re-running the packager for
the same tag produces the same bytes. The manifest records tag, commit, commit timestamp, artifact
checksum, and toolchain pins. A checksum detects accidental change; it is not a cryptographic
signature or proof of publisher identity.

The archive excludes:

* the seller landing site and private planning;
* historical Phase 2 instructions and internal dated author evidence;
* Git history, dependencies, build output, runtime environment, evaluation runs, and release output.

It includes the buyer template, root/scoped agent rules, CI, buyer/commercial contracts, golden
paths, create-app, and release/upgrade documentation.

## Upgrade rehearsal [#upgrade-rehearsal]

The deterministic rehearsal:

* extracts both exact tags;
* generates old/current buyers with the same name, scope, and brand;
* refuses any modified or removed previous migration;
* constructs a shared Git baseline, adds a buyer-owned product file, and merges the target
  foundation branch;
* proves the buyer-owned file survives;
* runs migration reversal, DB-enabled tests, frozen install, checks, all tests/builds, and audit.

This proves an unmodified reference plus a non-overlapping buyer change. It does not promise that
arbitrary application customizations merge without conflict. Buyers must review conflicts using
[Upgrading](./upgrading.md).

## Entitlement and access [#entitlement-and-access]

CI artifacts are maintainer evidence, not customer fulfillment. After approval, the fulfillment
system must copy the exact immutable artifact, checksum, manifest, notes, and license into the
access-controlled release channel.

For each release grant, record:

* order/license and authorized developer;
* tag, commit, artifact checksum, and release publication timestamp;
* entitlement type and `updates_end_at`;
* grant/revocation/recovery events.

A buyer receives a release only when its publication timestamp falls within the recorded update
entitlement, except an approved founding lifetime entitlement. Expiry never disables or revokes
source already lawfully downloaded. Revoking managed access cannot erase a clone.

Never grant customer access to a mutable seller branch as a substitute for versioned delivery.

## npm bootstrapper [#npm-bootstrapper]

The public `superslate` npm package is a source-delivery bootstrapper, not a copy of the paid
foundation. Its stable package version must match an approved immutable source tag exactly:

```text
superslate@1.2.3 → v1.2.3 → foundation-source-v1.2.3.tar.gz
```

Publish the npm version only after the matching private GitHub Release contains the source archive,
SHA-256 file, and manifest produced by this contract. The CLI downloads all three through the
buyer's existing authenticated GitHub CLI session and refuses malformed, mismatched, or modified
artifacts. It must never download `main`, `HEAD`, another mutable branch, or an unverified archive.

The npm package remains `private: true` and version `0.0.0` in the pre-release checkout. Removing
that guard, selecting the commercial version, creating the tag and release assets, and publishing
with provenance are one reviewed release operation—not routine development edits.

## Security and support [#security-and-support]

Security fixes use the same immutable release path, plus coordinated disclosure when needed. The
current tag is supported. When an update requires buyer action, the prior tag receives the
commercially defined transition window unless continued use is unsafe.

Do not publish exploit details, buyer data, secrets, private repository URLs, or provider
credentials in release notes, manifests, CI logs, or artifacts.


# Removing subsystems (/docs/removing-subsystems)



# Removing optional subsystems [#removing-optional-subsystems]

Use the executable
[optional subsystem removal golden path](./golden-paths/remove-optional-subsystem.md). This page
classifies the current product surface so an agent starts from a real inventory rather than a
feature name.

Disabling a key is not removal. A complete removal covers runtime, package, environment,
persistence, public routes, deployment, generated buyers, documentation, and commercial claims.

## Current classification [#current-classification]

| Capability                    | v1 status                       | Removal boundary                                                                                                               |
| ----------------------------- | ------------------------------- | ------------------------------------------------------------------------------------------------------------------------------ |
| Historical Cloudflare worker  | removed and reproduced          | no `apps/worker`, workspace/package/importer/env/deployment/runtime claim; Decision 0001 records re-entry criteria             |
| Sentry                        | optional, not removal-rehearsed | browser init/error boundaries, Vite plugin/source maps, API tunnel, typed environment, project allowlist, packages             |
| Slack operational messages    | optional, not removal-rehearsed | typed environment, Slack client, auth/marketing/deploy callers, server package, docs/tests                                     |
| Google OAuth                  | optional, not removal-rehearsed | Better Auth provider config, typed environment, login state/UI, callback deployment docs/tests                                 |
| Public profile uploads        | optional, not removal-rehearsed | upload contracts/domain/migration, storage adapters, web service/profile UI, environment, docs/tests                           |
| PWA/offline install           | optional, not removal-rehearsed | Vite PWA/Workbox packages and plugin, manifest/icons/offline page, service-worker tests/deployment behavior                    |
| Billing                       | product-optional, high risk     | billing package/domain/migration, adapters, plans/gates, settings UI, webhooks, email, provider environment, commercial claims |
| Better Auth and email auth    | core supported path             | removing either replaces the authentication/product contract; it is not a small optional-integration task                      |
| Postgres/dbmate and contracts | core architecture               | removal is a new product architecture, not a supported subsystem-removal path                                                  |

“Not removal-rehearsed” is deliberate wording. The generic runbook describes how to prove those
removals; it does not claim every combination has already passed.

## Reproduced worker removal [#reproduced-worker-removal]

The pre-release template previously contained a disconnected Cloudflare/Gemini document-processing
worker. It was removed because it had no production Better Auth-to-executor authorization, durable
job ownership, product data model, billing/abuse limits, or paid evidence.

The reproduced removal covered:

* the entire `apps/worker` workspace and its direct packages;
* workspace and root commands;
* worker environment examples and deployment configuration;
* generated-buyer output and current product claims;
* a decision fixture that rejects re-adding provider/runtime code when a PRD has not earned the
  capability;
* frozen install, static checks, tests, production builds, dependency audit, and a custom generated
  buyer.

Current contract tests assert that `apps/worker` is absent and that the no-worker decision remains
documented. This is same-context maintainer evidence, not a cold independent-agent claim.

## Inventory starting points [#inventory-starting-points]

Use these terms only to build an inventory; classify every result before deleting:

| Capability     | Search terms                                                                                                   |
| -------------- | -------------------------------------------------------------------------------------------------------------- |
| Sentry         | `SENTRY_`, `sentry`, `/tunnel`                                                                                 |
| Slack          | `SLACK_`, `slack`, `sendSlackMessage`                                                                          |
| Google         | `GOOGLE_CLIENT`, `google`, `socialProviders`                                                                   |
| Storage/upload | `STORAGE_`, `CLOUDFLARE_`, `PROFILE_PIC`, `upload`, `presign`, `object_key`                                    |
| PWA            | `VitePWA`, `workbox`, `site.webmanifest`, `offline`, `serviceWorker`                                           |
| Billing        | `BILLING_PROVIDER`, `POLAR_`, `STRIPE_`, `DODO_PAYMENTS_`, `subscription`, `billing`, `plan`, `webhook_events` |

Search source, manifests, lockfile importers, migrations, environment schemas/examples, CI,
deployment settings, tests, docs, generated output, and sales copy. Historical ADRs may retain
classified evidence; current buyer claims may not.

## Persistence and pre-release changes [#persistence-and-pre-release-changes]

This product is not live. Before the first commercial tag, a baseline migration may be corrected
only under the documented pre-release exception and only with empty-database `up → down → up`,
DB-enabled tests, and a clean generated buyer.

After the first tag, never edit a released migration to make a removal look clean. Add an explicit
retirement migration, data export/retention decision, and safe rollback or forward-recovery plan.
Deleting an external provider resource or stored customer data always requires explicit authority,
regardless of whether the application is pre-release.

## Product-contract consequences [#product-contract-consequences]

* Removing Sentry or Slack narrows observability/operations but need not change customer behavior.
* Removing Google preserves password/magic-link auth only when those flows and account linking still
  pass.
* Removing object storage requires removing profile upload UI and its persisted claims; do not leave
  a broken avatar action.
* Removing PWA requires removing install/offline claims and generated service-worker artifacts.
* Removing billing means the resulting application is not revenue-ready through the supported paid
  path. Remove gates, plans, portal, lifecycle email, webhook routes/state, and every pricing claim
  together.
* Removing Better Auth, Postgres, the separate API, or typed contracts is a deliberate architecture
  fork outside normal support.

## Required proof [#required-proof]

```sh
vp install --frozen-lockfile
vp check
vp run -r test
vp run -r build
pnpm audit --audit-level high
git diff --check
```

Also prove zero-key local startup, affected negative behavior, exact residue classification,
migration reversal where persistence changed, and an untouched custom-scope/custom-brand generated
buyer. Record remaining hits and their owners; an empty unscoped search is not trustworthy evidence.


# Security (/docs/security)



# Security contract [#security-contract]

This document states the implemented security boundary and the work a buyer must complete before
accepting real users. It is not a certification, penetration-test report, privacy policy, DPA, or
claim of compliance.

## Release security bar [#release-security-bar]

A production release must satisfy all of the following:

* frozen install, static checks, all workspace tests, all builds, and dependency audit pass;
* real Postgres authentication, ownership, billing, migration, and upload negative paths pass;
* environment validation rejects missing production email and partial selected billing/storage
  provider configuration;
* secrets exist only in the owning server/provider environment;
* every API target runs migrations as a one-shot release primitive, gates traffic on `/ready`, and
  uses the matching trusted proxy profile;
* Vercel has only explicitly public `VITE_*` values and does not cache authenticated API responses;
* backup, restore, rollback, auth email, billing webhook, upload, Sentry, and account-deletion paths
  are rehearsed for every enabled integration;
* product-specific terms, privacy, retention, deletion, support, and incident contacts are set.

## Implemented controls [#implemented-controls]

### HTTP boundary [#http-boundary]

The Hono application applies:

* request IDs and structured request logging;
* an explicit credentialed CORS allowlist;
* security headers;
* a 30-second request timeout;
* a 10 MiB request-body cap;
* stable production error responses without raw stack/database/provider details;
* separate liveness and database readiness endpoints.

Do not expose development error detail in production or route public traffic around the supported
proxy.

### Authentication and authorization [#authentication-and-authorization]

Better Auth owns password hashing, verification, reset, magic links, Google OAuth, cookies,
sessions, and database-backed auth rate limiting. Magic-link tokens are hashed and short-lived;
password reset revokes existing sessions; implicit account linking is disabled.

Private product data is authorized through owner-scoped SQL using the Better Auth session identity.
The generic disclosing “resource exists but access denied” helper is intentionally absent. Use the
same non-disclosing 404 for absent and wrong-owner private resources where existence is sensitive.

See [Authentication](./authentication.md).

### Billing [#billing]

Billing webhook signatures are verified in the selected provider adapter before payload
normalization. Provider event claims and local subscription mutations share a database transaction.
Duplicate events do not reapply effects, failed mutations release the claim for retry, and older
provider timestamps cannot overwrite newer lifecycle state.

Premium access is derived on the server; a checkout success URL never grants it. Account deletion is
blocked until recurring billing is resolved and paid access has ended.

See [Diagnose a failed webhook](./golden-paths/diagnose-failed-webhook.md).

### Email and logs [#email-and-logs]

Production requires Resend. The zero-key fallback intentionally logs complete auth URLs only in
development/test. Those logs are sensitive and must not be published, retained as analytics, or
forwarded as console breadcrumbs.

Structured server logs must not include credentials, cookies, raw webhook secrets, provider access
tokens, database URLs, or full provider responses.

### Object storage [#object-storage]

Selected storage-provider configuration is all-or-none. Uploads require an authenticated owner,
explicit upload purpose, MIME allowlist, size limit, owner-shaped key, pending database claim, and
short-lived presigned `PUT`. Confirmation verifies actual object metadata and atomically consumes
the claim.

The current metadata check does not inspect file magic bytes, run malware scanning, or process
untrusted documents. Add those controls before enabling higher-risk upload types.

### Browser and PWA [#browser-and-pwa]

The browser carries the Better Auth HttpOnly cookie with `credentials: 'include'`. It does not put
an auth token in local storage. The PWA has no runtime API cache because Cache Storage keys do not
vary by session cookie; authenticated responses must never be shared across users on one browser.

Only public values may use `VITE_*`. A value in a Vite build is not a secret.

### Sentry [#sentry]

Sentry is disabled when its browser DSN is absent. When enabled:

* default PII collection is disabled;
* console breadcrumbs are disabled;
* browser envelopes use the API tunnel;
* the tunnel requires an allowed browser origin;
* the DSN hostname must be exactly `sentry.io` or a subdomain boundary of `sentry.io`;
* the numeric project ID must appear in typed `SENTRY_PROJECT_IDS` configuration.

Production source maps are generated only when an authenticated Sentry upload is configured, and the
build deletes them from `dist` after upload. A production build without Sentry credentials emits no
source maps.

The tunnel is not an open proxy. Keep its allowlist limited to the deployed Sentry project and test
redaction with production-like errors before launch.

### Secrets and environment [#secrets-and-environment]

`apps/server/src/config/env.ts` is the only server environment reader. It validates:

* `BETTER_AUTH_SECRET` at 32 or more characters;
* complete shared and provider-specific storage configuration when storage is enabled;
* selected-provider credentials plus at least one unique product/price ID when billing is enabled;
* production `RESEND_API_KEY`;
* one supported trusted proxy profile.

Domain code consumes typed `Config` rather than reading `process.env` ad hoc. The optional
[Add an environment variable](./golden-paths/add-environment-variable.md) playbook provides the
maintained implementation and verification sequence.

### Deployment and data [#deployment-and-data]

The production API runs as an unprivileged user in the included image. `server migrate` fails the
release when the database or migration resources are unavailable. `server serve` fails before
binding traffic when the database or required email templates are unavailable. Every production
readiness probe queries Postgres.

Postgres access, encryption at rest, network isolation, point-in-time recovery, and regional
residency depend on the configured infrastructure plan. Rehearse the included backup/restore scripts
against a disposable environment; their presence is not recovery evidence.

## Known limits [#known-limits]

The v1 foundation does not include:

* organization/role authorization, enterprise multi-tenancy, SSO, SCIM, or admin impersonation;
* a web application firewall;
* automated malware/file-content scanning;
* a generic worker, durable queue, or dead-letter system;
* audit-log UI or compliance export;
* field-level application encryption or customer-managed keys;
* automatic user-data retention/erasure for product-specific domains;
* an independent penetration test, SOC 2 controls, legal compliance mapping, or vulnerability SLA.

Hono endpoint limits use atomic Postgres fixed windows and share budgets across replicas. Capacity
planning must reserve at least 20% of database connections outside the combined application and
Better Auth pools.

## Production checklist [#production-checklist]

### Identity [#identity]

* Use a newly generated `BETTER_AUTH_SECRET`; never reuse the template or test value.
* Verify password, verification, reset, magic-link, logout, and session-revocation flows on the
  exact deployed origins.
* Register the exact Google callback only if Google is enabled.
* Confirm cookie and redirect behavior in every supported browser.

### Network and configuration [#network-and-configuration]

* Keep the application origin inaccessible around its selected ingress.
* Set `TRUSTED_PROXY_PROFILE` to the exact deployed ingress and do not trust a generic forwarding
  header configuration.
* Restrict CORS and Better Auth trusted origins to intended HTTPS origins.
* Keep server secrets out of Vercel and all private values out of `VITE_*`.
* Leave optional integrations fully unset or configure them completely.
* Send Railway deployment alerts through Railway's direct Slack integration; do not add an
  unauthenticated application relay for unsigned platform events.

### Providers [#providers]

* Verify the Resend sending domain and send real verification/reset/magic-link messages.
* Verify the selected provider's sandbox/test checkout, signature rejection, duplicate/out-of-order
  events, portal, cancellation, refund, reconciliation, and account deletion.
* Verify provider CORS, wrong owner, wrong MIME/size, expiry, replay, delete, and lifecycle cleanup.
* Trigger a redacted Sentry error and confirm no email, cookie, auth link, or console content
  appears.

### Operations [#operations]

* Run `vp check`, all tests, all builds, and `pnpm audit --audit-level high`.
* Run migrations `up → down → up` against a disposable database when schema changes.
* Build and smoke the production Docker image, including all rendered email templates.
* Restore a fresh database from backup and record recovery time and data loss.
* Assign security/incident ownership and publish product-specific reporting and support contacts.
* Record dependency, secret-rotation, data-retention, and access-review ownership.

## Vulnerability response [#vulnerability-response]

Before launch, choose and publish a monitored private security contact. A report should include the
affected version, reproducible steps, impact, and safe proof. Do not request public disclosure of
credentials or customer data.

For a suspected incident:

1. preserve relevant structured logs and provider event IDs without copying secrets;
2. contain access by rotating the narrowest affected credential;
3. stop harmful provider or deployment behavior without deleting evidence;
4. reconcile auth sessions, billing events, uploads, and database state;
5. restore or roll back through the rehearsed path;
6. document cause, affected data/users, required notices, and a regression test.

The seller's pre-release support policy does not create a guaranteed vulnerability SLA. Buyers own
the security and legal operation of products they build from the source.


# Setup (/docs/setup)



# Setup [#setup]

This is the supported path from delivered source to a custom local product. It requires Postgres but
no third-party account or API key.

## Pinned prerequisites [#pinned-prerequisites]

Use the repository-pinned versions, not whatever happens to be globally current:

| Tool       | Required version | Repository authority |
| ---------- | ---------------- | -------------------- |
| Node       | v24 (LTS line)   | `.node-version`      |
| Bun        | 1.3.14           | `.bun-version`       |
| pnpm       | 11.18.0          | `package.json`       |
| Vite+      | 0.2.7            | `package.json`       |
| PostgreSQL | 18               | `docker-compose.yml` |

Docker Desktop or a compatible Docker engine is required for the supported local database. dbmate is
installed in the server workspace and does not need a separate global install.

Verify the tools before scaffolding:

```sh
node --version
bun --version
pnpm --version
vp --version
docker version
```

## Create a product repository [#create-a-product-repository]

### Released buyer path [#released-buyer-path]

After accepting the private GitHub repository invitation for your purchase, install GitHub CLI and
authenticate the entitled account once:

```sh
gh auth login
```

Then run the public bootstrapper. It contains no paid source and never asks you to paste a GitHub
token:

```sh
cd /path/to/parent
pnpm dlx superslate@latest paid-monitor
cd paid-monitor
```

`bunx superslate@latest paid-monitor` is equivalent. The CLI version selects the same immutable
source tag, downloads its release archive, checksum, and manifest through the authenticated GitHub
CLI session, verifies them, applies the chosen scope and brand, installs the frozen dependency
graph, and optionally initializes Git. Use an exact version instead of `latest` when reproducing a
specific source release.

### Pre-release and maintainer path [#pre-release-and-maintainer-path]

Keep the delivered source directory unchanged so it remains a clean upgrade/reference checkout. Run
the local scaffold from its parent directory; the generated product must be a sibling, not a child
of the template. This remains the supported path until the first commercial source tag and matching
npm bootstrapper are published:

```sh
cd /path/to/parent
bun ./foundation-source/packages/create-app/src/cli.ts paid-monitor \
  --from ./foundation-source \
  --brand "Paid Monitor" \
  --git
cd paid-monitor
```

Replace the example directory and brand with the intended product values. The scaffold derives the
internal workspace namespace from the brand (`Paid Monitor` becomes `@paid-monitor`); use the
advanced `--scope` option only when an automation contract requires a different value. The namespace
groups private packages and does not need to exist on npm. The scaffold:

* copies only buyer-owned product files;
* excludes the commercial landing app, private plans, historical Phase 2 notes, evaluation runs,
  build output, dependency directories, and source-control history;
* rewrites the root package name and workspace scope;
* replaces the source brand only on the counted brand manifest;
* creates ignored root, server, web, and docs environment files;
* generates a unique 32-byte Better Auth secret;
* optionally creates an initial Git repository and commit;
* automatically installs dependencies unless `--no-install` is selected.
* offers to start Postgres and every development server immediately after creation.

It refuses a non-empty target. Do not point it at an existing project or run `--from .` while the
target would be inside the source directory.

## Install and start [#install-and-start]

Accepting the CLI's `Start local development now?` prompt runs Compose and Vite+ from the generated
directory automatically. For non-interactive automation, pass `--start`; use `--no-start` when the
command must finish after generation. `--yes` does not start a long-running development process
unless `--start` is also present. Ctrl+C stops the application development servers, while the
detached Postgres container remains available for the next run. Stop it with `docker compose down`
from the generated directory.

From the generated repository, skip the install command when the CLI already completed it:

```sh
vp install --frozen-lockfile
docker compose up -d postgres
vp run --filter @app/server dev
```

In a second terminal:

```sh
vp run --filter @app/web dev
```

The separate documentation application is optional during product development. Run it in a third
terminal when editing buyer documentation:

```sh
pnpm --filter @app/docs dev
```

Open `http://localhost:3002/docs`. See [Operate the documentation site](./docs-site.md) before
publishing it.

Compose scopes the database container to the generated project. The scaffold selects the first
available port starting at `5432`, records it in the ignored root `.env`, and writes the same port
into `apps/server/.env`. The normal command therefore remains:

```sh
docker compose up -d postgres
```

Use the namespace derived during scaffolding. Open `http://localhost:5173`. The API listens on
`http://localhost:8000`; the CLI completion summary reports the selected Postgres port.

The server must connect to Postgres, apply pending dbmate migrations, register event handlers, and
preload all rendered email templates before it binds the port. Verify both operational endpoints:

```sh
curl --fail http://localhost:8000/health
curl --fail http://localhost:8000/ready
```

`/health` proves the process can answer. `/ready` proves the initialized database can answer. A
successful health response with failed readiness is not a successful setup.

## Zero-key behavior [#zero-key-behavior]

With the generated environment files unchanged:

* password signup, email verification, password reset, and magic-link sign-in work;
* email bodies and action links are written to the local server log;
* Google sign-in is disabled;
* billing checkout and customer portal actions are disabled;
* object-storage upload endpoints return a stable unavailable response;
* Slack alerts and Sentry delivery are disabled.

Never use the email-log fallback in production. Production environment validation requires
`RESEND_API_KEY`.

Optional integrations are atomic. Select one billing adapter and configure all of its required
values, or leave billing unset. Configure every selected storage provider value together. Partial
billing or storage configuration fails at startup.

## Demo sign-in during local development [#demo-sign-in-during-local-development]

Repeated sign-in during development is avoidable. Seed a verified local account and enable a
one-click button on `/auth/login`:

```sh
pnpm dev:server     # the seed script calls the running API
pnpm seed:demo      # creates demo@example.com and marks it verified
```

Then set both values in `apps/web/.env`:

```sh
VITE_DEV_LOGIN_EMAIL=demo@example.com
VITE_DEV_LOGIN_PASSWORD=demopassword123
```

The button signs in through the normal Better Auth password route; it adds no server endpoint and no
authentication bypass. It renders only when `import.meta.env.DEV` is true, so `vp build` removes the
control and both values from the bundle even when they are present in the build environment.
`pnpm seed:demo` refuses any API origin outside `localhost`. Override the defaults with
`DEMO_EMAIL`, `DEMO_PASSWORD`, or `DEMO_API_URL`.

## First verification [#first-verification]

Run the same deterministic path used by CI:

```sh
vp check
vp run -r typecheck
vp run -r test
vp run -r build
```

`vp run -r test` intentionally skips real Postgres integration unless both opt-in variables are set.
Run the database suite against a disposable migrated database:

```sh
RUN_DB_INTEGRATION_TESTS=1 \
DATABASE_URL=postgresql://app:app@localhost:5432/app?sslmode=disable \
bun test --cwd apps/server
```

See [Testing](./testing.md) for isolation rules and [Deployment](./deployment.md) for the compiled
binary, Docker, Railway or Dokploy, Vercel, production email, and rollback proof.

## Pinned-toolchain risk and escape hatches [#pinned-toolchain-risk-and-escape-hatches]

Vite+ `0.x` and TypeScript 7 are intentional early-adopter choices. They provide one fast workspace
command surface and current language/tooling behavior, but their APIs, plugin compatibility, and
diagnostics can change faster than stable major releases.

Operational rules:

* keep `pnpm-lock.yaml`, Node, Bun, pnpm, and Vite+ pinned together;
* do not accept automated major or beta upgrades without a fresh scaffold, CI, build, PWA, Sentry,
  and deployment proof;
* reproduce a Vite+ failure with the owning package command before changing architecture;
* retain package-native commands as the recovery path:

```sh
bun test --cwd apps/server
pnpm --filter @app/server exec tsc -b --noEmit
pnpm --filter @app/web exec tsc -b --noEmit
pnpm --filter @app/web exec vitest run
pnpm --filter @app/server run build
(cd apps/web && pnpm dlx vite@8.1.5 build --mode production)
```

The final command is an online diagnostic escape hatch: it changes into the web workspace and runs
the exact underlying official Vite version without changing the lockfile. Running it from the
monorepo root is invalid because the root has no `index.html`. It has been verified against the
current web config, but it does not replace the Vite+ release command or prove Vite+ orchestration.
If it passes while `vp build` fails, restore the last green pinned Vite+ version/lockfile and
diagnose the orchestrator instead of rewriting the application. Changing Vite+, Vite, TypeScript,
Node, or Bun is a release-engineering change, not routine dependency maintenance.

## Failure guide [#failure-guide]

| Symptom                                       | Check                                                                  |
| --------------------------------------------- | ---------------------------------------------------------------------- |
| `vp install` changes the lockfile             | Use pnpm 11.18.0 and `--frozen-lockfile`; do not accept the diff.      |
| Server cannot connect to Postgres             | Compare `DATABASE_URL` with Compose and any `POSTGRES_PORT` override.  |
| `/health` works but `/ready` returns 503      | Database initialization, migration output, and Postgres health.        |
| Server exits before binding                   | Missing migrations/templates, invalid environment, or failed DB.       |
| No email arrives locally                      | Read structured server logs; local delivery does not call Resend.      |
| Google, billing, upload, Slack, or Sentry off | Expected when its complete optional configuration is absent.           |
| Browser requests return 401                   | API origin, CORS origin, Better Auth cookie, and `credentials` policy. |
| Generated code keeps source scope or brand    | Re-run scaffold tests; do not perform an unbounded text replacement.   |

## Setup acceptance [#setup-acceptance]

Setup is complete only when:

* the product is a generated sibling repository with its own unique auth secret and clean Git
  history;
* frozen install, checks, tests, and builds pass without editing generated or lock files;
* Postgres starts from the included Compose contract and `/ready` returns 200;
* a password signup can be verified from the local email log;
* a magic link works once and replay fails;
* disabled integrations fail clearly without blocking basic local use.


# Object storage (/docs/storage)



# Object storage [#object-storage]

The foundation supports Cloudflare R2, Amazon S3, and Google Cloud Storage behind one upload
contract. The included product purpose is a public profile image. Uploads cross a private staging
boundary before an immutable public object is created; new upload types require their own policy,
ownership, and adversarial proof.

## Provider selection [#provider-selection]

| Deployment         | Default  | Available override | Runtime identity                   |
| ------------------ | -------- | ------------------ | ---------------------------------- |
| AWS                | S3       | R2                 | ECS task role for S3               |
| GCP                | GCS      | R2                 | Cloud Run service identity for GCS |
| Azure              | R2       | none               | R2 API credentials                 |
| Railway or Dokploy | disabled | R2                 | R2 API credentials                 |
| Local              | disabled | R2, S3, or GCS     | provider development credentials   |

Guided AWS and GCP deployment provisions the selected native buckets, CORS, lifecycle policy, public
access, and workload permissions. Azure remains a supported deployment target but has no Azure Blob
adapter.

`STORAGE_PROVIDER` is one of `disabled`, `r2`, `s3`, or `gcs`. Every enabled provider requires:

```text
STORAGE_BUCKET_NAME
STORAGE_STAGING_BUCKET_NAME
STORAGE_PUBLIC_URL
```

The destination and staging buckets must differ. The staging bucket must never be public. Provider
requirements are additive:

| Provider | Additional configuration                                                            |
| -------- | ----------------------------------------------------------------------------------- |
| R2       | `CLOUDFLARE_ACCOUNT_ID`, `CLOUDFLARE_ACCESS_KEY_ID`, `CLOUDFLARE_SECRET_ACCESS_KEY` |
| S3       | `STORAGE_REGION`; credentials come from the AWS SDK default chain                   |
| GCS      | `GOOGLE_CLOUD_PROJECT`; credentials come from Application Default Credentials       |

Partial selected-provider configuration fails environment validation. `disabled` preserves the
zero-key local path and returns a stable 503 from upload endpoints.

## Included upload contract [#included-upload-contract]

`PROFILE_PIC` permits:

* authenticated owner only;
* `image/jpeg`, `image/jpg`, `image/png`, or `image/webp`;
* 1 byte through 5 MiB;
* a 15-minute presigned `PUT` to `pending/<user-id>/<upload-id>.<extension>`;
* a validated conditional copy to `user-profiles/<user-id>/<upload-id>.<extension>`;
* a public immutable-cache URL only after confirmed completion.

The profile image is public by design. Do not reuse this flow for private documents, identity data,
financial files, or confidential customer uploads.

`USER_FILE` backs the Files module and permits:

* authenticated owner only; `entity_id` must equal the session user id;
* `image/jpeg`, `image/jpg`, `image/png`, `image/webp`, or `application/pdf`;
* 1 byte through 10 MiB;
* a 15-minute presigned `PUT` to `pending/<user-id>/<upload-id>.<extension>`;
* a validated conditional copy to `user-files/<user-id>/<upload-id>.<extension>` **inside the
  staging bucket**, outside the `pending/` prefix that the one-day lifecycle rule expires;
* no public URL, ever. Delivery is a 5-minute presigned `GET` whose signature pins
  `response-content-disposition: attachment` and `response-content-type` to the stored MIME type, so
  a PDF cannot render inline from the bucket origin.

Private placement needs no new bucket and no new environment variable. The `uploads` row is the
durable file record; the Files API speaks `file_id` (`uploads.upload_id`) only and never accepts or
returns a storage key.

PDF bytes are checked for structure — a `%PDF-1.<digit>` header, and `startxref` before `%%EOF`
within the final 1 KiB. That stops a renamed PNG from passing as a PDF. It is **not** sanitization
and **not** malware scanning; attachment-only private delivery is the compensating control. Widening
the MIME allowlist requires extending both the validation and the delivery story first.

Deleting a file transitions the owner-scoped row to `deleted` in one guarded statement, then deletes
the object best-effort. A replayed delete, a cross-owner delete, and an unknown id are
indistinguishable: all three return the same not-found response.

## State machine [#state-machine]

```mermaid
sequenceDiagram
  participant Browser
  participant API
  participant DB as Postgres
  participant Stage as Private staging bucket
  participant Public as Public asset bucket

  Browser->>API: purpose + owner target + MIME + exact byte size
  API->>API: session, limit, target ownership, lifecycle readiness
  API->>DB: create pending claim with private and public keys
  API-->>Browser: 15-minute staged PUT + upload ID + required Content-Type
  Browser->>Stage: PUT with signed Content-Length and Content-Type
  Browser->>API: confirm upload ID
  API->>DB: lock owner-scoped pending claim
  API->>Stage: read metadata and exact claimed object version
  API->>Public: publish the validated object version with immutable metadata
  API->>DB: complete once
  API->>Stage: delete temporary object
  API-->>Browser: public key and URL
```

The presigned URL is a temporary bearer credential that addresses only the private staging bucket.
Its signature binds the exact declared `Content-Length` and `Content-Type`. Confirmation checks the
stored byte count and MIME metadata, reads the complete provider-pinned object, and validates PNG
chunks/CRCs, JPEG markers/scans, or WebP RIFF/chunks with bounded dimensions and no trailing
polyglot payload.

S3 and R2 pin reads and copies to the same ETag. GCS pins them to the same object generation. An
overwrite racing metadata, content validation, or publication therefore fails instead of publishing
unvalidated bytes.

The public key is never disclosed before confirmation and never receives a presigned write URL.
Published responses use `Cache-Control: public, max-age=31536000, immutable`. Concurrent or replayed
confirmation completes at most once under a database row lock. At most four confirmation operations
run concurrently per API process; excess requests receive a retryable 503.

Rejected and completed claims best-effort delete their staged object. A mandatory one-day lifecycle
rule is the crash and abandonment backstop. The API verifies that policy before issuing an upload
URL, refreshes the verification at least every five minutes, and returns 503 when it is absent or
too permissive.

## Provider operations [#provider-operations]

### AWS and GCP deployment [#aws-and-gcp-deployment]

Select the native provider during `create-app` or `pnpm deploy:configure`. OpenTofu creates two
application-specific buckets per environment, configures exact frontend-origin `PUT` CORS, grants
the API workload identity object access, makes only `user-profiles/` publicly readable, and applies
the one-day `pending/` lifecycle rule.

AWS uses the JavaScript SDK default credential chain, so ECS receives temporary task-role
credentials. GCS uses Application Default Credentials and grants the Cloud Run service account the
object and `iam.serviceAccounts.signBlob` permissions required for V4 signed URLs. Neither native
path stores an application access key.

### Cloudflare R2 [#cloudflare-r2]

1. Create a private staging bucket and a public asset bucket per environment.
2. Give API credentials object read/write/delete access to both buckets and lifecycle-read access to
   staging.
3. Add `app-pending-upload-expiry-v1` for the `pending/` prefix with one-day expiry.
4. Configure staging CORS for exact app origins, `PUT`, and `Content-Type`.
5. Configure the public custom domain/base URL.
6. Set the shared storage values and all three `CLOUDFLARE_*` credentials atomically.

Keep the lifecycle rule scoped to the `pending/` prefix. `USER_FILE` objects live in the same
staging bucket under `user-files/`, deliberately outside `pending/`, so a bucket-wide expiry rule
would delete users' files after a day. The adapter's readiness check rejects a rule whose prefix is
not exactly `pending/`, so a mis-scoped rule fails closed rather than deleting data.

Do not share production credentials with previews or local development. Use separate buckets,
credentials, lifecycle rules, and public bases per environment.

### Demo data [#demo-data]

`scripts/seed-demo-data.sh` inserts demo users and a `USER_FILE` library for the local demo account.
Those rows describe objects that do not exist yet, so downloads fail until the bytes are written:

```sh
pnpm --filter @app/server seed:demo-objects
```

That command generates a real, valid PDF or PNG for every seeded row at the exact recorded byte
size, then writes it through the same staged `PUT` and conditional private publish the product uses,
so nothing bypasses the storage contract. It refuses to run against a production environment, and
skips any object that already exists. Seeded rows are PDF and PNG only because those are the two
formats the generator can produce as genuinely valid files.

## Known limits [#known-limits]

* Structural validation rejects malformed containers and trailing payloads but does not fully
  decompress pixels, strip EXIF, resize, moderate, or detect malware.
* Lifecycle deletion is asynchronous; application deletion remains the fast path.
* Public URLs remain valid until object deletion and may be cached externally.
* The baseline contains no generic file browser, quota ledger, multipart upload, private download
  authorization, Azure Blob adapter, or attachment domain.

Add higher-risk purposes only after specifying retention, private/public access, processing,
malware/content controls, deletion, abuse/cost bounds, and support behavior.

## Verification and diagnosis [#verification-and-diagnosis]

Run the provider-independent suite:

```sh
bun test --cwd apps/server \
  src/domains/upload/service.test.ts \
  src/infra/storage/r2.test.ts
RUN_DB_INTEGRATION_TESTS=1 DATABASE_URL=postgresql://... \
  bun test --cwd apps/server src/domains/upload/service.integration.test.ts
vp check
pnpm verify:infra
```

Use disposable buckets for live provider rehearsals. Prove correct upload, wrong origin, wrong
owner, wrong MIME header, spoofed bytes, wrong size, expiry, replay, concurrent confirmation,
staging cleanup, lifecycle cleanup, publication, and public deletion.

| Failure                     | Inspect                                                                 |
| --------------------------- | ----------------------------------------------------------------------- |
| Upload endpoint returns 503 | selected provider and every required shared/provider field              |
| Lifecycle-policy 503        | staging `pending/` rule, one-day expiry, and lifecycle-read permission  |
| Browser CORS failure        | staging bucket, exact origin, `PUT`, and `Content-Type`                 |
| Signature mismatch          | exact bytes, MIME header, method, expiry, provider identity, and bucket |
| Confirmation says not found | staging key/bucket, completed PUT, claim expiry, and object version     |
| Size/MIME/content mismatch  | staged metadata and complete image structure                            |
| Version changed during copy | start a new upload claim                                                |
| Public URL is wrong         | public bucket, `STORAGE_PUBLIC_URL`, and final key mapping              |

Consult the optional [Add an upload type](./golden-paths/add-upload-type.md) playbook.


# Support (/docs/support)



# Requesting product support [#requesting-product-support]

This file describes how to prepare a safe, reproducible source-product support request. It does not
create a support entitlement, response target, contact address, or service beyond the buyer's
approved Order and the applicable support policy in `docs/commercial/support.md`.

The product is pre-release. Until an approved Order publishes a real contact and release channel,
this repository alone does not open a support channel. Do not guess or publish an email address.

## Before requesting support [#before-requesting-support]

1. Confirm the exact current tagged product release and update/support entitlement.
2. Follow the relevant setup, provider, troubleshooting, or golden-path document.
3. Reproduce the failure in an unmodified custom-scope/custom-brand reference generated from the
   same tag when practical.
4. Reduce the failure to the smallest command, request, migration, or UI path.
5. Redact the evidence and inspect it manually before sending.

## Request template [#request-template]

```text
Order or entitlement reference:
Product release tag and commit:
Operating system:
Node / Bun / pnpm / Vite+ / Postgres / browser versions:
Document and exact step followed:
Expected behavior:
Actual behavior:
Failing command and exit code:
Smallest safe reproduction:
Material modifications near the boundary:
Does it reproduce in an unmodified generated reference?
Redacted logs/tests attached:
Business impact:
```

Send the request only through the private contact or release channel identified in the approved
Order. A merchant-of-record receipt or checkout support thread may identify the purchase, but it is
not permission to expose source, credentials, customer data, or vulnerabilities publicly.

## Never send [#never-send]

* passwords, session cookies, auth/reset/magic links, OAuth secrets, or Better Auth secrets;
* provider API keys, webhook secrets, signed URLs, private repository tokens, or deployment tokens;
* `.env` files, database dumps, payment details, or real customer records;
* proprietary application source unrelated to the smallest reproduction;
* unreviewed coding-agent transcripts or logs that may contain any of the above;
* an unpatched vulnerability in a public issue or community channel.

Use key names, redacted identifiers, safe synthetic data, error codes, hashes, and minimal excerpts.
Rotate a credential immediately if it was exposed.

## Supported boundary [#supported-boundary]

The strongest defect report reproduces on the current tagged, unmodified release with its pinned
toolchain and supported Railway/Vercel, Dokploy/Vercel, and provider paths. Product modifications,
arbitrary dependency combinations, provider-account operations, custom deployment, data recovery,
incident response, and feature implementation are not automatically included support.

The maintainer may respond with a documentation correction, configuration finding, reproduction,
workaround, tracked defect, security coordination, or future release. Support does not promise that
every request becomes custom code.


# Tech stack (/docs/tech-stack)



The stack is intentionally split by responsibility. Your browser app, API, shared contracts,
provider adapters, and operational tooling can evolve independently without losing type safety.

<StackShowcase />

## Choose the layer you are working in [#choose-the-layer-you-are-working-in]

<Tabs items="['Product UI', 'API & data', 'Operations']">
  <Tab>
    Work in `apps/web` for product screens, React Router routes, TanStack Query server state, and
    narrowly scoped Zustand client state. Browser code never owns authorization or provider secrets.
  </Tab>

  <Tab>
    Work in `apps/server` for HTTP routes, policy, transactions, repositories, and dbmate
    migrations. Put shared wire schemas in `packages/contracts` so the client and server stay
    aligned.
  </Tab>

  <Tab>
    Configure external providers through typed server configuration. Email, billing, storage,
    Sentry, and Slack are optional locally and fail clearly when partially configured in a selected
    environment.
  </Tab>
</Tabs>

| Layer         | Technology                               | Why it is here                                                                   |
| ------------- | ---------------------------------------- | -------------------------------------------------------------------------------- |
| Web app       | React 19, React Router, Vite+            | Fast client application with explicit route and module boundaries.               |
| Client data   | TanStack Query, Zustand                  | Server-state caching and narrowly scoped client state.                           |
| API           | Bun, Hono, Hono RPC                      | A compact typed HTTP API and direct end-to-end contract inference.               |
| Validation    | Zod                                      | Shared request and response schemas in `packages/contracts`.                     |
| Data          | PostgreSQL 18, dbmate, parameterized SQL | Durable schema history without hiding database behavior behind an ORM.           |
| Identity      | Better Auth                              | Password, verification, reset, magic links, sessions, and optional Google OAuth. |
| Billing       | Polar, Stripe, or Dodo Payments          | One selected provider behind a normalized entitlement contract.                  |
| Email         | React Email, Resend                      | Typed templates, deterministic HTML, and local log fallback.                     |
| Storage       | Cloudflare R2, Amazon S3, or GCS         | Owner-scoped, presigned uploads with server confirmation.                        |
| Observability | Pino, optional Sentry, Slack             | Structured logs, protected error reporting, and operational alerts.              |
| Quality       | TypeScript, Vite+, Bun test, Playwright  | Fast feedback plus deterministic workspace verification.                         |

## What is deliberately not included [#what-is-deliberately-not-included]

Version one does not include multi-tenant organizations, generic background jobs, durable queues,
file malware scanning, or automatic compliance controls. Add those only when your product needs them
and after choosing the right ownership and operational model.

<Callout title="Rate limits are shared" type="info">
  The included Hono endpoint limiters use atomic Postgres fixed windows, so configured budgets are
  shared across API replicas. Provider and infrastructure limits still need capacity and abuse proof
  before raising replica counts.
</Callout>

Continue with [Project structure](./project-structure.mdx) to see where each concern lives.


# Testing (/docs/testing)



# Testing [#testing]

The repo uses **two runners**, split by runtime:

| Workspace                                                           | Runner               | Why                                                                                     |
| ------------------------------------------------------------------- | -------------------- | --------------------------------------------------------------------------------------- |
| `apps/server`                                                       | `bun test`           | Server code imports Bun built-ins (`bun:sql`, `Bun.file`) that Node/vitest cannot load. |
| `packages/billing`, `create-app`, `agent-context`, and `agent-eval` | `bun test`           | Package tests use Bun and Node-compatible APIs.                                         |
| `apps/docs`                                                         | `bun test`           | Publication-contract tests run without starting Next.js.                                |
| `packages/contracts`                                                | vitest via `vp test` | Pure Zod schemas and helpers.                                                           |
| `apps/web`                                                          | vitest via `vp test` | Node-based utilities and static component-state rendering; browser interaction is E2E.  |

> Why not vitest-on-bun for the server? Verified: vitest always spawns its worker processes with
> **Node** even when invoked as `bun x vitest` (probe: `Bun` global is `undefined`, worker
> `process.execPath` is node), so Bun built-ins don't exist inside vitest tests and there is no
> supported option to change the worker runtime. `bun test` has a jest-compatible API
> (`describe`/`test`/`expect` from `bun:test`), so test files look the same as the vitest suites.

`packages/email` has no standalone test script:

```sh
vp run --filter @app/email dev
vp run --filter @app/email deploy
```

The first command previews sources. The second regenerates and inventory-checks buyer HTML. Run
renderer, local-fallback, and provider tests through the server suite.

## Commands [#commands]

```sh
pnpm test                 # root: `vp run -r test` — every workspace suite, each with its own runner
cd apps/server && bun test          # server suite standalone
cd packages/contracts && vp test    # any vitest workspace standalone (watch mode: `vp test watch`)
```

Verified against the pinned toolchain (vite-plus 0.2.7, vitest ^4.1.10, bun 1.3.x):

* `vp test` in a workspace runs **vitest** in that workspace, forwarding options (`vp test run`,
  `vp test watch`, `--coverage`, ...). Workspaces without a vite config use vitest defaults
  (`**/*.test.ts`); `apps/web` has a `vitest.config.ts` so tests skip the PWA/Sentry build plugins
  in `vite.config.ts`.
* A bare `vp test` at the repo root also works: the root `vite.config.ts` excludes `apps/server/**`
  so vitest never tries to load Bun built-ins. It runs only the vitest suites — use `pnpm test` to
  include the server.
* `bun test` discovers `*.test.ts` under `apps/server` and preloads `src/test-setup.ts` (see
  `bunfig.toml`), which provides safe env defaults because `src/config/env.ts` validates
  `process.env` at import time.

## DB-gated integration tests [#db-gated-integration-tests]

Unit tests never touch the database. Integration tests run only when `RUN_DB_INTEGRATION_TESTS=1`
and `DATABASE_URL` are both set before `bun test`; they skip visibly otherwise. The explicit flag
prevents an unrelated `DATABASE_URL` in your shell from changing the default test suite:

```sh
docker compose up -d
cd apps/server && dbmate --migrations-dir ./migrations --no-dump-schema up
RUN_DB_INTEGRATION_TESTS=1 DATABASE_URL=postgres://... bun test
```

`apps/server/src/domains/notifications/repository.integration.test.ts` is the repository-test
template: `initDb()` in `beforeAll`, clean up your fixtures and `closePool()` in `afterAll`, and
gate with `describe.skipIf`. The test preload forcibly disables external email, Slack, Google, and
rate limit side effects even if Bun loaded values from a developer `.env`.

Better Auth has one `pg` pool for the Bun test process. The global test preload closes it after all
files finish; individual suites must not call `closeAuthPool()` because later HTTP suites may still
need session storage.

`apps/server/src/infra/db/migrate.integration.test.ts` is the migration-failure fixture. It applies
one valid temporary migration followed by intentionally invalid transactional DDL, then proves the
valid version remains recorded while the failed file leaves neither its table nor a version row. Use
it with the [failed-migration golden path](./golden-paths/diagnose-failed-migration.md); never point
it at a shared database.

## Billing and authentication coverage [#billing-and-authentication-coverage]

Billing remains covered by its DB-gated integration suite. Better Auth's HTTP suite exercises the
real Hono handler and Postgres adapter across password signup, verification, authenticated user
resolution, logout, password reset and session revocation, magic-link signup, single-use replay
rejection, and the optional-Google failure state. The compiled-server smoke test separately proves
that the same flow survives the standalone Bun build. Add browser E2E coverage when changing buyer
UI flows, providers, cookies, redirects, or auth plugins.

* `apps/server/src/domains/billing/service.integration.test.ts`
* `apps/server/src/lib/auth.integration.test.ts`

The upload suite uses a fake provider adapter plus real Postgres to prove owner scoping, expiry,
actual metadata validation, failed-object cleanup, replay, and concurrent confirmation without
requiring storage credentials:

* `apps/server/src/domains/upload/service.test.ts`
* `apps/server/src/domains/upload/service.integration.test.ts`

CI supplies both variables, so these suites run against its Postgres service instead of skipping. CI
also starts the compiled server from the repository root, requires all `9/9` rendered email
templates to preload, and sends a real magic-link request. This guards the external-resource layout
used by the standalone binary rather than proving only source execution.

## Browser end-to-end coverage [#browser-end-to-end-coverage]

Playwright runs through `pnpm test:e2e` from the repository root. It starts the local API and web
application, migrates the exact `DATABASE_URL` it receives, and exercises browser-visible behavior.
Use a named disposable database; never point the browser suite at a shared, staging, or production
database.

The complete local workflow, CI boundary, failure artifacts, and test-writing rules are in
[End-to-end testing](./e2e-testing.md). Browser coverage complements rather than replaces contract,
server, integration, provider, and deployed smoke tests.

## Web bundle budget [#web-bundle-budget]

Every web build runs `apps/web/scripts/verify-bundle-budget.ts` after Vite emits `dist/`. It reads
the built HTML rather than guessing from source imports and fails when:

* the module entry plus its module-preload dependencies exceed 512 KiB; or
* any individual JavaScript chunk exceeds 450 KiB.

Route-lazy JavaScript is not charged to initial startup, but it remains subject to the per-chunk
limit. The budget does not claim that the complete PWA precache is downloaded on first navigation;
measure transfer, parse, and interaction timing in a real browser before making a performance claim.
When the budget fails, inspect eager route/layout imports before adding manual vendor chunk rules or
raising the limit.


# Troubleshooting (/docs/troubleshooting)



# Troubleshooting [#troubleshooting]

Start from an observable symptom. Preserve the first failure and exact command before changing code,
dependencies, environment, data, or provider state.

## Safe evidence bundle [#safe-evidence-bundle]

Record:

```sh
git status --short
git rev-parse HEAD
node --version
bun --version
pnpm --version
vp --version
docker compose ps
vp run --last-details
```

Include the failing command, exit code, expected behavior, smallest reproduction, relevant redacted
logs, and whether the same failure occurs in an unmodified custom-scope reference scaffold.

Never include `.env` contents, auth links, cookies, API keys, OAuth secrets, webhook secrets,
database dumps, private customer data, signed upload URLs, or unreviewed agent transcripts. Name an
environment key; do not paste its value.

## Install, checks, and builds [#install-checks-and-builds]

| Symptom                              | Observe first                                                     | Next action                                                                                    |
| ------------------------------------ | ----------------------------------------------------------------- | ---------------------------------------------------------------------------------------------- |
| Frozen install reports lock drift    | `pnpm --version`, changed manifests, `git diff -- pnpm-lock.yaml` | restore pnpm 11.18.0 and the release lockfile; regenerate only for a deliberate package change |
| Supply-chain policy rejects install  | exact rejected package/script and `pnpm-workspace.yaml` owner     | review the package and install script; never blanket-enable all build scripts                  |
| `vp check` fails only after scaffold | file, formatter diff, chosen scope/brand length                   | reproduce in a fresh custom scaffold and fix the source template                               |
| Vite+ command fails                  | owning package command and `vp run --last-details`                | run `pnpm verify:toolchain`; do not rewrite the app                                            |
| Type checking fails                  | first diagnostic and package-native `tsc -b --noEmit`             | restore release pins, fix the type boundary, and retain type-aware checks                      |
| Bun server build/test fails          | pinned Bun version, direct test/build output                      | restore Bun 1.3.14 before diagnosing an upgrade; Node is not a server-runtime fallback         |
| Web build warns about chunk size     | emitted entry and PWA precache sizes                              | treat as visible performance risk; a successful build does not make the warning disappear      |

## Local server and database [#local-server-and-database]

| Symptom                              | Exact observation                                                               | Follow-up                                                                                            |
| ------------------------------------ | ------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------- |
| Postgres is unavailable              | `docker compose ps` and redacted host/port/database name                        | compare `DATABASE_URL` ownership with Compose; do not paste credentials                              |
| `/health` is 200 but `/ready` is 503 | migration output, Postgres health, readiness log                                | inspect initialization and [failed migration diagnosis](./golden-paths/diagnose-failed-migration.md) |
| Server exits before binding          | first startup error; rendered template/migration directories                    | restore required resources or explicit paths; do not bypass preload                                  |
| Migration fails                      | `dbmate status`, exact migration, disposable reproduction                       | never edit a released migration or forge `schema_migrations`                                         |
| DB tests skip unexpectedly           | `RUN_DB_INTEGRATION_TESTS` and resolved test database name, without credentials | use a disposable migrated Postgres and the documented opt-in                                         |

Use [Setup](./setup.md), [Testing](./testing.md), and the migration golden paths for exact commands.

## Authentication and browser sessions [#authentication-and-browser-sessions]

| Symptom                            | Inspect                                                                                       |
| ---------------------------------- | --------------------------------------------------------------------------------------------- |
| Local email or magic link missing  | structured server log; local delivery deliberately does not call Resend                       |
| Auth request returns 401           | browser API origin, cookie presence/attributes, `credentials: include`, CORS, trusted origins |
| Verification/reset link is wrong   | `FRONTEND_URL`, `SERVER_URL`, callback allowlist, provider redirect URL                       |
| Google button is absent            | complete Google configuration; the flow is deliberately hidden when unset                     |
| OAuth works locally but not live   | exact HTTPS origin/callback configured in app, Better Auth, provider, and deployment          |
| Another user's resource is visible | stop deployment; add owner-scoped SQL and missing/cross-owner tests                           |

Follow [Authentication](./authentication.md) and [Security](./security.md). Never diagnose sessions
by logging raw cookies, tokens, passwords, auth links, or provider secrets.

## Billing, email, and storage [#billing-email-and-storage]

| Symptom                                  | Inspect                                                                                              |
| ---------------------------------------- | ---------------------------------------------------------------------------------------------------- |
| Checkout/portal is unavailable           | selected provider, complete environment, plan mapping, authenticated user                            |
| Paid state differs from provider         | event ID/type/timestamp, local claim/state, provider subscription, ordering—not secret/payload dumps |
| Webhook retries or returns non-2xx       | [failed webhook diagnosis](./golden-paths/diagnose-failed-webhook.md); repair then provider replay   |
| Billing state committed but email absent | post-commit notification log; do not replay the financial event solely to resend communication       |
| Resend rejects sender                    | verified domain, `EMAIL_FROM`, environment/account ownership, provider error code                    |
| Upload endpoint is unavailable           | selected provider, shared bucket values, provider fields, and private staging lifecycle policy       |
| Presigned PUT fails in browser           | staging CORS, exact method/origin, URL expiry, signed byte length and `Content-Type`                 |
| Upload confirmation fails                | claim owner/state/expiry, staged size/MIME/signature bytes, and copy precondition                    |

Follow [Billing](./billing.md), [Email](./email.md), and [Storage](./storage.md). A provider
dashboard is evidence of provider state, not permission to mutate local database rows by hand.

## Deployment and PWA [#deployment-and-pwa]

| Symptom                                 | Inspect                                                                                |
| --------------------------------------- | -------------------------------------------------------------------------------------- |
| Railway/Dokploy image is not healthy    | build architecture, migration/template copy, environment validation, `/ready` logs     |
| Vercel loads 404 on a client route      | SPA rewrite and output directory                                                       |
| Live auth loops or loses session        | exact HTTPS origins, cookie attributes, proxy headers, API URL, CORS                   |
| Old UI persists after deployment        | deployed artifact ID, service-worker update, browser/PWA cache, release commit         |
| Sentry source maps or tunnel fail       | build-time token/org/project, runtime DSN, server project allowlist, exact Sentry host |
| Rollback restores code but not behavior | database/provider changes that outlived the deployment                                 |

Use [Deployment](./deployment.md). Clear only disposable local browser/PWA state during diagnosis;
do not instruct customers to erase application data as a generic fix.

## Branding, customization, and removal [#branding-customization-and-removal]

* A stale name usually means a surface is missing from the counted manifest or generated email was
  not refreshed. Follow [Customization](./customization.md).
* An `@app` reference in a generated buyer is a scaffold contract failure, not a request for global
  replacement.
* A removed integration that still asks for a key, mounts a route, ships a dependency, or appears in
  current claims was disabled, not removed. Follow [Removing subsystems](./removing-subsystems.md).
* Historical decision records may retain a retired name with explicit historical context.

## Escalation [#escalation]

Use [Requesting support](./support.md) only after producing a safe reproduction. A failure caused by
product changes may still be diagnosable, but it is not automatically a foundation defect. An
unmodified reference failure on the current tagged release is the strongest support signal.


# Unused code (/docs/unused-code)



# Unused code [#unused-code]

[Knip](https://knip.dev) reports files, dependencies, and exports that nothing reaches. It resolves
the real import graph per workspace, so it finds the leftovers that a generated feature, a
half-finished refactor, or a removed subsystem leaves behind — the kind of code lint rules and the
type checker cannot see, because every dead file is internally consistent.

Run it after a feature is built and before the change is declared complete.

## Commands [#commands]

```sh
pnpm knip       # apps/*, packages/*, and e2e
pnpm knip:fix   # apply the safe removals, then re-verify
```

`knip.json` at the repository root configures every workspace: its entry points, the files each
workspace analyzes, and the dependencies that are referenced by runtime string rather than by
import.

`pnpm knip:fix` removes unused dependencies from manifests and unused `export` keywords from source
files. It does not delete files. Nothing about it is a substitute for verification: run `vp check`,
`vp run -r typecheck`, and `vp run -r test` afterwards, and read the diff before committing it.

## What fails, and what only reports [#what-fails-and-what-only-reports]

| Reported                                                                 | Severity | Why                                                            |
| ------------------------------------------------------------------------ | -------- | -------------------------------------------------------------- |
| Unused files                                                             | error    | A file nothing imports is waste in every deployment artifact.  |
| Unused dependencies and dev dependencies                                 | error    | Unused packages still carry install time, size, and CVE risk.  |
| Unlisted, unresolved, and missing binaries                               | error    | An import with no declared owner breaks a fresh clean install. |
| Unused exports, exported types, namespace members, and duplicate exports | warning  | The foundation deliberately ships surface no page calls yet.   |

CI runs `pnpm knip` after `vp check`, so the error tier is a merge gate and the warning tier is
review information. Warnings are still findings: an export the change itself introduced and never
used is dead code, and it should leave with the same commit.

## Declared entry points [#declared-entry-points]

Knip treats these as reachable even when nothing imports them, because they are product surface a
buyer consumes rather than code this repository calls:

| Path                              | Reason                                                               |
| --------------------------------- | -------------------------------------------------------------------- |
| `apps/web/src/components/ui/**`   | The shipped primitive library documented in [Web UI](./web-ui.md).   |
| `**/*.test.ts`, `scripts/**`      | Test suites and maintenance scripts are roots, not imported modules. |
| `packages/*` package entry points | Workspace libraries are consumed through their manifest entry.       |

Add a primitive to `apps/web/src/components/ui` and knip stays quiet — record it in
[Web UI](./web-ui.md) instead, so the inventory stays the review surface for that directory.

## Handling a finding [#handling-a-finding]

Work in this order:

1. **Wire it up.** A helper, hook, or component that a feature was supposed to use is a missing
   call, not a knip problem. Fix the feature.
2. **Delete it.** Nothing plans to call it: remove the file, the export, and the dependency it kept
   alive. Prove the deletion with `vp check`, typecheck, tests, and a build.
3. **Declare it.** It is genuinely reachable outside the import graph — a runtime string reference,
   a provider plugin, a buyer-facing primitive. Add an `entry`, `ignoreDependencies`, or `project`
   pattern to the owning `knip.json` in the same change, and say in the commit why the code cannot
   be reached statically.

Do not silence a finding by widening `ignore` patterns because the report is inconvenient. A
directory-wide exemption removes that directory from every future report, which is exactly how the
next generated feature hides its own leftovers.

## Known false-positive sources [#known-false-positive-sources]

* **Runtime string references.** `pino-pretty` is named as a transport target string, never
  imported; it is declared in `apps/server`'s `ignoreDependencies`.
* **Toolchain aliases.** The root `vite` entry is the Vite+ core alias that `vp` resolves, so it is
  declared at the root workspace.
* **Generated content.** `apps/docs/.source` is produced by `fumadocs-mdx` at build time and is
  covered by `.gitignore`, so knip never sees it as source.


# Upgrading (/docs/upgrading)



# Upgrading a generated product [#upgrading-a-generated-product]

The buyer owns a product repository; the foundation does not update it at runtime. An update is a
reviewed source merge with application, migration, provider, and deployment verification.

## Pre-release truth [#pre-release-truth]

There is no supported previous commercial release yet. The mutable default branch and the current
uncommitted development checkout are not releases. Until the first commercial tag and release bundle
exist, a “previous-to-current upgrade” would be an engineering rehearsal, not a buyer update.

The first commercial tag establishes the baseline. Do not invent compatibility with planning
documents, historical branches, or the pre-release custom-auth schema that Better Auth replaced.

## Version and compatibility policy [#version-and-compatibility-policy]

Product release tags, not private workspace package versions, define the delivered foundation:

* patch: compatible defect, security, documentation, or provider fix;
* minor: backward-compatible capability or supported-path improvement;
* major: a change requiring buyer action to preserve an existing supported contract.

The first commercial release will establish `v1.0.0`. Before that tag, version numbers inside
private package manifests are implementation metadata and must not be presented as product releases.

Every tagged update must identify:

* supported toolchain and provider versions;
* changed product/architecture contracts;
* required environment additions, removals, or rotations;
* database migrations and whether rollback is safe;
* generated artifacts that must be refreshed;
* breaking or manual merge actions;
* exact verification and known limitations.

See the commercial update policy in `docs/commercial/updates.md`. The current tagged release is the
supported source baseline; the default branch is not an entitlement or stability promise.

## Responsibilities [#responsibilities]

The foundation maintainer must provide a tag, changelog, upgrade notes, migration impact,
verification commands, and a reproducible unmodified-reference comparison.

The buyer must:

* preserve a recoverable application and database backup;
* compare the exact release they started from with the exact target tag;
* disclose and review material local architecture departures;
* resolve application-specific conflicts;
* test migrations against disposable or staged data;
* verify provider dashboards, origins, cookies, webhooks, and environment;
* deploy with a rollback point and observe health/readiness.

An update entitlement does not include merging a customized application on the buyer's behalf.

## Prepare [#prepare]

Record a clean product state before merging:

```sh
git status --short
git rev-parse HEAD
git tag --points-at HEAD
node --version
bun --version
pnpm --version
vp --version
vp install --frozen-lockfile
vp check
vp run -r test
vp run -r build
```

Commit or deliberately set aside product work before upgrading. Never hide a dirty starting state in
an upgrade report.

Download the old and target foundation releases into separate read-only sibling directories. Run
`create-app` from each with the same app name, workspace namespace, and brand into two disposable
reference directories. The resulting reference diff removes seller-only files and normalizes
scope/brand changes before comparison.

Do not:

* rerun `create-app` into the non-empty product repository;
* copy the target release over the product;
* accept an unbounded search/replace;
* merge from the mutable default branch;
* apply migrations to production first.

## Review in dependency order [#review-in-dependency-order]

Classify the release diff before porting it:

1. toolchain pins, install policy, package manifests, and lockfile;
2. shared compiler/configuration policy;
3. migrations and persistence;
4. shared wire contracts;
5. server repositories, services, routes, auth, and provider adapters;
6. web services, modules, routing, PWA, and styles;
7. email source followed by regenerated HTML;
8. deployment, environment, CI, tests, and documentation.

Keep a short merge ledger: accepted release change, local conflict, resolution, command, and
remaining risk. A second agent should be able to explain every non-trivial resolution without the
author transcript.

## Migration and provider safety [#migration-and-provider-safety]

* Never edit, rename, reorder, or delete a migration already applied by the product.
* Apply new migrations to a disposable database, prove `up → down → up` when the down path is safe,
  and run domain integration tests.
* Treat destructive or irreversible data changes as explicit deployment decisions with backups and
  forward-recovery steps.
* Preserve webhook idempotency and ordering while provider adapters change.
* Add new environment values atomically; remove stale values from validators, examples, deployment,
  CI, and secrets management.
* Regenerate email HTML through the source command; do not resolve conflicts in generated templates
  by hand.

## Toolchain escape hatches [#toolchain-escape-hatches]

Run:

```sh
pnpm verify:toolchain
```

This exercises Bun server tests/build, package-native TypeScript checks, package-native web tests,
and an exact official Vite build without Vite+ orchestration. It is diagnostic, not a second
supported stack:

* if package-native commands pass while Vite+ fails, restore the last green Vite+ pin and diagnose
  the orchestrator;
* if TypeScript fails after an update, restore the release lockfile/pins and isolate the compiler or
  type change; do not disable type-aware checks;
* if Bun tests/build fail, restore the pinned Bun version; a Node API runtime is not a supported
  fallback;
* the official Vite diagnostic is online and exact-version pinned; it must not modify the lockfile.

Changing Bun, TypeScript, Vite, Vite+, Node, or pnpm is release engineering and requires a fresh
buyer, CI, PWA, Sentry, compiled-server, and deployment proof.

## Verify and deploy [#verify-and-deploy]

After conflicts are resolved:

```sh
vp install --frozen-lockfile
vp check
vp run -r typecheck
vp run -r test
vp run -r build
pnpm verify:toolchain
pnpm audit --audit-level high
git diff --check
```

Run DB-enabled integration tests and provider-specific tests for every affected boundary. Deploy to
a disposable or staging environment, then verify `/health`, `/ready`, authentication, paid access,
email, upload, PWA/offline behavior, and the application-specific critical path.

## Failure and rollback [#failure-and-rollback]

Stop when a migration, ownership check, auth flow, billing state, or provider contract is uncertain.
Do not weaken a test to complete the merge.

Application rollback means redeploying the last verified release-compatible build. Database rollback
is separate: use a reviewed down migration only when data effects are safe; otherwise forward-fix
from a backup-informed plan. Provider configuration and already-delivered webhooks may not roll back
with code.

Capture the old commit/tag, target tag, reference diff, conflict ledger, database proof, test
output, deployment identifier, and rollback point. Follow [Troubleshooting](./troubleshooting.md)
and use the support request contract when an unmodified reference also fails.


# Web UI patterns (/docs/web-ui)



# Web UI patterns [#web-ui-patterns]

The web application uses CSS Modules and semantic tokens rather than a utility framework. Reuse the
existing primitives when they fit; add product-specific composition in the owning module instead of
turning `components/ui` into a second product domain.

## Ownership [#ownership]

| Path                        | Responsibility                                                       |
| --------------------------- | -------------------------------------------------------------------- |
| `src/components/ui`         | reusable interaction and presentation primitives                     |
| `src/components/layout`     | application shell, navigation, responsive layout, and error surfaces |
| `src/modules/<domain>`      | route pages and product-specific composition                         |
| `src/services`              | typed Hono calls and TanStack Query lifecycle                        |
| `src/store`                 | deliberate client-only Zustand state                                 |
| `src/styles/_variables.css` | semantic color, spacing, typography, elevation, and motion tokens    |
| `src/configs/rpc-client.ts` | credentialed transport and stable application errors                 |

TanStack Query owns remote data. Do not copy API results into Zustand. Components do not call raw
`fetch`; service modules own query keys, mutations, invalidation, and error conversion.

## Included primitives [#included-primitives]

The current inventory includes buttons, inputs, forms, checkboxes, switches, selects, selectors,
tabs, menus, breadcrumbs, tooltips, modals, drawers, bottom sheets, cards, stat cards, tags, date
and date-range pickers, tables with pagination, filters, search, infinite-scroll triggers,
skeletons, spinners, empty states, toasts, avatars, identity cells, file icons, file cells, date
cells, area charts, uploads, async images, keyboard hints, truncated text, the command palette, and
a floating action button. The users module is the reference composition for a paginated data table
(`Table` + `createTableStore` + `useTableQueryParams` against a typed list endpoint); the files
module is the reference for table cell primitives (`FileCell` / `FileIcon` / `DateCell`) over an
owner-scoped list; the dashboard module is the reference for stat cards and charts. Zag-backed
popovers are composed inside the date controls and notification bell rather than exported as a
standalone primitive.

`FileIcon` resolves its glyph and category tint through `resolveFileType`, which maps a MIME type
onto a fixed category set and falls back to the filename extension. Extend that map when a new
upload type ships; do not branch on MIME strings inside a page. Its category tints are the one place
in the primitive library that sets colour outside the token palette, and each has a dark-mode value.

Zag.js-backed controls must retain their keyboard, focus, and ARIA behavior. A similar visual built
from generic `div` elements is not an equivalent replacement. Keep semantic labels and error
associations when composing form controls.

## Responsive and application states [#responsive-and-application-states]

Every material screen should deliberately handle:

* initial loading and mutation-pending states;
* empty data and disabled-provider states;
* recoverable and terminal errors;
* unauthenticated and unauthorized behavior;
* keyboard navigation, visible focus, and screen-reader labels;
* narrow viewport layout and touch targets;
* reduced motion and sufficient contrast;
* long names, translated-like expansion, and unbroken user content.

The settings shell has distinct desktop and mobile layouts. Tables include mobile card rendering;
test both representations when columns or filtering change.

## Localization [#localization]

The buyer SPA ships English, Spanish, and German through `i18next` and `react-i18next`. Locale
resources live in `apps/web/src/i18n/locales`, and the language switcher is available on both
authentication and authenticated application surfaces, with the same preference also exposed as a
language field in `/settings/profile`. The selected locale is stored in `app.locale`; otherwise the
browser language is used, with English as the fallback. The runtime updates the document `lang`
attribute when the locale changes.

Use `useTranslation` from `@/i18n` in React components and the exported `i18n` instance in service
callbacks that cannot use hooks. Use interpolation and plural keys for variable copy, and pass the
resolved locale into `Intl` or `date-fns` formatting. Do not assemble translated sentences from
fragments when word order can vary by language.

Add every new key to every locale file. The i18n test enforces key parity so a missing buyer-facing
translation fails the web test suite. API-provided notification bodies and provider error messages
remain server-owned content and are displayed verbatim.

The service worker fallback at `apps/web/public/offline.html` cannot import the React locale bundle,
so it carries the minimal offline copy for each supported locale and reads the same `app.locale`
preference. Update that static copy when adding a locale.

## Testing [#testing]

Vitest runs in a Node environment. Existing component contract tests use `react-dom/server` to prove
important static zero-key and disabled-provider states. They do not prove focus, pointer, portal,
measurement, animation, or browser navigation behavior.

Use the smallest useful layer:

```sh
vp run --filter @app/web test
pnpm test:e2e
vp run --filter @app/web build
```

Add a browser test when behavior depends on the DOM or user interaction. Keep static rendering tests
for deterministic copy and availability contracts, and preserve the production bundle budget.

## Branding [#branding]

Change identity and tokens through
[Customize branding and design tokens](./golden-paths/customize-branding-design-tokens.md). Do not
introduce Tailwind into the buyer SPA, embed the product name into every component, or manually edit
generated PWA assets.


# Worked feature recipe (/docs/worked-feature-bookmarks)



# Worked feature recipe: personal saved links [#worked-feature-recipe-personal-saved-links]

This example turns one bounded product request into a complete vertical slice. It is deliberately
personal: an authenticated user saves, lists, and deletes their own links. It does not add sharing,
organizations, roles, billing, uploads, background work, or a public API.

Use this as a concrete companion to [Build your first feature](./first-feature.mdx). The exact
product name and UI can change; the owner, boundaries, and proof cannot be silently dropped.

## Product contract [#product-contract]

| Situation                                   | Required result                                                                            |
| ------------------------------------------- | ------------------------------------------------------------------------------------------ |
| Signed-in owner saves a valid URL and label | A personal link appears in the owner's list.                                               |
| Same owner saves the same URL twice         | The application returns a stable conflict without a duplicate row.                         |
| Anonymous caller                            | The route returns `401`; the browser sends the user to sign in.                            |
| Another user reads or deletes a link ID     | The application returns the chosen non-disclosing missing result and changes no row.       |
| New user opens the page                     | The page has a clear empty state and an accessible way to add the first link.              |
| Save request is pending or fails            | The form communicates progress and a useful recovery action without losing its safe input. |

The public fields are `bookmark_id`, `url`, `label`, and `created_at`. `user_id` is persistence and
authorization data, not client-controlled input.

## 1. Design the schema around ownership [#1-design-the-schema-around-ownership]

Create a new post-release dbmate migration. A suitable table has a generated UUID primary key, a
non-null `user_id UUID REFERENCES users(id) ON DELETE CASCADE`, a bounded URL and label, a creation
timestamp, a unique `(user_id, url)` constraint, and an index supporting the owner's ordered list.

The important behavior is expressed by the database:

```sql
CREATE TABLE bookmarks (
    bookmark_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
    url TEXT NOT NULL CHECK (length(url) <= 2048),
    label TEXT NOT NULL CHECK (length(trim(label)) BETWEEN 1 AND 120),
    created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    UNIQUE (user_id, url)
);

CREATE INDEX bookmarks_user_created_at_idx
    ON bookmarks (user_id, created_at DESC);
```

The migration's down section drops only `bookmarks` and its dependent index. Before release, prove
fresh `up → down → up` on a named disposable database. Follow
[Add a database migration](./golden-paths/add-database-migration.md) for reversible migration and
deployment rules.

## 2. Define one wire contract [#2-define-one-wire-contract]

Create `packages/contracts/src/bookmarks.ts` and export it from the contracts package index. The
create request accepts only a trimmed URL and label; it does not accept `user_id`, `bookmark_id`, or
an entitlement claim. The list response is a public, mapped bookmark record rather than a raw
database row.

Validate the request once with Zod, then infer its TypeScript type from that schema. Add contract
tests for a valid HTTPS URL, an invalid URL, blank/overlong labels, and unknown input keys. This
gives both the route and browser client the same public shape.

## 3. Keep the server layers narrow [#3-keep-the-server-layers-narrow]

Create `apps/server/src/domains/bookmarks/` with `repository.ts`, `service.ts`, `routes.ts`, and an
internal `types.ts` only when an internal row type is needed.

The repository receives both `bookmarkId` and current `userId` for detail or delete operations. Its
SQL owns the authorization predicate:

```sql
DELETE FROM bookmarks
WHERE bookmark_id = $1
  AND user_id = $2
RETURNING bookmark_id;
```

The service translates no returned row into the deliberate private-resource not-found result. It
also translates the unique-constraint conflict into a stable application error. The route mounts
`sessionAuth`, obtains identity with `requireUserId(c)`, validates with the shared Zod schema, and
maps the result to the standard response helper. It contains no SQL and never reads `user_id` from
the request.

Mount the route once in `apps/server/src/app.ts`, then run the server and contracts typechecks. The
typed Hono client should expose the route without a manually duplicated browser interface.

## 4. Add the browser feature [#4-add-the-browser-feature]

Create `apps/web/src/services/bookmarks.service.ts` with one query-key factory, a paginated list
query, a create mutation, and a delete mutation. Each mutation invalidates only the affected
bookmark list or detail key.

Create `apps/web/src/modules/bookmarks/` with an accessible form and list. Add a lazy `/saved-links`
route and navigation only after the page exists. The page must cover:

* a labeled URL and label form with invalid-input feedback;
* pending save/delete affordances that prevent accidental double-submit;
* empty, loading, error, and populated list states;
* keyboard operation, visible focus, reduced motion, and a narrow viewport;
* a clear outcome after duplicate-save conflict and after delete.

The page calls its service hook, not `fetch`, and it does not treat the browser as the authority for
the current user or access policy.

## 5. Prove the behavior at the right boundaries [#5-prove-the-behavior-at-the-right-boundaries]

| Layer                | Proof                                                                                                                                 |
| -------------------- | ------------------------------------------------------------------------------------------------------------------------------------- |
| Contract             | Request parser accepts a valid link and rejects invalid, blank, oversized, or unknown fields.                                         |
| Migration/repository | The owner can list/delete; another user cannot read or delete; the uniqueness constraint holds under a second insert.                 |
| HTTP/auth            | No session returns `401`; malformed input returns `400`; another user gets the non-disclosing result; no cross-owner mutation occurs. |
| Browser              | A signed-in user creates and sees a link, receives the empty state before creation, and can delete it.                                |
| Release              | Migration reversal, database-enabled tests, E2E, root checks, builds, and a custom generated buyer pass.                              |

Add the browser behavior to `e2e/bookmarks.spec.ts` using accessible labels and roles. See
[End-to-end testing](./e2e-testing.md) for the disposable stack and failure artifacts.

## 6. Decide what comes next [#6-decide-what-comes-next]

Sharing a bookmark, adding a team workspace, importing bookmarks asynchronously, charging for a
limit, attaching preview images, or exposing a public integration are separate product decisions.
They change ownership, failure recovery, cost, and security boundaries. Do not add them as an
unreviewed extension of this small personal domain.

Use the [domain golden path](./golden-paths/add-domain-end-to-end.md) as the full change checklist
and [Go live](./go-live.md) before accepting real users.


# Golden paths overview (/docs/golden-paths/README)



# Golden paths [#golden-paths]

Golden paths are optional, maintained playbooks for common changes. They provide a tested route for
builders and agents that want one, but they are not required file-by-file procedures. A different
implementation approach is valid when it preserves the repository's architecture and security
invariants and passes outcome-level verification.

`AGENTS.md`, executed code, migrations, configuration, and tests remain authoritative. A golden path
supplements those sources; it does not override them or make a particular skill, MCP client, or
coding agent mandatory.

## Foundation runbooks [#foundation-runbooks]

* [Add a domain end to end](./add-domain-end-to-end.md)
* [Add a database migration](./add-database-migration.md)
* [Add an environment variable](./add-environment-variable.md)
* [Diagnose or recover a failed migration](./diagnose-failed-migration.md)
* [Add a billing-gated feature](./add-billing-gated-feature.md)
* [Add a billing plan](./add-billing-plan.md)
* [Diagnose a failed billing webhook](./diagnose-failed-webhook.md)
* [Deploy a fresh application](./deploy-fresh-application.md)
* [Add an authenticated route](./add-authenticated-route.md)
* [Add a transactional email](./add-transactional-email.md)
* [Add an upload type](./add-upload-type.md)
* [Customize branding and design tokens](./customize-branding-design-tokens.md)
* [Remove an optional subsystem](./remove-optional-subsystem.md)
* [Decide a background-work request](./decide-background-work.md)

Each runbook defines when it is useful, the files and boundaries commonly involved, a recommended
procedure, verification, security constraints, failure modes, rollback or diagnosis, acceptance
criteria, and unsafe actions. A structural test in `packages/create-app/src/golden-paths.test.ts`
prevents those sections from silently disappearing from buyer output.

## Completion rule [#completion-rule]

A runbook is not verified because its Markdown exists. Rehearse it from a clean generated buyer
application, record every incorrect assumption or intervention, and fix the architecture,
instructions, or verification harness that caused the failure. A product change is accepted because
its behavior and required checks pass, not because the author followed a runbook.


# Add an authenticated route (/docs/golden-paths/add-authenticated-route)



# Add an authenticated route [#add-an-authenticated-route]

## When to use [#when-to-use]

Use this path when API or web behavior requires a signed-in application user or access to a
user-owned resource. It applies to read and write routes backed by Better Auth sessions.

Authentication answers who the caller is. Authorization separately answers whether that user may act
on the resource. A successful session must never imply access to every row.

## Files and boundaries [#files-and-boundaries]

* `apps/server/src/lib/auth.ts`: Better Auth configuration and application-user hooks.
* `apps/server/src/infra/http/middlewares/auth.ts`: session resolution and `requireUserId`.
* `apps/server/src/domains/<domain>/routes.ts`: middleware order, validation, and HTTP response.
* `apps/server/src/domains/<domain>/service.ts`: use-case and authorization policy coordination.
* `apps/server/src/domains/<domain>/repository.ts`: SQL with ownership predicates.
* `packages/contracts/src/`: public Zod request schemas and response types.
* `apps/web/src/configs/rpc-client.ts`: typed client, cookie transport, stable error conversion.
* `apps/web/src/services/`: TanStack Query hooks and cache ownership.
* `apps/server/src/lib/auth.integration.test.ts`: real-session and protected-resource reference
  proof.

Better Auth is the only session authority. The canonical stored application identity is `users.id`;
domain and wire code may call the same value `user_id`.

## Procedure [#procedure]

1. Define or extend the request schema once in `packages/contracts`; export it through the package
   index. Do not accept an unvalidated body, query, or resource identifier.

2. Add repository methods whose signatures require the current `userId`. Put the ownership predicate
   in the same SQL statement that reads or mutates the row:

   ```sql
   WHERE resource_id = ${resourceId}
     AND user_id = ${userId}
   ```

   Never fetch by resource ID and perform a later in-memory owner comparison for a write.

3. Let the service translate a missing owned row into the deliberate public policy:
   * use `404 NOT_FOUND` for private resources when confirming existence would disclose another
     user's data;
   * use `403 FORBIDDEN` only when the resource is already visible and the caller lacks a known
     action or role.

4. Mount `sessionAuth` before the protected handlers. In each handler call `requireUserId(c)` and
   pass that value through service and repository layers:

   ```ts
   export const resourceRoutes = new Hono<AppEnv>()
     .use(sessionAuth)
     .get('/:resource_id', async c => {
       const userId = requireUserId(c)
       const resource = await service.getResource(userId, c.req.param('resource_id'))
       return sendSuccess(c, 'Resource fetched', resource)
     })
   ```

5. The foundation has no generic resource-authorization middleware. Keep private-resource ownership
   in the repository predicate. If a visible resource needs a reusable role or action policy,
   extract a domain-owned checker only after its resource model and disclosure behavior are defined.

6. Mount the domain router in `apps/server/src/app.ts`. Do not put SQL, cookie parsing, or Better
   Auth internals in the route.

7. In the web app, call the route through the generated `api` client and `rpc`/`rpcPaginated`. These
   preserve Hono inference, include the Better Auth HttpOnly cookie, redirect non-auth 401 responses
   to login, and throw `ApiClientError` with the stable server code.

8. Give every query/mutation a resource-scoped TanStack Query key. Invalidate only affected
   resources after writes. Do not cache authenticated API responses in the service worker.

9. Add real Postgres HTTP proof for:
   * no cookie → `401 UNAUTHORIZED`;
   * expired or revoked session → `401 UNAUTHORIZED`;
   * current owner → expected success contract;
   * authenticated wrong owner → deliberate `404` or `403` policy with no protected data;
   * cross-owner update/delete → no row mutation;
   * malformed input → `400` before service work.

10. Run the architecture scan and the full verification path. Inspect the diff for any token,
    cookie, JWT, owner ID from input, or raw `fetch` shortcut.

## Verification [#verification]

Use a disposable Postgres 18 database with the baseline migration applied:

```sh
RUN_DB_INTEGRATION_TESTS=1 \
  DATABASE_URL='postgres://localhost:5432/app_auth_route_proof?sslmode=disable' \
  bun test --cwd apps/server src/lib/auth.integration.test.ts
bun test --cwd packages/create-app src/auth-contract.test.ts
vp check
pnpm test
vp run -r build
```

The reference integration creates a real verified Better Auth session, reads an owned notification,
rejects a request with no session, hides another user's notification, expires a stored session, and
requires the expired cookie to fail.

## Security constraints [#security-constraints]

* The caller identity comes only from `auth.api.getSession` and server context, never request JSON,
  query, URL owner fields, local storage, or an unverified JWT.
* Apply the ownership predicate to the database read/write itself. Test that an unauthorized write
  changes zero rows.
* Keep Better Auth cookies HttpOnly and sent with `credentials: 'include'`; do not copy tokens into
  application state.
* Preserve trusted-origin, secure-cookie, redirect, proxy-header, verification, and rate-limit
  behavior.
* Prefer non-disclosing `404` for private resources. Never include the real owner or protected row
  in the error.
* Log request IDs and stable error codes, not session cookies, tokens, passwords, or private row
  contents.

## Failure modes [#failure-modes]

* Mounting a handler before `sessionAuth`, or using optional auth for a required route.
* Accepting `user_id` from the client and using it instead of `requireUserId(c)`.
* Reading by resource ID, then authorizing later, especially before a mutation.
* Returning `403` or owner metadata in a way that confirms a private resource exists.
* Treating a valid session as product entitlement or resource ownership.
* Adding a custom JWT/session refresh path beside Better Auth.
* Using raw browser `fetch` without credentials or stable error handling.
* Redirecting a Better Auth endpoint's own 401 back to login and creating a loop.
* Testing middleware with a fake context while never exercising real session expiry and Postgres.

## Rollback and diagnosis [#rollback-and-diagnosis]

If a route leaks or mutates cross-owner data, disable or unmount that route before attempting a
cosmetic client fix. Preserve request IDs, identify the exact repository predicate, and test the
affected read and write with two isolated users.

For unexplained `401`, inspect cookie presence, exact web/API origins, CORS credentials, trusted
origin configuration, session row expiry/revocation, and proxy header ownership. Do not weaken
cookie or origin policy to make the request pass.

For an unexpected `404`/`403`, verify the session's canonical `users.id`, active user status,
resource owner/membership row, and the chosen disclosure policy. Do not add a second user key or
fall back to email matching.

## Acceptance criteria [#acceptance-criteria]

* Shared contracts, route validation, service policy, repository ownership, and typed web client
  agree on one request/response shape.
* No cookie and expired/revoked session return `401`.
* The owner succeeds; the wrong owner receives the deliberate non-disclosing `404` or visible
  resource `403` and no protected data.
* Cross-owner writes mutate zero rows.
* The client includes credentials, preserves typed errors, and does not service-worker cache the
  response.
* Real Postgres auth integration, architecture scan, `vp check`, tests, and builds pass.
* No parallel JWT, cookie, session table, user key, or client-supplied owner trust is introduced.

## Agent prohibitions [#agent-prohibitions]

* Do not add custom JWTs, refresh tokens, session cookies, password logic, or auth tables.
* Do not trust `user_id`, owner, role, entitlement, or email supplied by the browser.
* Do not authorize after a write or outside the repository predicate.
* Do not expose another user's resource existence, identity, or data in errors or logs.
* Do not bypass the generated Hono client with unauthenticated raw `fetch`.
* Do not weaken CORS, cookie security, trusted origins, verification, redirects, proxy handling, or
  rate limits.
* Do not call a mocked middleware test complete without a real expired-session and cross-owner
  Postgres HTTP test.


# Add a billing-gated feature (/docs/golden-paths/add-billing-gated-feature)



# Golden path: add a billing-gated feature [#golden-path-add-a-billing-gated-feature]

## When to use [#when-to-use]

Use this path when an authenticated product capability must require either an active application
trial or a paid entitlement. First decide whether the entire domain, a mutation, or only an
expensive operation is paid. Do not gate authentication, account recovery, billing status,
checkout/portal recovery, health, or provider webhooks.

The server is authoritative. A hidden button, private React route, or cached `user.is_premium` flag
alone is not a billing gate.

## Files and boundaries [#files-and-boundaries]

* `apps/server/src/domains/billing/entitlements.ts`: the only subscription-status-to-access policy.
* `apps/server/src/domains/billing/service.ts`: combines paid entitlement with the application
  trial.
* `apps/server/src/infra/http/middlewares/trial.ts`: reusable API enforcement after `sessionAuth`.
* `apps/server/src/domains/<domain>/routes.ts`: mounts authentication and entitlement middleware.
* domain service/repository code: product behavior; it must not import a provider SDK or trust a
  client plan.
* `apps/web/src/hooks/use-trial.ts` and protected UI: explanation and navigation, not authority.
* HTTP/integration tests: allowed trial/paid access plus denied/repair behavior.

Provider SDK types stay in `packages/billing`. Application domains depend on an entitlement result,
never a provider customer, product, or webhook object.

## Procedure [#procedure]

1. State the paid job and the denial boundary. Decide whether reads, writes, exports, compute, or
   the whole domain are gated. Keep enough account and billing UI reachable to recover payment.

2. Reuse the current policy unless the commercial decision itself is changing:
   * `active` and provider `trialing` grant paid access;
   * canceled subscriptions grant access strictly before `current_period_end`;
   * `past_due`, `unpaid`, `paused`, `incomplete`, and expired cancellations do not;
   * the separate application trial may still grant access.

3. For a whole domain, mount middleware in this order:

   ```ts
   new Hono<AppEnv>().use(sessionAuth).use(requireActiveTrialOrPremium)
   ```

   For one operation, run the same server-side access check immediately after authentication and
   before expensive work or mutation. Do not introduce a second status list.

4. Leave `/billing/status`, checkout, portal recovery, auth, health, and signed webhooks outside the
   paid gate. A past-due subscription must reach the portal and must not start a duplicate checkout.

5. In the web app, use the billing-status query to explain the state, disable pending actions, and
   route denied users to billing. Treat this as usability and defense-in-depth.

6. Add tests proving:
   * an authenticated active application trial succeeds;
   * an expired trial without paid entitlement receives 403;
   * active/provider-trial and canceled-with-period access succeed;
   * past-due and expired cancellation fail;
   * unauthenticated requests still receive the authentication contract;
   * the billing recovery route remains usable.

7. Run the change in a custom generated buyer and record any manual clarification the agent needed.

## Verification [#verification]

```sh
vp check
vp run --filter @app/server typecheck
bun test --cwd apps/server src/domains/billing/entitlements.test.ts
RUN_DB_INTEGRATION_TESTS=1 \
  DATABASE_URL='postgres://localhost:5432/app_billing_gate_proof?sslmode=disable' \
  bun test --cwd apps/server src/infra/http/middlewares/trial.integration.test.ts
vp run -r test
vp run -r build
git diff --check
```

Also call the protected API directly rather than relying on the browser. Verify the denial happens
before a write, upload URL, paid provider call, or expensive computation.

## Security constraints [#security-constraints]

* Resolve user identity only from the Better Auth session.
* Resolve entitlement only from server-owned subscription and user records.
* Never accept `is_premium`, plan, status, expiry, owner, or trial days from request input.
* Keep billing recovery available to denied users.
* Deny before side effects and keep the denial response free of provider/customer secrets.
* Do not cache a grant beyond a lifecycle update unless expiry and invalidation are proven.

## Failure modes [#failure-modes]

* The UI redirects but direct API calls still succeed.
* Middleware is mounted before session authentication and sees no user.
* A copied status array grants `past_due` while the central policy denies it.
* The whole API is gated, preventing payment recovery or logout.
* A route performs storage/provider work before checking access.
* `users.is_premium` drifts and becomes the only authority.
* A canceled customer loses prepaid access early or keeps it at the exact period boundary.

## Rollback and diagnosis [#rollback-and-diagnosis]

If an entitled user is denied, inspect the local subscription, `provider_modified_at`,
`current_period_end`, and the application-trial end before changing policy. Replay the signed
provider event only after checking the event ledger. If direct API access succeeds while the UI
denies it, add/fix the server middleware; do not weaken the web guard.

To remove a gate, delete the middleware at the intended domain/operation and its product-specific
copy, but retain auth, ownership, validation, rate limits, and the central billing tests.

## Acceptance criteria [#acceptance-criteria]

* The paid job and recovery paths are explicit.
* The API denies expired/unentitled access before side effects.
* The browser communicates the same state without being the authority.
* Every lifecycle status and cancellation boundary follows Decision 0003.
* Authenticated, unauthenticated, entitled, expired, past-due, and recovery cases are tested.
* Root checks and a generated-buyer execution pass with interventions recorded.

## Agent prohibitions [#agent-prohibitions]

* Do not authorize billing in React alone.
* Do not trust client billing claims or provider payloads outside the signed webhook adapter.
* Do not duplicate the entitlement status table in a domain.
* Do not gate auth, billing recovery, health, or webhook endpoints.
* Do not add an implicit past-due grace period.
* Do not call the path complete without a direct API denial test.


# Add a billing plan (/docs/golden-paths/add-billing-plan)



# Golden path: add a billing plan [#golden-path-add-a-billing-plan]

## When to use [#when-to-use]

Use this path when the product offer has a validated new billing interval or package that must be
selectable at checkout. A coupon, temporary launch price, feature flag, usage meter, seat quantity,
or provider-side price replacement is not automatically a new application plan.

Adding a plan changes a commercial contract, shared wire contract, environment contract, webhook
mapping, checkout, and buyer UI. Do it only with an approved offer and provider product.

## Files and boundaries [#files-and-boundaries]

* `packages/contracts/src/billing.ts`: canonical `PlanTypeSchema`, checkout input, and public plan
  response.
* `apps/server/src/domains/billing/types.ts`: provider-neutral persisted/internal plan type.
* `apps/server/src/config/env.ts` and `.env.example`: product ID and display price validation.
* `apps/server/src/domains/billing/service.ts`: plan labels, product mapping, checkout, and public
  configs.
* `apps/web/src/modules/settings/pages/billing/BillingSettings.tsx`: description and interval-aware
  presentation.
* `packages/billing/src/providers/<selected>.ts`: SDK normalization only; no product offer
  decisions.
* migration only when the database has a real enum/constraint that must change. The current plan
  column is text, so do not create a no-op migration.
* contracts, environment, service/integration, and UI tests.

## Procedure [#procedure]

1. Record offer name, buyer, interval, provider product ID owner, price display source, cancellation
   behavior, upgrade/downgrade behavior, and whether existing customers may select it.
2. Create the product/price in the selected provider's sandbox/test mode. Never commit its token or
   webhook secret. Treat the product ID as deployment configuration even though it is not a
   credential.
3. Add the plan once to `PlanTypeSchema` and derive/import the shared type where possible. Update
   the server internal union only where persistence requires it; do not create provider-named wire
   types.
4. Add one product-ID and display-price variable at the validated config boundary. Billing remains
   fully optional when every selected-provider value is unset. Once configured, all credentials and
   at least one product are required, prices are positive, and product/price IDs are unique.
5. Extend `getProductIdForPlan`, reverse product mapping, label, public config, and interval. A
   signed webhook with an unknown product or mismatched `plan_type` metadata must fail and roll back
   its event claim; never default it to another paid plan.
6. Update the billing UI's exhaustive plan description and price formatting. Render from
   `/billing/plans`; do not embed a provider product ID or treat a displayed number as the amount
   charged.
7. Ensure checkout sends `user_id`/external customer identity and `plan_type` metadata. Keep
   provider payloads inside `packages/billing`.
8. Test valid checkout input, invalid plan input, disabled billing, partial config, duplicate IDs,
   plan/product mismatch, and the new public plan response. Use the selected provider's sandbox/test
   mode for the final checkout-to-webhook smoke test.
9. Update commercial copy and support policy only after the sandbox lifecycle succeeds.

## Verification [#verification]

```sh
vp check
vp run --filter @app/contracts test
vp run --filter @app/server typecheck
bun test --cwd apps/server src/config/env.test.ts src/domains/billing
vp run --filter @app/web typecheck
vp run -r test
vp run -r build
git diff --check
```

In the selected provider's sandbox/test mode, verify checkout creation, redirect, one signed
lifecycle webhook, local plan mapping, billing status, portal recovery, cancellation, and a replay
of the same event ID. Verify a custom generated buyer with no selected-provider values still boots
locally.

## Security constraints [#security-constraints]

* Never put access tokens or webhook secrets in source, web variables, fixtures, command logs, or
  issue comments.
* Do not accept a product ID, price, status, or entitlement from checkout request JSON.
* Validate the requested plan with shared Zod and map it to server configuration.
* Require signed provider events and transactional idempotency before changing local state.
* Reject unknown or contradictory product mappings; silent fallback can grant the wrong product.
* Keep displayed prices clearly separate from provider-charged amounts.

## Failure modes [#failure-modes]

* Contracts accept the plan but server mapping or UI exhaustive records omit it.
* Two plan names point to the same provider product.
* A partially configured deployment boots and fails only after a customer clicks checkout.
* The UI displays a local price that differs from the selected provider.
* An unknown webhook product silently becomes `monthly`.
* The plan is added directly to one provider adapter, coupling product policy to one SDK.
* Tests use a real production token or generate a billable purchase.

## Rollback and diagnosis [#rollback-and-diagnosis]

Disable selection in application code before archiving a provider product. Existing customer
webhooks still need a resolvable mapping; after launch, removal requires an explicit migration and
support policy rather than deleting the environment variable. Inspect the event ledger, provider
timestamp, metadata plan, and configured reverse mapping when diagnosis reports an unknown product.

Before the first commercial tag, a rejected offer may be removed cleanly because there are no live
installations. After that tag, preserve valid historical plan values or migrate them deliberately.

## Acceptance criteria [#acceptance-criteria]

* The commercial offer and lifecycle behavior are approved.
* Shared input, server mapping, environment validation, webhook mapping, and UI are exhaustive.
* Disabled, partial, duplicate, unknown, mismatched, and successful configurations are tested.
* Selected-provider sandbox/test checkout through signed webhook and replay succeeds.
* Zero-key generated-buyer startup remains valid.
* Root verification and a generated-buyer execution pass with interventions recorded.

## Agent prohibitions [#agent-prohibitions]

* Do not invent a plan to increase feature count.
* Do not trust client-supplied price or product ID.
* Do not default an unknown provider product to an existing plan.
* Do not duplicate provider payload types in public contracts.
* Do not change or combine adapters as part of adding a plan.
* Do not claim completion from a rendered pricing card without checkout/webhook proof.


# Add a database migration (/docs/golden-paths/add-database-migration)



# Golden path: add a database migration [#golden-path-add-a-database-migration]

## When to use [#when-to-use]

Use this path for a durable Postgres schema, constraint, index, enum, or data-shape change required
by application behavior. Do not create a migration for an application-only refactor.

Before the first commercial release tag, the single baseline may be corrected only under
`docs/decisions/0002-pre-release-better-auth-schema-baseline.md`. After that tag, shipped files are
immutable and every change uses a new timestamped dbmate migration.

## Files and boundaries [#files-and-boundaries]

* `apps/server/migrations/*.sql`: dbmate-formatted schema history and the only migration authority.
* `apps/server/src/domains/*/repository.ts`: application SQL that consumes the schema.
* `apps/server/src/domains/*/*.integration.test.ts`: real Postgres behavior.
* `packages/contracts`: wire-shape changes only; database rows are not public contracts by default.
* `.github/workflows/ci.yml`: full-chain reversal and DB-enabled verification.
* `docs/decisions`: required when the change establishes a lasting support or upgrade policy.

Better Auth CLI output may be inspected to discover required columns, but it must be translated into
a reviewed dbmate migration. Routes do not contain SQL, and migrations do not hide application
backfills in startup code.

## Procedure [#procedure]

1. Read the applicable `AGENTS.md`, the current schema, repository queries, integration tests, and
   recent migration history. Confirm whether the first commercial release has been tagged.

2. From `apps/server`, create a new file after release:

   ```sh
   pnpm db:new add_resource_owner
   ```

3. Write both `-- migrate:up` and `-- migrate:down`. Prefer transactional DDL. Put foreign keys,
   uniqueness, checks, and ownership rules in the database when they are invariants.

4. For a data migration, define the affected row count, batching/restart behavior, lock impact, and
   compatibility window. Separate a large backfill from a blocking constraint when needed.

5. Update repository queries, types, contracts, and tests in the same change. Keep old and new
   application versions compatible during a rolling deployment, or state why the supported
   one-replica deployment makes an ordered stop/migrate/start safe.

6. Start Postgres and create a disposable, explicitly named proof database:

   ```sh
   docker compose up -d postgres
   dropdb --if-exists --force app_migration_proof
   createdb app_migration_proof
   ```

7. Prove the complete fresh chain, the new down section, and reapplication:

   ```sh
   DATABASE_URL='postgres://localhost:5432/app_migration_proof?sslmode=disable' \
     dbmate --migrations-dir apps/server/migrations --no-dump-schema up
   DATABASE_URL='postgres://localhost:5432/app_migration_proof?sslmode=disable' \
     dbmate --migrations-dir apps/server/migrations --no-dump-schema down
   DATABASE_URL='postgres://localhost:5432/app_migration_proof?sslmode=disable' \
     dbmate --migrations-dir apps/server/migrations --no-dump-schema up
   ```

   One `down` reverses one migration. CI loops over the migration count when it verifies an entire
   multi-file history.

8. Assert the resulting columns, constraints, indexes, and absence of removed objects with `psql` or
   an integration test. Do not treat `dbmate` exit zero as proof that the application can use the
   result.

9. Run the DB-enabled server suite and repository-wide verification.

10. Remove the exact disposable database when finished:

    ```sh
    dropdb --if-exists --force app_migration_proof
    ```

## Verification [#verification]

```sh
vp check
vp run --filter @app/server typecheck
RUN_DB_INTEGRATION_TESTS=1 \
  DATABASE_URL='postgres://localhost:5432/app_migration_proof?sslmode=disable' \
  bun test --cwd apps/server
vp run -r test
vp run -r build
```

For buyer-facing changes, also generate a custom-scope/custom-brand application and repeat frozen
install, migration `up → down → up`, DB tests, and its compiled-server smoke from the generated
repository root.

## Security constraints [#security-constraints]

* Never paste production URLs, credentials, customer rows, tokens, or database dumps into commands,
  tests, documentation, logs, or issue comments.
* Use a disposable database whose exact name you control. Never point `down`, `dropdb`, destructive
  fixtures, or manual repair SQL at a shared or production database.
* Review foreign-key delete behavior, uniqueness under concurrency, tenant/resource ownership,
  defaults, nullability, and index/lock cost.
* Backups and a tested restore path are prerequisites for destructive production DDL; a `down`
  section is not a backup.

## Failure modes [#failure-modes]

* The migration works on an existing developer database but fails from empty because it relies on
  untracked manual schema.
* `down` succeeds syntactically but loses data or cannot restore the old application contract.
* A new non-null column fails on existing rows or creates a long table rewrite/lock.
* Application queries deploy before the required schema or stop working after rollback.
* A migration version is edited after release, so buyer databases with the same recorded version
  have different schemas.
* Better Auth or another tool applies an untracked second migration history.

## Rollback and diagnosis [#rollback-and-diagnosis]

If verification fails, stop and follow
[Diagnose or recover a failed migration](./diagnose-failed-migration.md). Determine whether the
failure is connection, lock, transactional DDL, or application compatibility before changing SQL.
Use `down` only when its data and compatibility effects are understood; otherwise ship a forward
repair.

## Acceptance criteria [#acceptance-criteria]

* A new post-release timestamped migration exists, or the recorded pre-release baseline exception
  applies.
* Fresh `up`, one `down`, and re-`up` pass on an exact disposable database.
* Schema assertions and DB integration tests prove the application invariant.
* Old/new application compatibility and rollback behavior are explicit.
* Root checks, tests, builds, and generated-buyer verification pass.
* No secret, production URL, customer data, or untracked manual schema is introduced.

## Agent prohibitions [#agent-prohibitions]

* Do not edit or reorder a migration included in a commercial release.
* Do not delete or forge `schema_migrations` rows to make a deployment look green.
* Do not run destructive verification against an ambient `DATABASE_URL`.
* Do not add schema mutation to application startup outside the reviewed migration runner.
* Do not use the Better Auth CLI or an ORM as a second schema authority.
* Do not mark the path complete from SQL review alone.


# Add a domain end to end (/docs/golden-paths/add-domain-end-to-end)



# Golden path: add a domain end to end [#golden-path-add-a-domain-end-to-end]

## When to use [#when-to-use]

Use this path when a product concept needs a new durable resource or business capability across
Postgres, the Hono API, shared contracts, the typed web client, and the React UI. Use a smaller path
for a presentation-only view, an application-only refactor, or a field on an existing resource.

Before editing, write the resource owner, allowed actors, lifecycle, invariants, public fields,
failure semantics, and whether unauthorized callers may learn that a record exists. Avoid adding
organizations, roles, collaboration, billing, uploads, or background work unless the PRD requires
and verifies them.

## Files and boundaries [#files-and-boundaries]

* `apps/server/migrations/*.sql`: durable schema, foreign keys, constraints, indexes, and reversal.
* `packages/contracts/src/<domain>.ts` and `index.ts`: Zod request/query schemas and public response
  types; the server and web must not redeclare them.
* `apps/server/src/domains/<domain>/repository.ts`: SQL and database row mapping only.
* `apps/server/src/domains/<domain>/service.ts`: ownership-aware business rules, transactions, and
  stable domain errors.
* `apps/server/src/domains/<domain>/routes.ts`: authentication, validation, HTTP mapping, and status
  codes; no SQL.
* `apps/server/src/domains/<domain>/types.ts`: internal types only when they are not wire contracts.
* `apps/server/src/app.ts`: one mounted domain route.
* `apps/web/src/services/<domain>.service.ts`: typed Hono calls, React Query keys, mutations, and
  precise invalidation.
* `apps/web/src/modules/<domain>/`: accessible loading, empty, error, success, and mutation UI.
* `apps/web/src/AppRouter.tsx`, `utils/routes.ts`, and navigation only when the domain has a page.
* contract, repository/service, HTTP/auth, web utility/component, and route tests.

Routes translate HTTP. Services own business decisions. Repositories own SQL. Shared contracts own
wire shapes. UI components do not call `fetch` or duplicate the server cache outside TanStack Query.

## Procedure [#procedure]

1. Write a short acceptance table covering authenticated owner success, invalid input,
   unauthenticated access, cross-owner access, missing record, conflict/idempotency where relevant,
   loading, empty, error, and narrow/keyboard UI behavior.
2. Add request and query Zod schemas plus public response types in `packages/contracts`. Parse
   representative valid/invalid fixtures. Do not expose a database row merely because it exists.
3. Follow [Add a database migration](./add-database-migration.md). Use `users(id)` for personal
   ownership, define delete behavior deliberately, and index every ownership/list predicate. A
   unique invariant belongs in Postgres, not only in the UI.
4. Implement repository functions with explicit inputs and mapped outputs. Every read/update/delete
   of a user-owned record includes the owner predicate in the SQL statement; do not fetch globally
   and authorize afterward.
5. Implement services that express product behavior and translate absent/forbidden resources into
   stable application errors. Prefer the same not-found result for a missing record and another user
   record when revealing existence is unnecessary.
6. Implement routes with `sessionAuth`, `requireUserId`, and `sValidator`. Pass the authenticated
   identity into the service; never accept owner identity from JSON, query, headers, or URL params.
   Use `sendSuccess`, `sendPaginated`, or `sendOk` consistently.
7. Mount the route once in `apps/server/src/app.ts`. Run the server and contracts typechecks before
   writing the web service so Hono RPC exposes the new route accurately.
8. Add one service module in the web app. Define stable query-key factories, use `api` with `rpc` or
   `rpcPaginated`, pass inferred contract values, and invalidate the narrowest list/detail keys
   after mutations.
9. Add a lazy route and page module. Preserve semantic form labels, keyboard operation, focus,
   actionable errors, loading state, empty state, mutation pending state, and narrow viewport
   behavior. Do not add a second state or styling system.
10. Add tests at the lowest valuable layers:
    * contract parsing and transforms;
    * real Postgres ownership and constraints;
    * authenticated HTTP success, invalid input, no session, and cross-owner denial;
    * query-key/route utilities and meaningful UI behavior.
11. Prove the new migration from empty with `up → down → up`, run the DB-enabled tests, then run the
    full repository checks and build.
12. Generate a custom-scope/custom-brand buyer, repeat the change or replay its commit, and execute
    the evaluation harness from its clean tagged start. Record every intervention and architecture
    violation.

## Verification [#verification]

From repository root, with an exact disposable database:

```sh
vp check
vp run --filter @app/contracts test
vp run --filter @app/server typecheck
vp run --filter @app/web typecheck
RUN_DB_INTEGRATION_TESTS=1 \
  DATABASE_URL='postgres://localhost:5432/app_domain_proof?sslmode=disable' \
  bun test --cwd apps/server src/domains/<domain>
vp run -r test
vp run -r build
git diff --check
```

Also assert that:

* route files do not import `bun:sql` or `infra/db`;
* application code outside `apps/server/src/config/env.ts` does not add `process.env` reads;
* existing released migrations are unchanged;
* the web imports request/response types from shared contracts or infers them from Hono RPC;
* unauthenticated calls return 401 and cross-owner detail/update/delete cannot reveal or mutate
  data;
* a second clean install can understand the changed boundary without the author transcript.

Use `docs/evaluations/fixtures/domain-golden-path.json` for the representative bookmarks proof.

## Security constraints [#security-constraints]

* Resolve identity exclusively from the Better Auth session.
* Scope ownership in the SQL statement for reads and writes; a client-provided `user_id` is never
  authorization.
* Validate every untrusted body, query, and identifier before the service boundary.
* Use indistinguishable missing/cross-owner responses unless disclosure is an explicit requirement.
* Review mass assignment, URL/HTML rendering, uniqueness races, foreign-key deletion, pagination
  bounds, and sensitive fields in responses/logs.
* Keep secrets, cookies, auth links, credentials, private URLs, and customer fixtures out of
  commits, evaluation specs, command logs, and issue comments.

## Failure modes [#failure-modes]

* Duplicating a request interface in the web app and drifting from the server schema.
* Putting SQL or business branching in Hono routes to save one file.
* Fetching a record by ID and checking `user_id` later, which leaks timing/existence and invites a
  missed authorization branch.
* Trusting owner identity from the request body or an ad hoc header.
* Invalidating every React Query cache entry after each mutation.
* Rendering only the populated happy path with no loading, empty, error, or pending state.
* Treating TypeScript inference as runtime input validation.
* Adding product-unrelated collaboration, admin, billing, queue, or provider abstractions.
* Marking the workflow complete after source tests without a generated-buyer execution.

## Rollback and diagnosis [#rollback-and-diagnosis]

If the HTTP type is missing in the web client, verify the route is mounted in the fluent `createApp`
chain, contracts export once, and both server and web typechecks see the same workspace package. Do
not add a manual client interface as a workaround.

For incorrect ownership, stop before deployment. Add cross-owner tests at repository and HTTP
boundaries, move the owner predicate into the SQL mutation, and inspect every list/detail/update/
delete function. Follow the failed-migration path for schema errors. Roll back UI and API code in
compatibility order; use the migration down section only when its data effect is safe.

## Acceptance criteria [#acceptance-criteria]

* One short PRD maps to an explicit owner, lifecycle, invariants, and failure contract.
* Shared Zod contracts parse valid input and reject invalid input without duplicate wire types.
* The migration passes fresh `up → down → up` and real Postgres tests.
* Repository, service, routes, app mounting, typed web service, route, and UI each retain their
  boundary.
* Owner success, invalid input, unauthenticated access, cross-owner access, and missing records are
  tested.
* UI loading, empty, error, success, pending, keyboard, and narrow-viewport behavior is addressed.
* Root verification and a custom generated-buyer evaluation pass without architecture violations.
* Any human intervention or remaining unsupported behavior is recorded rather than hidden.

## Agent prohibitions [#agent-prohibitions]

* Do not put SQL in routes or HTTP objects/status codes in repositories.
* Do not duplicate shared request or response types.
* Do not accept `user_id`, role, entitlement, or ownership claims from the client.
* Do not authorize only in the UI or only after an unscoped database lookup.
* Do not mutate a released migration or forge migration history.
* Do not add a second API client, server-state cache, styling system, or authentication mechanism.
* Do not weaken validation, auth, ownership, constraints, or tests to make the fixture pass.
* Do not claim cold or independent execution when the authoring agent performed the run.


# Add an environment variable (/docs/golden-paths/add-environment-variable)



# Golden path: add an environment variable [#golden-path-add-an-environment-variable]

## When to use [#when-to-use]

Use this path when runtime behavior genuinely varies by deployment or requires a credential,
external endpoint, resource path, or operational limit. Prefer code constants for invariants and
database/product configuration for values a user should change without redeploying.

Classify the variable before editing:

* required boot input;
* optional integration credential;
* safe value with a deterministic default;
* deployment-only resource path;
* public web build input.

## Files and boundaries [#files-and-boundaries]

Server variables:

* `apps/server/src/config/env.ts`: Zod declaration, normalization, production checks, and `Config`.
* `apps/server/.env.example`: safe example plus required/optional behavior.
* `apps/server/src/test-setup.ts`: deterministic test values and external-side-effect prevention.
* `apps/server/Dockerfile`, `apps/server/railway.toml`, CI, and deployment docs when an owner must
  map the value.
* `packages/create-app/src/core.ts` and tests when scaffolding must generate or rewrite it.

Web variables:

* `apps/web/.env.example`;
* `apps/web/src/configs/config.ts` or a validated public-config boundary;
* Vercel/build configuration and buyer documentation.

All `VITE_*` values are public build output. Secrets belong only on the server.

## Procedure [#procedure]

1. State the owner, sensitivity, required/optional status, default, environments, rotation behavior,
   and no-key behavior.

2. Add the variable to `envSchema` in `apps/server/src/config/env.ts`. Validate URLs, enums,
   positive integers, header names, and minimum secret lengths at the boundary rather than at each
   call site.

3. Map the inferred value into `Config` and read it through `config()`. Do not read
   `process.env.NEW_VALUE` throughout domain code.

4. Add a safe `.env.example` entry. Use a placeholder, never a copied real value. Explain exactly
   what remains usable when an optional key is absent.

5. If the variable is a third-party integration, preserve zero-key local startup with an actionable
   disabled result or a deterministic local adapter. Production may deliberately require the key,
   but that check belongs in the validated config boundary.

6. Update `src/test-setup.ts` so a developer `.env` cannot send email, post Slack messages, call a
   paid provider, or enable an ambient integration during tests.

7. Update every deployment owner:

   * Railway or Dokploy server runtime for server-only values;
   * Vercel build/runtime only for explicitly public web values;
   * Docker `ENV` only for safe defaults, never image-baked secrets;
   * CI with test-only values where the path must execute.

8. If create-app must generate the value, update the environment generator, scope/brand behavior if
   applicable, and its exact tests. Required secrets must be unique per scaffold.

9. Add positive, missing, malformed, and production-required tests. For resource paths, test the
   supported layouts and make an explicit missing override fail fast.

10. Scan the diff and logs for copied values, then run a custom buyer scaffold.

## Verification [#verification]

```sh
vp check
vp run --filter @app/server typecheck
bun test --cwd apps/server
vp run -r test
vp run -r build
git diff --check
```

Then verify both configurations that matter:

1. the variable present with a safe test value;
2. the variable absent, proving the documented default or actionable startup failure.

For a public web variable, inspect the production bundle and assume every value is readable by a
buyer or end user. For a secret, scan output for the exact test value without printing a real
secret.

## Security constraints [#security-constraints]

* Never expose credentials through `VITE_*`, response JSON, health endpoints, logs, exception
  messages, analytics, source maps, Linear, or committed `.env` files.
* Do not use production credentials in local tests. Test setup must override values Bun loaded from
  a developer `.env`.
* Generate secrets with a cryptographically secure source and document rotation/revocation.
* Validate callback origins, provider URLs, proxy headers, and resource paths rather than accepting
  arbitrary strings.
* Treat a secret that appeared in git history or tool output as compromised; removal from the latest
  file is not remediation.

## Failure modes [#failure-modes]

* The Zod schema accepts a value but `Config` forgets to map it, or code bypasses `Config`.
* `.env.example`, create-app, Docker, Railway, Dokploy, Vercel, and CI disagree on the variable
  name.
* An optional integration crashes basic local setup instead of degrading clearly.
* A default silently enables billable network traffic, weakens auth/security, or changes production
  behavior.
* A web-prefixed value leaks a server credential into JavaScript.
* A compiled binary depends on a cwd-relative resource and source tests never exercise the packaged
  layout.
* Tests inherit a real developer key and create an external side effect.

## Rollback and diagnosis [#rollback-and-diagnosis]

Remove a newly optional variable only after all consumers tolerate absence. For a renamed variable,
support both names for a documented transition or make the breaking release explicit; do not
silently reinterpret an existing value. If startup fails, use the Zod field error and deployment
owner to diagnose mapping before weakening validation.

The `MIGRATIONS_DIR` and `EMAIL_TEMPLATES_DIR` implementation is the reference for explicit resource
overrides: verified layouts work without configuration, an explicit invalid path is authoritative
and fatal, and the compiled binary is tested from repository root.

## Acceptance criteria [#acceptance-criteria]

* Sensitivity, owner, required/optional behavior, default, and no-key behavior are documented.
* Schema, `Config`, examples, tests, deployment mapping, and scaffold generation agree.
* Present, absent, malformed, and production-required cases behave intentionally.
* No secret is client-visible, logged, committed, or used by tests.
* Root verification and a generated buyer artifact pass.

## Agent prohibitions [#agent-prohibitions]

* Do not read new environment variables ad hoc outside the config boundary.
* Do not make a third-party key mandatory for basic local setup without an explicit release-contract
  decision.
* Do not put secrets in `VITE_*`, Docker image layers, examples, tests, fixtures, comments, or issue
  trackers.
* Do not weaken validation merely to make one environment boot.
* Do not add a variable without updating its deployment owner and removal/rotation behavior.
* Do not call the path complete after typecheck without testing present and absent states.


# Add transactional email (/docs/golden-paths/add-transactional-email)



# Add a transactional email [#add-a-transactional-email]

## When to use [#when-to-use]

Use this path for a product-triggered message whose recipient, event, and action are known: account
verification, authentication, security, billing, or a similarly bounded lifecycle event. Do not use
it for campaigns, newsletters, or a generic notification blast.

This runbook makes a template and delivery call production-capable. It does not promise durable
outbox semantics. If losing a non-auth email after a process crash would violate the product
contract, add a database-backed delivery claim and retry policy as explicit domain work.

## Files and boundaries [#files-and-boundaries]

* `packages/email/src/<template>.tsx`: React Email source and preview defaults.
* `packages/email/src/index.ts`: source export.
* `packages/email/scripts/deploy.ts`: exact exported-file inventory and link placeholder.
* `apps/server/email-templates/`: generated HTML; never edit it manually.
* `apps/server/src/infra/email/renderer.ts`: typed template name, props, subject, preload, escaping.
* `apps/server/src/infra/mailer/client.ts`: Resend delivery and zero-key local fallback.
* `apps/server/src/domains/<domain>/`: decides when, why, and to whom the email is sent.
* `apps/server/src/config/env.ts`: provider configuration contract.

The email package owns presentation. The renderer owns the runtime template contract. A domain owns
the sending decision. Provider details must not leak into domain code.

## Procedure [#procedure]

1. Define the event, recipient, deduplication behavior, and whether delivery failure must fail the
   initiating request. Authentication and security flows should await required delivery. Best-effort
   product notifications may log and continue only when that is a deliberate product decision.

2. Create `packages/email/src/<template>.tsx` with placeholder defaults such as `{{actionLink}}`. Do
   not hard-code the product name, support address, production host, or a real token.

3. Export the component from `packages/email/src/index.ts`.

4. Add `<template>.html` and its required action-link placeholder to `templateLinkMap` in the deploy
   script. The deploy command rejects missing and unexpected exports.

5. Add the template name, exact props type, and subject to the renderer. Keep props minimal and
   serializable. Runtime placeholder values are HTML-escaped.

6. Regenerate buyer HTML through the owning command:

   ```sh
   vp run --filter @app/email deploy
   ```

7. Call `mailer().send(...)` from the owning domain with the typed template and props. Send only
   after the database state that justifies the email has committed; never send an irreversible
   provider side effect inside a transaction that can roll back.

8. Add tests for the domain decision, rendered placeholders, the zero-key log path, and provider
   rejection. If the event can replay, prove its email policy under replay.

9. Confirm production has a verified sending domain, an aligned `EMAIL_FROM`, and a non-empty
   `RESEND_API_KEY`. Production environment parsing fails without the provider key.

## Verification [#verification]

Run:

```sh
vp run --filter @app/email deploy
bun test --cwd apps/server src/infra/email/renderer.test.ts src/infra/mailer/client.test.ts
vp check
vp run --filter @app/server typecheck
vp run --filter @app/server build
```

Then start the server without `RESEND_API_KEY`, trigger the real event, and verify the structured
server log contains the expected local action link and no provider request occurs. With a Resend
test configuration, send to an owned inbox and inspect sender, subject, action URL, mobile layout,
and delivery result.

## Security constraints [#security-constraints]

* Treat recipient addresses, names, amounts, plan labels, and URLs as untrusted input. Use typed
  placeholders; do not concatenate raw HTML.
* Never log `RESEND_API_KEY`, OAuth secrets, passwords, or provider payloads. Auth links are logged
  only by the intentional zero-key local fallback and must not be enabled in production.
* Generate action URLs through the owning auth or domain flow. Do not invent reusable tokens in the
  template layer.
* Keep reset, verification, and magic-link expiry and one-use behavior in Better Auth.
* Do not accept a recipient or template name directly from an unauthenticated request.
* Avoid sensitive personal data in subjects, because subjects can appear in notifications and logs.
* Verify the sending domain and use a product-controlled `From` address; do not spoof user input.

## Failure modes [#failure-modes]

* `Email template ... not loaded`: startup did not preload templates or the typed name drifted.
* `Required email template is missing`: regenerate HTML and inspect the inventory failure.
* Placeholder remains in delivered HTML: the renderer props contract and source placeholder differ.
* Local event produces no link: the owning domain did not await/call the mailer, or its mapping
  returned no email.
* Resend rejects the message: inspect the stable provider error, sender verification, recipient
  restrictions, and provider status; do not report the event as delivered.
* A database rollback occurs after delivery: the side effect was sent inside the wrong transaction.
* Duplicate email on webhook replay: the domain lacks an idempotent event/delivery policy.

## Rollback and diagnosis [#rollback-and-diagnosis]

Remove the domain call first to stop new sends. Revert the typed renderer entry, source export,
deploy-map entry, and source component together, then rerun the deploy command so generated HTML
matches the source inventory. Do not hand-delete or patch a generated HTML file.

For delivery incidents, preserve the product event identifier, template name, recipient hash or
approved address, provider request identifier, and failure category. Never paste secrets or live
auth tokens into an issue.

## Acceptance criteria [#acceptance-criteria]

* The source, export map, renderer inventory, generated HTML, and typed props agree.
* Startup preloads every required template and fails closed when one is missing.
* Untrusted placeholder values are escaped and rendered output has no unresolved required token.
* Zero-key development logs the actionable local link and never calls Resend.
* Configured delivery sends the expected typed payload and surfaces provider rejection.
* The owning domain has an explicit replay and failure policy.
* Production configuration requires the provider key and uses a verified sender.
* The focused tests, `vp check`, server typecheck, and server build pass.

## Agent prohibitions [#agent-prohibitions]

* Do not edit `apps/server/email-templates/*.html` manually.
* Do not introduce a second mail provider directly inside a product domain.
* Do not silently swallow a required authentication or security email failure.
* Do not send before the state authorizing the email commits.
* Do not log provider credentials or production action tokens.
* Do not claim durable delivery, retries, or exactly-once email without a persisted delivery claim.
* Do not add Tailwind to the web application; React Email's isolated styling is separate.


# Add an upload type (/docs/golden-paths/add-upload-type)



# Add an upload type [#add-an-upload-type]

## When to use [#when-to-use]

Use this path when a product needs a new, explicitly owned object-storage purpose. The launch
foundation currently supports one narrow flow: an authenticated user uploads their own public
profile image, up to 5 MiB, through a 15-minute presigned `PUT`.

Do not generalize this into arbitrary files. Documents, executables, private downloads, shared
workspace files, and user-generated public media have different authorization, malware, content
validation, retention, and delivery requirements.

## Files and boundaries [#files-and-boundaries]

* `packages/contracts/src/upload.ts`: type, request schemas, MIME types, size, extension, expiry.
* `apps/server/src/domains/upload/routes.ts`: session, billing gate, rate limit, validation.
* `apps/server/src/domains/upload/service.ts`: authorization, keys, claim, byte/metadata validation,
  and publication policy.
* `apps/server/src/domains/upload/repository.ts`: owner-scoped persistence and atomic state changes.
* `apps/server/src/infra/storage/contract.ts`: provider-neutral storage contract.
* `apps/server/src/infra/storage/`: R2, S3, and GCS provider mechanics.
* `apps/server/src/config/env.ts`: selected-provider configuration.
* `apps/web/src/components/ui/file-upload/use-file-upload.ts`: signed `PUT`, progress, confirmation.
* `apps/server/migrations/`: schema changes after the first commercial tag.

Contracts define wire input, the domain defines authorization and policy, and each storage adapter
defines provider mechanics. Never authorize from an object-key prefix alone when a database owner
record is available.

## Procedure [#procedure]

1. Write the purpose, owner, visibility, maximum size, accepted media types, retention, overwrite
   behavior, abuse controls, and post-upload validation before adding an enum.
2. Add the type and its maximum size, MIME allowlist, and extension mapping to the shared contract.
   Keep claim input limited to purpose, owner/entity ID, MIME type, declared byte size, and optional
   display filename.
3. Add target authorization to `assert_upload_target_access`. Resolve ownership from the session and
   database state, never from client claims.
4. Add a unique public key shape to `generate_file_path` and its inverse to `parse_file_path`.
   Staged keys stay under `pending/`; public keys keep an immutable random component and never
   receive a presigned write URL.
5. Persist the pending claim before returning it: owner, purpose, entity, private staging key,
   intended public key, declared size, signed MIME type, expiry, and `pending` status. Never return
   either key from the claim endpoint.
6. Upload the raw file to the private staging bucket with `PUT`, exact signed `Content-Type`, and
   the browser-derived `Content-Length`. The included path uses direct presigned `PUT`, not browser
   multipart `POST`.
7. Confirm with only the server-issued `upload_id`. Load an owner-scoped, unexpired pending row,
   read actual object metadata, compare size and MIME type, structurally validate the complete
   provider-version-pinned object, then publish that same validated version to the public bucket
   before atomically transitioning `pending → completed`.
8. On mismatch, mark the claim failed and best-effort delete the staged object. Keep the mandatory
   one-day `pending/` lifecycle rule on the private bucket; claim creation refuses to sign when the
   rule cannot be verified.
9. Add web UI validation for feedback, but keep all security validation on the server.
10. Configure bucket CORS for the exact web origin, `PUT`, and `Content-Type`.
11. Add database-backed tests for owner, cross-owner, expiry, missing object, size mismatch, MIME
    header mismatch, MIME spoofing, version-bound publication, replay, concurrency, and cleanup.

## Verification [#verification]

Run:

```sh
vp run --filter @app/contracts test
bun test --cwd apps/server src/domains/upload/service.test.ts
RUN_DB_INTEGRATION_TESTS=1 DATABASE_URL=postgresql://... \
  bun test --cwd apps/server src/domains/upload/service.integration.test.ts
vp check
vp run --filter @app/server typecheck
vp run --filter @app/web typecheck
```

Against disposable private and public buckets for each supported provider, prove a correct upload,
wrong `Content-Type`, wrong byte length, expired URL, over-limit declaration, spoofed content,
provider-version race, replay, cross-owner confirmation, staging cleanup, and delete. Verify a
public URL only for types deliberately classified as public.

## Security constraints [#security-constraints]

* Presigned URLs are bearer tokens and can be reused until expiry. Keep expiry short, keys unique,
  logs redacted, and HTTPS mandatory.
* The presigner binds exact `Content-Length` and `Content-Type`; confirmation independently checks
  stored provider metadata. The web client sets only `Content-Type` because browsers own the
  forbidden `Content-Length` header and derive it from the `File`.
* PNG chunks/CRCs, JPEG markers/scans, and WebP RIFF/chunks are structurally validated with bounded
  dimensions and no trailing payload before publication. This is not full pixel decompression, EXIF
  removal, moderation, or malware scanning. Add normalization or scanning before supporting
  documents or broadly public user content.
* Publication uses an ETag-conditional S3/R2 copy or a generation-pinned GCS copy. Do not replace it
  with a path that can publish a different version than the one validated.
* Keep owner predicates in SQL transitions so concurrent or cross-owner requests cannot consume
  another claim.
* Partial selected-provider configuration is invalid. Disabled storage is valid locally and upload
  endpoints return a stable 503 without a provider call.

References: [R2 presigned URLs](https://developers.cloudflare.com/r2/api/s3/presigned-urls/),
[S3 presigned uploads](https://docs.aws.amazon.com/AmazonS3/latest/userguide/PresignedUrlUploadObject.html),
and
[GCS V4 signed URLs](https://cloud.google.com/storage/docs/access-control/signing-urls-with-helpers).

## Failure modes [#failure-modes]

* `UNAVAILABLE`: storage is disabled, selected-provider configuration is incomplete, or the staging
  lifecycle rule is missing/inaccessible.
* Provider signature mismatch: request method, browser byte length, or `Content-Type` differs from
  the signed claim.
* Browser CORS failure: the bucket origin, method, or allowed header is incomplete.
* Object not found: the `PUT` failed or the wrong bucket/key was used; start a new claim.
* Size or MIME mismatch: the object is failed and deleted; never trust browser confirmation fields.
* Confirmation replay: the owner-scoped pending update affects zero rows and returns a stable error.
* Copy precondition failure: the staged ETag or GCS generation changed after validation; start a new
  claim.
* Orphan after failed immediate cleanup: inspect failed claims and the mandatory lifecycle rule;
  retry deletion without changing the claim to completed.

## Rollback and diagnosis [#rollback-and-diagnosis]

Disable the new type at the contract and authorization boundary first so no new URLs are signed.
Preserve existing rows and keys until their ownership and product references are known. Remove UI
entry points, then delete objects through an owner-scoped maintenance operation.

Record upload ID, purpose, status, owner ID, key, declared/actual metadata, expiry, request ID, and
provider error category. Never record credentials or a still-live presigned URL.

Before the first commercial tag, a baseline correction is allowed with a fresh migration
`up → down → up` proof. After that tag, add a reversible timestamped migration; never edit the
shipped baseline.

## Acceptance criteria [#acceptance-criteria]

* The type has an explicit purpose, owner, visibility, limits, retention, and abuse policy.
* Claim creation authorizes before signing and rejects invalid size/MIME input.
* Staging and public keys are separate and unique; a reusable staged URL cannot overwrite a
  published object.
* Confirmation accepts only `upload_id` and verifies owner, pending state, expiry, actual size, MIME
  metadata, and complete image structure before copying the validated provider version.
* Cross-owner, expired, mismatched, spoofed, replayed, and concurrent confirmations are proven.
* Rejected objects are failed and best-effort deleted; claim creation verifies automatic abandoned
  object expiration before signing.
* Zero-key local development and all-or-none production configuration are proven.
* The focused tests, `vp check`, server typecheck, and web typecheck pass.

## Agent prohibitions [#agent-prohibitions]

* Do not accept `file_path`, completion status, or actual file size from the browser at
  confirmation.
* Do not use multipart `POST` with the included presigned `PUT` flow.
* Do not use a stable overwrite key for mutable public content.
* Do not presign a public destination key or return a private staging key to the browser.
* Do not authorize solely by parsing a client-supplied path.
* Do not claim structural image validation is pixel decoding or malware scanning.
* Do not add arbitrary document uploads without a processing and security contract.
* Do not require storage credentials for basic local setup.


# Customize branding and design tokens (/docs/golden-paths/customize-branding-design-tokens)



# Customize branding and design tokens [#customize-branding-and-design-tokens]

## When to use [#when-to-use]

Use this path when turning a fresh buyer scaffold into a named product or deliberately changing its
visual identity. Run `create-app --brand` first; use the rest of this path for product description,
sender identity, URLs, imagery, icons, typography, and color.

The CLI applies a safe display name to every counted static surface. It does not invent a logo,
marketing promise, support address, social account, legal entity, or accessible color system.

## Files and boundaries [#files-and-boundaries]

* `packages/create-app/src/core.ts`: counted static brand manifest and safe name validation.
* `apps/web/src/configs/config.ts`: one runtime `BRAND_NAME` used by React UI.
* `apps/web/index.html`: browser, social, and application metadata.
* `apps/web/public/favicon/site.webmanifest`: PWA name, description, icons, theme, and start URL.
* `apps/web/public/offline.html`: standalone offline identity and colors.
* `apps/web/src/components/brand/`: runtime logo-mark and product-name composition.
* `apps/web/public/logo.svg` and `apps/web/public/favicon/`: source mark and generated image assets.
* `apps/web/src/styles/_variables.css`: font, surface, accent, and status tokens.
* `packages/email/src/`: email identity, logo URL, sender-facing copy, and footer links.
* `apps/server/email-templates/`: generated email HTML; update it through the email deploy command.
* `apps/server/src/config/env.ts` and `.env.example`: default sender identity.

Runtime React copy must read `BRAND_NAME`; static HTML, JSON, server, and email surfaces belong in
the counted brand manifest. Product descriptions and support/legal URLs are explicit product
decisions, not brand-name substitutions.

## Procedure [#procedure]

1. Generate the buyer with a safe brand name:

   ```sh
   pnpm superslate proof-product \
     --from /absolute/path/to/template \
     --scope @proof \
     --brand "Proof Product" \
     --no-git
   ```

2. Open `apps/web/src/configs/config.ts`, `index.html`, `site.webmanifest`, `offline.html`, the auth
   pages, settings, and every email source. Confirm the generated name is correct and replace the
   neutral description with one accurate customer outcome.

3. Keep `short_name` concise enough for installed-app launchers. Remove every PWA shortcut that does
   not point to a real, authorized route.

4. Replace `apps/web/public/logo.svg` with an optimized, text-free source mark, then regenerate the
   favicon and splash raster set from that one reviewed master with
   `vp run --filter @app/web assets:splash`. Preserve declared dimensions, transparency, safe-zone
   behavior, and the maskable-icon requirement.

5. Set real `alt` text only where an image communicates information. Keep repeated marks decorative
   when adjacent text already names the product.

6. Change the semantic values in `_variables.css`: `--surface-canvas`, `--surface-raised`,
   `--accent`, `--accent-strong`, their opacity variants, and status colors. Do not replace semantic
   token names with a new product-color vocabulary.

7. Verify normal, hover, active, disabled, selected, error, and focus-visible states on both
   surfaces. Preserve visible keyboard focus and test text, icons, borders, and controls for
   contrast.

8. Update fonts deliberately. Ship required webfont files locally or document their provider,
   licensing, fallback, preload, and failure behavior.

9. Update `packages/email/src/components/EmailLayout.tsx` with a production PNG logo URL and real
   footer destinations. Set `EMAIL_FROM` to a verified sender owned by the product.

10. Regenerate email artifacts:

    ```sh
    vp run --filter @app/email deploy
    ```

11. Search buyer output for the placeholder and source-product markers. If a new static brand
    surface is intentional, add it to `BRAND_MANIFEST` with its exact occurrence count and update
    the contract test.

## Verification [#verification]

Run:

```sh
vp run --filter create-app test
vp run --filter @app/email deploy
vp check
vp run --filter @app/web test
vp run --filter @app/web build
```

Generate a product with a multiword custom brand and inspect:

* document title, metadata, install prompt, PWA manifest, offline page, auth, settings, and errors;
* navbar, narrow viewport, splash/loading, missing-route, and reduced-motion behavior;
* verification, magic-link, welcome, security, payment, and subscription email HTML;
* favicons, Apple touch icon, maskable icon, install prompt, and installed launcher name;
* email sender, logo, links, reply behavior, and inbox rendering.

Run a case-insensitive scan for the old product name, placeholder descriptions, retired analytics
events, stale routes, and source-product class prefixes. A source-only pass is insufficient: inspect
the generated buyer and its production build.

## Security constraints [#security-constraints]

* Treat HTML, JSON, TypeScript, JSX, shell, and SVG as different escaping contexts. The CLI rejects
  punctuation that could break those formats; do not loosen validation without context-aware
  escaping and tests.
* Never put secret values in `VITE_*`, HTML metadata, the PWA manifest, source maps, or image files.
* Do not point email or web UI at unverified domains, sender addresses, support inboxes, or social
  accounts.
* Preserve CSP-compatible asset loading, HTTPS URLs, Sentry redaction, and authenticated-route
  behavior while replacing identity.
* Do not make focus indicators, status states, or legal/support links disappear for visual purity.

## Failure modes [#failure-modes]

* Placeholder survives: a static surface is missing from `BRAND_MANIFEST`, or generated email HTML
  was not refreshed.
* Broken TypeScript/JSON/HTML after scaffold: brand validation or context handling was bypassed.
* Wrong installed-app name or icon: stale manifest, service worker, favicon cache, or maskable
  asset.
* Long name clips: a component embedded text in a fixed-size image instead of composing the runtime
  name with the text-free mark.
* Email shows the old identity: source was changed without running the email deploy command.
* New palette is unreadable: raw values were changed without testing semantic states and contrast.
* Production build keeps old assets: browser/PWA cache was not invalidated or the wrong source
  master was regenerated.

## Rollback and diagnosis [#rollback-and-diagnosis]

Keep the last reviewed source mark, icon master, token values, and generated email artifact commit.
Revert identity as one unit: runtime name, static metadata, PWA assets, design tokens, email source,
sender configuration, and generated templates.

Diagnose from source to consumer: config → React UI; HTML/manifest → browser and installed PWA;
email source → deploy output → provider; token definition → component state. Clear only disposable
local browser/PWA caches during diagnosis; never delete customer data or external provider
resources.

## Acceptance criteria [#acceptance-criteria]

* A fresh custom-brand scaffold passes without manual syntax or formatting repair.
* The counted manifest rewrites every intended static surface and rejects unsafe input.
* Runtime UI uses one brand constant and a text-free, size-independent mark.
* Metadata, PWA, offline, auth, billing, account, error, and email surfaces contain no
  source-product behavior or placeholder route.
* Semantic tokens cover canvas, raised surface, accent, strong accent, status, and focus states.
* Keyboard focus, contrast, reduced motion, narrow viewport, long-name, and missing-image behavior
  are reviewed.
* Generated email HTML matches source and uses an approved sender, logo, and destinations.
* The create-app test, email deploy, `vp check`, web test, and production build pass.

## Agent prohibitions [#agent-prohibitions]

* Do not lock or register a commercial product name, domain, organization, or account without owner
  approval.
* Do not inject arbitrary brand text into source contexts without validation and escaping.
* Do not edit generated email HTML manually.
* Do not embed a buyer name in fixed-size raster or SVG artwork when runtime text can own it.
* Do not ship stale PWA shortcuts, source-product copy, event names, class prefixes, or profile
  fields.
* Do not rename semantic tokens to colors such as `pink` or scatter raw brand hex values through
  components.
* Do not claim accessibility from a palette alone; verify actual rendered states and interactions.


# Decide background work (/docs/golden-paths/decide-background-work)



# Decide a background-work request [#decide-a-background-work-request]

## When to use [#when-to-use]

Use this path whenever a PRD asks for a worker, queue, scheduled job, async AI processing, retries,
webhook fan-out, long-running task, or cleanup outside the supported request/response path.

The v1 foundation deliberately has no generic worker or queue. This runbook produces a supported,
deferred, rejected, or product-specific architecture decision. It does not authorize silently
restoring the historical Cloudflare/Gemini worker.

## Files and boundaries [#files-and-boundaries]

* `docs/decisions/0001-exclude-background-worker-from-v1.md`: accepted exclusion and re-entry bar.
* `AGENTS.md`: current background-work prohibitions and architecture requirements.
* the product PRD and product contract: whether delayed work is truly required.
* `apps/server/src/domains/<domain>/`: synchronous bounded work and persisted domain state.
* `packages/contracts`: typed job input/output only if a product-specific mechanism is approved.
* deployment, environment, cost, retention, and incident docs for any approved executor.

A provider SDK or scheduler does not own application authorization, job identity, data ownership,
idempotency, or product state. Those remain server/domain responsibilities.

## Procedure [#procedure]

1. Rewrite the request as a concrete job: trigger, authenticated owner, input, output, maximum
   runtime, frequency, concurrency, latency target, retryability, terminal failure, data retention,
   provider cost, and user-visible states.
2. Decide whether the work is actually background work:
   * bounded, fast, and safe to retry with the request may remain synchronous;
   * optional post-response convenience work may be deferred from the PRD;
   * work whose correctness depends on surviving process exit needs durable execution and may not be
     hidden in an unawaited promise or in-memory timer.
3. Test the request against all re-entry criteria in Decision 0001: Better Auth authorization;
   server-owned job/object ownership; idempotency, retry, terminal failure, and deletion; abuse and
   provider-cost limits; typed MIME-correct contracts; zero-key local behavior; retention and
   observable paginated cleanup; deployed end-to-end proof; and a proportional support/removal path.
4. Produce one explicit result:
   * **supported synchronous**: bounded request/response work with tests and timeouts;
   * **defer/re-scope**: not required for the validated user outcome;
   * **unsupported in v1**: PRD depends on durable background infrastructure but evidence and
     re-entry criteria are absent;
   * **approve product-specific design investigation**: a real product job and evidence justify
     designing the smallest executor, subject to a new ADR and full proof.
5. For unsupported/deferred outcomes, update the PRD and Linear scope. Do not add packages or code.
6. For an approved investigation, write an ADR before implementation. Name authorization handoff,
   persisted state machine, idempotency key, retry/backoff/dead-letter behavior, rate/cost limits,
   retention, local disabled path, deployment owner, observability, rollback, and removal.
7. Re-run the product contract and support-cost review. One validated job does not automatically
   justify advertising a generic queue.

## Verification [#verification]

For a decision-only result:

```sh
rg -n -i "worker|queue|background|cron|scheduled|job" \
  AGENTS.md README.md docs package.json pnpm-workspace.yaml apps packages
vp check
```

Verify no worker package, shared worker JWT, queue environment key, deployment trigger, or product
claim was introduced. For supported synchronous work, add timeout, duplicate-request, process-error,
authorization, and database integration proof. For an approved executor, Decision 0001 requires a
deployed API-to-executor smoke in addition to all repository checks.

## Security constraints [#security-constraints]

* Never pass a Better Auth session cookie or a long-lived shared JWT to a separate executor.
* Every job needs purpose, audience, subject/owner, short authorization lifetime or server-side
  claim, and replay behavior.
* Persist ownership and status before external work. Provider metadata is not authorization.
* Bound input size/type, concurrency, retries, provider spend, retention, and cleanup.
* Do not put secrets or full customer payloads into queue names, logs, job IDs, or evidence.
* Cleanup must be paginated, observable, retryable, and ownership-safe.
* Unawaited promises and in-memory timers are not durable background infrastructure.

## Failure modes [#failure-modes]

* “Add BullMQ/Trigger/Cloudflare Queues” appears before a job contract: solution-first expansion.
* An HTTP handler returns before an unpersisted promise: process exit silently loses required work.
* A provider task ID becomes the application job ID: ownership and retries are outsourced.
* A shared secret authenticates every user/job: compromise has unbounded scope.
* Retry creates duplicate charges or objects: no domain idempotency key/state transition exists.
* Cleanup is periodic but unpaginated or silent: retention claims are false.
* Local setup requires queue/AI/storage credentials: zero-key product setup regressed.
* A sample is shipped but unsupported in docs/incidents: distributed code became hidden support
  liability.

## Rollback and diagnosis [#rollback-and-diagnosis]

For a decision-only path, revert the PRD scope change if evidence later satisfies the re-entry bar.
For an implementation, stop new job creation before disabling consumers, preserve persisted state,
drain or explicitly fail owned jobs, revoke scoped credentials, and follow the ADR rollback.

Diagnose by job ID, application owner, state transition, attempt, idempotency key, and redacted
provider request ID. Never use a queue purge as the first recovery action.

## Acceptance criteria [#acceptance-criteria]

* The job is defined by outcome, ownership, lifecycle, limits, and failure behavior before tooling.
* Every Decision 0001 re-entry criterion is answered with evidence or an explicit gap.
* The result is one of supported synchronous, defer/re-scope, unsupported v1, or approved
  product-specific investigation.
* Unsupported/deferred decisions add no runtime dependency, environment key, deployment resource,
  auth mechanism, or commercial claim.
* Approved work has a new ADR and complete authorization/state/idempotency/cost/retention/local/
  deployment/support contract before implementation.
* `vp check` passes and current docs remain consistent with the worker exclusion.

## Agent prohibitions [#agent-prohibitions]

* Do not restore the deleted Cloudflare/Gemini worker from git history.
* Do not add a generic queue because a PRD contains the word background.
* Do not implement required work with an unawaited promise, timer, or process-local map.
* Do not mint a parallel shared-secret auth system.
* Do not call provider task state the application source of truth.
* Do not advertise background work from an ADR, mock, local-only example, or package installation.
* Do not treat this runbook as approval; it is a decision gate.


# Deploy a fresh application (/docs/golden-paths/deploy-fresh-application)



# Deploy a fresh application [#deploy-a-fresh-application]

## When to use [#when-to-use]

Use this path after a generated buyer application passes local verification and is ready for its
first commercial deployment or a new isolated environment. It covers Railway, Dokploy, and the
OpenTofu-managed AWS, GCP, and Azure backends with native, Vercel, or Cloudflare static frontends.

Read [the deployment contract](../deployment.md) first. A host outside those targets is a new proof
obligation, not a small variation of this runbook.

## Files and boundaries [#files-and-boundaries]

* `.node-version`, `.bun-version`, root `package.json`: pinned toolchain.
* `apps/server/Dockerfile`: standalone API artifact and runtime user.
* `apps/server/railway.toml`: Railway build, readiness, and restart policy.
* `deploy/dokploy/compose.yml`, `deploy/dokploy/.env.example`: reproducible self-hosted topology.
* `scripts/verify-dokploy-compose.sh`: disposable production-container proof.
* `apps/server/.env.example`, `apps/server/src/config/env.ts`: private runtime configuration.
* `apps/web/vercel.json`, `apps/web/.env.example`: public static build configuration.
* `apps/web/wrangler.toml`, `apps/web/public/staticwebapp.config.json`: static frontend adapters.
* `infra/`: OpenTofu bootstrap, provider modules, executable roots, locks, and mocked tests.
* `deployment/config.json`, `deployment/README.md`: locked non-secret selection and operator steps.
* `scripts/deploy/`: migration-gated cloud release and native frontend scripts.
* `apps/server/src/main.ts`: startup order.
* `apps/server/src/app.ts`: liveness and readiness endpoints.
* `apps/server/migrations/`: dbmate schema authority.
* `apps/server/email-templates/`: rendered runtime templates.
* `.github/workflows/ci.yml`: fresh-install, migration, buyer, build, and compiled-server proof.

The selected backend owns server secrets, Postgres, proxy behavior, and the API artifact. The
selected frontend owns only the static browser build and its public `VITE_*` values. Provider
dashboards own credentials, callback URLs, products, webhooks, and sender/domain verification.

## Procedure [#procedure]

1. Generate the buyer repository and install from its own frozen lockfile.

2. Honor `.node-version` and `.bun-version`; confirm the root pnpm version from `packageManager`.

3. Start disposable Postgres and run:

   ```sh
   vp install --frozen-lockfile
   vp check
   pnpm test
   vp run -r build
   docker build --pull -f apps/server/Dockerfile -t app-server:release .
   bash scripts/verify-dokploy-compose.sh
   ```

4. Prove migration reversibility on a disposable database with `up → down → up`. Do not use an
   ambient or production `DATABASE_URL`.

5. Choose exactly one supported API target. Railway uses `apps/server/railway.toml`; Dokploy uses
   `deploy/dokploy/compose.yml`. For AWS, GCP, or Azure, run `pnpm deploy:configure` if deployment
   was deferred during application creation, review the generated checklist, bootstrap isolated
   state, select native S3/GCS or R2 storage, and dispatch the protected OIDC workflow. Azure uses
   R2 because Azure Blob storage is not included.

6. Set `DATABASE_URL`, a new 32+ character `BETTER_AUTH_SECRET`, `LOG_PRETTY=false`, the exact
   `TRUSTED_PROXY_PROFILE`, exact HTTPS `FRONTEND_URL` and `SERVER_URL`, pool limits, deployment
   metadata, a verified `EMAIL_FROM`, and `RESEND_API_KEY` in the selected API platform.

7. Leave `MIGRATIONS_DIR` and `EMAIL_TEMPLATES_DIR` unset for the included image. Configure optional
   Google, the selected billing provider, and storage integrations atomically or leave each complete
   integration unset.

8. Run `./server migrate` as a one-shot release step using the exact image digest. Deploy the API
   only after it exits zero, then require `/ready` to return `200`. Confirm all `9/9` templates are
   preloaded and the structured release metadata matches the digest.

9. Deploy the chosen static frontend with `VITE_APP_ENV=production` and exact HTTPS `VITE_API_URL`.
   For Vercel or Cloudflare, use the stable production origin supplied before the backend apply.

10. Register the exact Google callback and selected-provider webhook routes when those integrations
    are enabled.

11. Deploy the web artifact and execute the new-browser production smoke checklist in
    `docs/deployment.md`.

12. Record artifact IDs, command results, URL, measured cost, interventions, architecture
    violations, and smoke evidence in the agent evaluation harness.

## Verification [#verification]

Before any external deploy:

```sh
vp check
pnpm test
vp run -r build
bun test --cwd packages/create-app src/deployment-contract.test.ts
docker build --pull -f apps/server/Dockerfile -t app-server:release .
```

For the container proof, run the image as its non-root user against disposable Postgres and require:

```text
GET /health -> 200 {"status":"ok",...}
GET /ready  -> 200 {"status":"ready",...}
```

Inspect the running container uid/gid, its Docker health state, the startup migration result, and
the `9/9` template preload. Then execute the auth and enabled-integration smoke checks; a green
health endpoint alone is not deployment proof.

## Security constraints [#security-constraints]

* Generate new secrets per environment and enter them only in the owning provider.
* Treat every `VITE_*` value as public. Never expose server keys, secrets, connection strings, or
  private URLs through the web build.
* Use exact HTTPS origins and callback URLs. Do not use wildcard credentialed CORS.
* Select the exact supported proxy profile and keep the origin inaccessible around its ingress.
* Schedule and test an application-database backup. A Dokploy control-plane backup is separate.
* Use sandbox/test provider products for proof; do not create a real billable test purchase.
* Use a new browser profile and non-customer test identity for production smoke checks.
* Redact tokens, cookies, private connection details, and email links from evaluation and Linear
  evidence.

## Failure modes [#failure-modes]

* Building the server with `apps/server` as Docker context, which omits the workspace lockfile.
* Leaving `VITE_APP_ENV` unset, causing production instrumentation and behavior to be classified as
  development.
* Pointing `VITE_API_URL`, `FRONTEND_URL`, `SERVER_URL`, CORS, OAuth, and webhook configuration at
  different origins.
* Marking the service healthy from `/health` while Postgres, migrations, or templates are unusable.
* Allocating replica pool limits that leave less than 20% of database connections for release and
  administrative work.
* Partially configuring the selected billing provider, Google, storage, or Resend.
* Rolling back application code after an incompatible or destructive schema change.
* Treating a local Docker proof as evidence that external email, auth cookies, callbacks, billing,
  storage, DNS, and TLS work.

## Rollback and diagnosis [#rollback-and-diagnosis]

If the API does not become ready, preserve logs and identify whether environment validation,
database connectivity, migration execution, or resource preload failed. Keep the new artifact out of
service and redeploy the previous schema-compatible artifact. Use the failed-migration golden path
before any `down`, manual SQL, or version-table change.

If the web build fails, inspect the frozen install, exact Vite environment, and output directory. If
the deployed SPA regresses, restore the previous static artifact together with the API version whose
contract it expects.

Rotate credentials after any accidental bundle, log, transcript, or issue exposure. A corrected
deploy does not make an exposed secret safe again.

## Acceptance criteria [#acceptance-criteria]

* A clean generated buyer repository passes frozen install, checks, tests, builds, migrations, and
  the exact Docker build.
* The API runs as non-root, preloads required resources before traffic, and reports database-backed
  readiness.
* Frontend and selected backend environment ownership is explicit and contains no misplaced secret.
* Password, verification, magic link, reset, session, email, CORS, and every enabled optional
  integration pass the deployed smoke checklist.
* Release and rollback order are compatible with the schema.
* Measured recurring cost and excluded overages are recorded.
* The evaluation record contains no credentials or customer data.

## Agent prohibitions [#agent-prohibitions]

* Do not create provider projects, domains, products, or permanent account names without the
  founder's approval.
* Do not claim a deployed integration from config, mocks, a local container, or a health response.
* Do not change the selected supported topology to avoid diagnosing its failure.
* Do not route Railway or Dokploy readiness to `/health`.
* Do not add replicas, a worker, a queue, or another cloud/provider during first-deploy proof.
* Do not put secrets in Git, Docker layers, `VITE_*`, browser logs, evaluation evidence, or Linear.
* Do not apply, reverse, or repair migrations against an unconfirmed database target.


# Diagnose a failed migration (/docs/golden-paths/diagnose-failed-migration)



# Golden path: diagnose or recover a failed migration [#golden-path-diagnose-or-recover-a-failed-migration]

## When to use [#when-to-use]

Use this path when dbmate or server startup cannot apply migrations, a deployment waits on database
work, the migration version and physical schema disagree, or the application fails immediately after
a recorded migration.

The objective is to classify the failure before changing state. A failed health check alone does not
identify a migration failure.

## Files and boundaries [#files-and-boundaries]

* `apps/server/migrations/*.sql`: expected up/down SQL.
* `apps/server/src/infra/db/migrate.ts`: startup runner, per-file transaction, and version
  recording.
* `schema_migrations`: evidence of recorded versions; not a manual repair switch.
* API/deployment logs: connection, startup, migration, and application-query errors.
* Postgres `pg_stat_activity`, `pg_locks`, catalog tables, and constraints: physical database facts.
* the currently deployed and previous application artifacts: compatibility evidence.
* backups/restore verification: recovery boundary for destructive changes.

## Procedure [#procedure]

1. Stop rollout automation and prevent additional replicas from racing the same migration. Keep the
   last known compatible artifact available.

2. Identify the exact environment, database host/name (without printing credentials), application
   version, migration artifact/commit, last recorded migration, first failing file, and timestamp.

3. Check connection separately from schema:

   ```sh
   DATABASE_URL='postgres://safe-test-host/database?sslmode=require' \
     dbmate --migrations-dir apps/server/migrations status
   ```

   A DNS, TCP, TLS, authentication, or database-not-found error occurs before migration SQL. Fix the
   connection/deployment mapping; do not edit SQL.

4. If connected, inspect recorded versions and the target objects:

   ```sql
   SELECT version FROM schema_migrations ORDER BY version;
   SELECT to_regclass('public.expected_table');
   ```

5. If the command waits, inspect blockers from a privileged operational session:

   ```sql
   SELECT
     pid,
     usename,
     application_name,
     state,
     wait_event_type,
     wait_event,
     pg_blocking_pids(pid) AS blocked_by,
     query_start,
     left(query, 200) AS query
   FROM pg_stat_activity
   WHERE datname = current_database()
   ORDER BY query_start;
   ```

   Identify the owner and business operation before canceling anything. A long-running customer
   transaction is not safe to terminate merely because a deploy is waiting.

6. Classify the evidence:

   | Class                                  | Version row            | Physical schema         | Typical evidence                       | Response                                                                     |
   | -------------------------------------- | ---------------------- | ----------------------- | -------------------------------------- | ---------------------------------------------------------------------------- |
   | Connection                             | unknown                | unknown                 | DNS/TCP/TLS/auth error                 | repair deployment/database connectivity                                      |
   | Lock/wait                              | unchanged              | prior schema            | wait event and blocking PID            | coordinate owner; retry after safe release                                   |
   | Transactional DDL failure              | absent                 | failed file rolled back | SQL error; no new objects/version      | correct the unreleased/failing artifact or publish a reviewed repair release |
   | Non-transactional/manual partial state | absent or inconsistent | some objects remain     | catalog differs from file              | back up; write explicit idempotent reconciliation; do not forge version      |
   | Application incompatibility            | present                | expected new schema     | repository/query error after migration | roll application or ship forward-compatible code/schema                      |

7. Compare the failed statement with Postgres facts: existing rows, nulls, duplicates, foreign-key
   or check violations, enum casts, extension availability, lock level, and disk/connection limits.

8. Choose recovery:

   * retry unchanged only for a transient connection or safely released lock;
   * use one reviewed `down` only when the version is recorded, reversal is data-safe, and the
     compatible application order is known;
   * use a forward repair when reversal loses data, other environments may have applied the version,
     or the schema must remain compatible;
   * restore a tested backup only for a declared destructive incident.

9. Reproduce on an isolated snapshot or fixture, then run `up → down → up`, DB integration tests,
   and the compatible application artifact before resuming rollout.

10. Record the root cause, exact evidence, recovery, affected versions, and prevention test without
    copying credentials or customer data.

## Verification [#verification]

The repository contains an intentionally failing transactional fixture:

```sh
RUN_DB_INTEGRATION_TESTS=1 \
  DATABASE_URL='postgres://localhost:5432/app_migration_proof?sslmode=disable' \
  bun test --cwd apps/server src/infra/db/migrate.integration.test.ts
vp check
vp run -r test
```

It proves that a successful preceding migration is recorded, the failing file's DDL is rolled back,
and the failing version is not recorded. Also test a connection failure against a disposable
non-listening endpoint and confirm no secret value appears in captured output.

## Security constraints [#security-constraints]

* Redact passwords, tokens, private hostnames, full customer queries, and row data from logs and
  issue comments.
* Do not cancel or terminate a blocking backend until its owner, transaction, and data-loss impact
  are understood.
* Take and verify a backup before manual reconciliation or destructive rollback.
* Use least-privilege diagnostic access; schema repair does not justify broad customer-data access.
* Preserve evidence before changing migration files, version rows, or physical objects.

## Failure modes [#failure-modes]

* Treating every startup failure as bad SQL and changing a correct migration.
* Retrying a lock indefinitely while multiple replicas amplify contention.
* Assuming transactional rollback when SQL or an external/manual step ran outside the transaction.
* Running `down` while the new application is serving traffic that requires the new schema.
* Editing a released version that some environments already recorded.
* Deleting a `schema_migrations` row or manually creating an object to silence the runner.
* Publishing raw connection strings or customer queries during incident coordination.

## Rollback and diagnosis [#rollback-and-diagnosis]

Rollback is a deployment sequence, not just `dbmate down`: quiesce incompatible traffic, verify
backup, deploy or retain the compatible application, execute one reviewed down section, assert the
schema and data, then reopen traffic. If any step is unsafe or the version is already divergent
across environments, prefer an idempotent forward repair and rehearse it on a snapshot.

For a missing migration or template directory in a standalone binary, do not alter the database.
Verify the artifact layout and `MIGRATIONS_DIR`/`EMAIL_TEMPLATES_DIR`; explicit missing overrides
are designed to fail startup.

## Acceptance criteria [#acceptance-criteria]

* The failure is classified as connection, lock, transactional/partial DDL, or application
  incompatibility with recorded evidence.
* Recovery preserves version history and has a verified backup/forward or down strategy.
* Reproduction passes on an isolated database, including relevant schema assertions and application
  tests.
* Rollout order and compatible application versions are explicit.
* Incident output contains no secret or customer data.
* A prevention test or documented contract closes the discovered gap.

## Agent prohibitions [#agent-prohibitions]

* Do not edit a released migration before determining where it has been applied.
* Do not delete, insert, or update `schema_migrations` manually to bypass the runner.
* Do not terminate blockers, drop objects, run `down`, or restore backups without exact environment
  and impact confirmation.
* Do not retry a deterministic SQL error as though it were transient.
* Do not expose connection strings, credentials, private hosts, customer rows, or raw provider data.
* Do not resume rollout from a green health endpoint without schema and application proof.


# Diagnose a failed webhook (/docs/golden-paths/diagnose-failed-webhook)



# Golden path: diagnose a failed billing webhook [#golden-path-diagnose-a-failed-billing-webhook]

## When to use [#when-to-use]

Use this path when provider delivery returns non-2xx, a customer paid but local access did not
change, a lifecycle email is missing, a duplicate appears, payment recovery is wrong, or local
status disagrees with the selected provider. Treat this as a financial and access-control incident.

Do not manually toggle `users.is_premium` as the first response. Establish the signed event,
ordering, correlation, transaction, and entitlement facts.

## Files and boundaries [#files-and-boundaries]

* `packages/billing/src/providers/<selected>.ts`: signature verification, provider SDK parsing, and
  normalization.
* `apps/server/src/domains/billing/routes.ts`: public webhook endpoint and HTTP response.
* `apps/server/src/domains/billing/service.ts`: claim transaction and lifecycle processing.
* `apps/server/src/domains/billing/repository.ts`: event ledger and provider-neutral persistence.
* `apps/server/src/domains/billing/entitlements.ts`: access result after state is stored.
* `billing_webhook_events`: successfully committed `(provider, event_id)` claims.
* `subscriptions.provider_modified_at`: provider ordering authority.
* application logs/provider delivery history: request ID, event ID/type, response, retry.

Provider SDK objects do not cross into product domains. Email/notification work happens after the
billing transaction and cannot determine whether the webhook is acknowledged.

## Procedure [#procedure]

1. Capture the provider environment, event ID, event type, delivery time, response status, local
   request ID, subscription/customer IDs, and expected customer outcome. Never copy signatures,
   tokens, complete payloads, email addresses, or card data into a ticket.
2. Classify the failure:
   * 403: missing/invalid standard webhook ID or signature headers;
   * 503: webhook integration is intentionally unconfigured;
   * 5xx: verified event failed correlation, plan mapping, or state mutation and should retry;
   * 2xx with no new state: duplicate/stale/unhandled event or a defect;
   * state correct but message absent: post-commit notification failure.
3. Check `billing_webhook_events` for the exact provider/event ID. Presence means the state
   transaction committed. Absence after a 5xx means the claim rolled back and retry is safe.
4. Compare provider `modifiedAt` with `subscriptions.provider_modified_at`. A lower timestamp is
   stale and should be claimed without overwriting state. Never compare provider time with local
   `updated_at`.
5. Verify application-user correlation from provider customer `externalId` or signed
   `metadata.user_id`. Confirm that the UUID names an active local user. Repair provider metadata
   and retry the same event rather than inventing a local customer.
6. Verify the provider product maps to exactly one configured plan and agrees with signed
   `metadata.plan_type`. Repair deployment mapping or provider metadata; do not default to monthly.
7. Inspect the local subscription status/period and evaluate it through the documented policy.
   `past_due` denies paid access but keeps billing recovery available; a canceled subscription
   grants access only before its period end.
8. Retry from the provider only after the cause is corrected. Reusing the same event ID proves
   transactional retry; replaying a committed event must be a no-op.
9. If state committed but notification failed, resend the communication through a deliberate support
   path. Do not replay or mutate billing merely to trigger email.
10. Add a redacted regression fixture for any new failure class, then run the full DB-enabled suite.

## Verification [#verification]

```sh
vp check
vp run --filter @app/server typecheck
RUN_DB_INTEGRATION_TESTS=1 \
  DATABASE_URL='postgres://localhost:5432/app_billing_webhook_proof?sslmode=disable' \
  bun test --cwd apps/server src/domains/billing/service.integration.test.ts
vp run -r test
git diff --check
```

The integration proof must retain invalid signatures, concurrent identical delivery, rollback and
same-ID retry, missing-user repair, unknown product rejection, out-of-order provider timestamps,
payment failure, and entitlement-cache assertions.

## Security constraints [#security-constraints]

* Never bypass signature verification or expose the webhook secret.
* Do not paste raw provider payloads or customer data into git, Linear, chat, or fixtures.
* Query by opaque IDs and redact email/customer metadata from shared evidence.
* Do not manually grant access before establishing whether the provider reports payment.
* Keep recovery endpoints reachable while paid product APIs are denied.
* Use sandbox events for reproducible tests; never replay production events into local databases.

## Failure modes [#failure-modes]

* Looking only at `users.is_premium` and ignoring the subscription and event ledger.
* Treating a 2xx duplicate as evidence the original mutation succeeded without checking the claim.
* Retrying an invalid signature rather than fixing endpoint/secret configuration.
* A missing user is acknowledged and permanently lost.
* Local `updated_at` is mistaken for provider event ordering.
* An unknown product silently maps to an existing plan.
* Replaying billing state to repair a post-commit email.
* Making an ad hoc SQL update with no provider reconciliation or regression test.

## Rollback and diagnosis [#rollback-and-diagnosis]

If a code release caused failures, keep provider retries pending, restore the last verified webhook
processor, and replay only unclaimed events after the fix. Do not delete committed ledger rows to
force a replay; create an explicit reconciler if already-committed state was wrong.

If a migration caused the failure, follow the failed-migration runbook. Preserve evidence before
rollback and ensure schema/application versions agree. For a compromised webhook secret, rotate it
in the provider and the deployment, reject the old value, and do not log either secret.

## Acceptance criteria [#acceptance-criteria]

* The incident has a redacted event ID/type, expected outcome, actual status, and root cause.
* Signature, ledger claim, correlation, plan mapping, provider ordering, local state, and
  entitlement were checked.
* Retry or replay behavior is deterministic and does not duplicate state.
* Customer access and billing recovery match Decision 0003.
* A regression test covers any newly discovered class.
* Root checks and a generated-buyer diagnostic run pass with interventions recorded.

## Agent prohibitions [#agent-prohibitions]

* Do not disable signature verification or transactional event claiming.
* Do not delete event-ledger rows or toggle premium as a shortcut.
* Do not leak payloads, signatures, tokens, email addresses, or payment data.
* Do not compare provider timestamps to local write time.
* Do not acknowledge an uncorrelated paid subscription as successfully processed.
* Do not claim resolution until provider state, local state, access, and retry behavior agree.


# Remove an optional subsystem (/docs/golden-paths/remove-optional-subsystem)



# Remove an optional subsystem [#remove-an-optional-subsystem]

## When to use [#when-to-use]

Use this path when a buyer does not need a distributed optional integration or subsystem and wants
to remove its code, dependencies, configuration, support surface, and claims. Examples include
Sentry, Slack notifications, object-storage uploads, or billing when the resulting product contract
deliberately excludes that capability.

Disabling a key is not removal. Keep the subsystem when near-term use and update support justify its
maintenance cost. Remove it when a smaller dependency, data-processing, security, and operational
surface is the intended product outcome.

## Files and boundaries [#files-and-boundaries]

Inventory every applicable layer before editing:

* package manifests, workspace configuration, install policy, and `pnpm-lock.yaml`;
* application imports, initialization, providers, hooks, routes, adapters, and types;
* shared Zod contracts and generated Hono client consumers;
* environment schemas, examples, generated `.env` files, deployment ownership, and CI secrets;
* database tables, migrations, indexes, triggers, seed data, and backup assumptions;
* proxy/tunnel routes, CSP/CORS rules, PWA behavior, build plugins, and deployment configuration;
* tests, fixtures, docs, agent rules, architecture diagrams, product copy, pricing, and support
  claims;
* `packages/create-app` rewrite/filter logic and the final generated buyer repository.

Historical ADRs and explicitly historical documents may retain the name with clear context. Current
buyer and sales claims may not.

## Procedure [#procedure]

1. State the capability being removed, the buyer outcome, whether stored data exists, and the
   supported replacement or deliberate absence. Get explicit approval before deleting live data or
   an external provider project.

2. Capture a pre-change inventory:

   ```sh
   rg -n -i "subsystem|package-name|ENV_PREFIX" . \
     -g '!node_modules/**' -g '!**/dist/**' -g '!docs/phase2/**'
   pnpm --filter <owning-workspace> why <package-name>
   ```

3. Classify each hit as runtime, build, environment, persistence, test, documentation, commercial
   claim, generated artifact, or intentional history. Write the expected removal list before editing
   so an empty search is meaningful evidence.

4. Remove top-down entry points first so the compiler exposes dependents: initialization/providers,
   user flows, domain routes/services, provider adapters, configuration constants, then packages.

5. Remove environment variables from examples, validators, deployment maps, CI, hosting settings,
   and setup docs. A removed integration must not leave a secret request or silent no-op.

6. Remove provider-specific routes, tunnels, webhooks, CSP/CORS hosts, source-map upload plugins,
   scheduled triggers, or public callbacks that no longer have an owner.

7. If persisted data exists, design a reversible retirement migration and export/retention policy.
   After a release, never edit an applied migration. Before the first commercial tag, follow the
   documented baseline exception and prove `up → down → up`.

8. Remove dependencies with the package manager and regenerate—not hand-edit—the lockfile:

   ```sh
   vp install --lockfile-only
   vp install --frozen-lockfile
   ```

9. Repeat the inventory search. Inspect remaining lockfile/package hits with `pnpm why`; a
   transitive occurrence is not residue when another retained package owns it.

10. Run checks, tests, builds, zero-key setup, and dependency audit. Exercise the nearest affected
    UI and failure boundary.

11. Generate a custom-scope/custom-brand buyer app, repeat frozen install/check/test/build there,
    and verify the removed subsystem is absent from buyer source, environment, and direct lockfile
    importers.

12. Record exact changed files, commands, scan exclusions, package-count change, failures, and
    interventions. A same-context author rehearsal is useful engineering evidence but is not a
    cold-agent claim.

## Verification [#verification]

Minimum:

```sh
vp install --frozen-lockfile
vp check
vp run -r test
vp run -r build
pnpm audit --audit-level high
rg -n -i "subsystem|package-name|ENV_PREFIX" . \
  -g '!node_modules/**' -g '!**/dist/**' -g '!docs/phase2/**'
```

For a generated buyer, also inspect `git diff --name-status`, the owning `package.json`,
`pnpm-lock.yaml` importers, `.env.example`, initialization, build configuration, current docs, and
the production bundle. If persistence changes, run disposable-database migration reversal and domain
integration tests.

## Security constraints [#security-constraints]

* Do not delete live provider projects, buckets, data, webhooks, domains, or secrets without exact
  target resolution and explicit authority.
* Remove public callbacks, tunnels, allowed origins, and secret inputs when their handler is gone.
* Preserve audit/export/retention obligations before dropping customer data.
* Never paste provider credentials into scans, evidence, test fixtures, git diffs, or Linear.
* Do not weaken CSP, authentication, error handling, or test coverage merely to remove an adapter.
* Verify logout/session cleanup when removing analytics or identity integrations that retain a
  browser identity.
* Treat package removal as supply-chain change: update the canonical lockfile and rerun the audit.

## Failure modes [#failure-modes]

* Build still imports the package: an entry point, hook, provider, or lazy route remains.
* Frozen install reports lock drift: the manifest changed without regenerating `pnpm-lock.yaml`.
* Search finds an environment key: setup, CI, Vercel/Railway/Dokploy configuration, or docs still
  request it.
* A route returns 404 unexpectedly: a shared route tree was removed with the optional branch.
* Historical docs fail the residue scan: narrow the exclusion to clearly marked history; do not
  delete decision evidence to manufacture zero results.
* The package remains in the lockfile: use `pnpm why` to distinguish direct importer residue from a
  legitimate transitive owner.
* A generated app still contains it: create-app copied a studio-only file, stale lock importer,
  generated environment, or excluded path.
* Product copy still promises the capability: engineering removal and commercial scope diverged.

## Rollback and diagnosis [#rollback-and-diagnosis]

Restore the smallest reviewed commit or patch containing the full vertical slice, regenerate the
lockfile, and rerun the same verification. Restore environment values from the deployment secret
manager, never from documentation or git.

If data was retired, execute the reviewed down/restore plan only against the resolved target and
confirm row/object counts before and after. If no reversible data path exists, the removal is
destructive and requires explicit approval before execution.

## Acceptance criteria [#acceptance-criteria]

* The removal inventory covers source, package, environment, persistence, CI, deployment, tests,
  docs, generated buyers, and commercial claims.
* No orphan direct dependency, importer, initialization, route, migration assumption, environment
  key, deployment setting, or current claim remains.
* Remaining search/lockfile hits are classified and owned.
* No-key local setup, frozen install, audit, `vp check`, tests, and builds pass.
* A custom-scope/custom-brand generated buyer passes the same path without the subsystem.
* Data retention and rollback are explicit when persistence is involved.
* Evidence distinguishes author rehearsal from genuinely cold execution.

## Agent prohibitions [#agent-prohibitions]

* Do not call a feature removed merely because its key is unset.
* Do not hand-edit `pnpm-lock.yaml` or generated artifacts.
* Do not delete shared infrastructure based only on a matching filename.
* Do not edit a shipped migration to erase history.
* Do not delete an external resource or customer data without explicit authority.
* Do not leave compatibility stubs, dead environment variables, empty routes, or stale sales claims.
* Do not claim a cold removal when the same agent authored the runbook or knew the expected diff.
