How a change reaches production. Four steps, with the same names and commands in every repository. All of it is a default. Skip or change anything when the app has a reason, and write the reason in that repository's release.md.
The New App Workflow gets an app to its first release. This is every change after that.
Main is always releasable.
Preview and production both build from main. An app that can't keep this says why in release.md and names what replaces it, usually a release branch.
| # | Step | What it is | How work gets in | Outside providers (Stripe, Resend) |
|---|---|---|---|---|
| 01 | Local | Runs on a laptop against stand-ins. No live data, no live accounts. | Anyone, any time. | A stand-in, so no key is needed. Test mode with keys in .env.local when the real API matters. Email renders locally and nothing sends. |
| 02 | Checks | Lint, tests, and types on every pull request. A failing change doesn't move on. | Automatic. | No network. Tests mock the provider at its seam. |
| 03 | Preview | A private site built from a branch. Real code, real look, no way to harm live data. The client and Will review here. | Automatic on push. No approval, because preview can't hurt anything. | Test mode. Stripe test keys and cards. Resend limited to an allow-list or its test addresses. Webhooks point at the preview address with their own secret. |
| 04 | Production | The live site. One command to release, one to go back, a record of each. | A human says go, in writing, after seeing the preview. | Live keys, only in the host's production environment. Prices, webhooks, and sending domains are live settings with their own go. |
Into main: a pull request with checks green and a review. Merging is not a release. Back out: one command restores the previous deployment and verifies. Live settings, domains, schema, jobs, and provider settings each get their own go.
| Default | Why |
|---|---|
| Departures are written down | In release.md, so nobody rediscovers them. |
The four names and the six commands: dev, check, preview, release, verify, rollback | "Staging" means preview. The host can differ. The words a person or agent types don't. |
One release.md with the same headings | Addresses, jobs, providers, record format, restore steps, departures. |
The gate sentences in AGENTS.md | Merge is not a release. No live change without a go. |
| Preview can't change live data | A read-only role, or a separate copy. The rule is fixed. The mechanism isn't. |
| Nothing real fires from preview | No jobs, no customer email, no live-mode writes. Test-mode traffic is fine. |
| Secrets live in the host, per environment | Local uses .env.local. The repo holds .env.example with names only. |
| Agent permissions are committed | Claude Code: .claude/settings.json. Codex: .codex/rules/. Nothing depends on one laptop. |
What differs: the host (Vercel, Cloudflare), the login provider (Memberful, Clerk), the database (Supabase, Convex), the outside providers, scheduled work, the runtime version. All of it sits behind the names above.
Thin wrappers, one job each. Policy lives in AGENTS.md and the host's settings, not here.
| Command | Does | Leaves alone |
|---|---|---|
| pnpm dev | Starts the app against stand-ins | Live services |
| pnpm check | Lint, tests, types, runtime pinned | Deploying |
| pnpm preview | Puts the work where the preview builds from | Production |
| pnpm release | Builds from a clean copy of main, deploys, verifies, prints the record | Running without the go |
| pnpm verify | Read-only checks: addresses, deployment id, job owner, provider mode | Changing anything |
| pnpm rollback | Returns production to the previous deployment, then verifies | Rebuilding code |
The workflow lives at the root, where a person or agent starts. The apps hold the how.
| What | Where |
|---|---|
| The commands | Root package.json, suffixed per app: pnpm release:web. Each calls the app's own scripts. pnpm check alone runs every app. |
release.md | One file at the root, docs/agents/release.md, with a Facts and Commands block per app. |
| The mechanics | Inside each app: host config, build scripts, .env.example. |
| The record | Names the app. Releasing one says nothing about its siblings. |
One app, no suffix. A block that outgrows the file moves to docs/agents/release/<app>.md, same headings.
# <Repo> release Merging to main is not a release. <Who> reviews the preview and says go. ## Departures <defaults this repo skips or changes, and why> ## Facts: <app> | Host project | <name, id> | | Production addresses | <list> | | Preview address | <address>, protected by <method> | | Preview data | <read-only role or separate copy> | | Scheduled jobs | <list>; production only | | Providers | <name>: test mode in preview, live in production | | Records | <ticket tracker> | ## Commands: <app> | Local | `pnpm dev:<app>` | | Checks | `pnpm check:<app>` | | Preview | `pnpm preview:<app>` | | Release | `pnpm release:<app>` | | Verify | `pnpm verify:<app> --env production` | | Rollback | `pnpm rollback:<app> <deployment>` | ## Record template <date, app, commit, deployment id, addresses, checks, restore target> ## Restore <steps, and what a rollback does not restore> ## Agent tool notes <limits, workarounds, where each agent's permission file is>
Open pull requests. Do not merge without the user's instruction. Merging is not a release. Release, roll back, or change live settings, domains, schema, jobs, or provider settings only after the user's explicit go. Preview can't change live data, runs no jobs, and talks to providers in test mode. Use it for every review. Local uses stand-ins and .env.local. Never copy production values into the repository or a record. Departures and tool limits: docs/agents/release.md.