Why
Today every change — including D1 schema migrations — flies straight to prod via the Cloudflare Git integration. With Pro subscriptions about to go live (#185 unblocks), the cost of a broken migration goes from "Cory notices and fixes" to "paying customer sees an outage." The expand-contract migration discipline already in CLAUDE.md is good, but it's a convention, not a backstop.
This issue stands up a minimum-viable staging environment that costs ~$0/mo and adds ~30 min of one-time setup, then makes migrations land in staging before prod.
Out of scope: gradual / versioned rollouts (`wrangler versions deploy`) — that's a separate track for risky code changes, filed separately if/when needed. This issue is specifically about the schema safety net.
Scope
1. Cloudflare resources
- Create D1 database `dmarcheck-db-staging`
- Add DNS: `staging.dmarc.mx` → Worker (Cloudflare DNS, no extra cost)
- Create staging WorkOS environment (sandbox tier, free)
- Reuse existing Stripe test mode keys (no new account needed)
- Cloudflare Web Analytics: do NOT add a token for staging — keep beacon off there
2. `wrangler.toml`
Add an `[env.staging]` block:
```toml
[env.staging]
name = "dmarcheck-staging"
route = { pattern = "staging.dmarc.mx/*", zone_name = "dmarc.mx" }
[[env.staging.d1_databases]]
binding = "DB"
database_name = "dmarcheck-db-staging"
database_id = ""
```
Wrangler secrets to set on the staging env (separate from prod):
- `WORKOS_API_KEY` / `WORKOS_CLIENT_ID` (sandbox)
- `STRIPE_SECRET_KEY` / `STRIPE_WEBHOOK_SECRET` / `STRIPE_PRICE_ID_PRO` (test mode)
- `SESSION_SECRET` (different from prod — staging sessions must not be valid in prod)
- (No `CF_ANALYTICS_TOKEN` — keep beacon off staging)
3. Sentry
- Don't create a separate Sentry project — set `environment: "staging"` in the SDK init based on a build-time flag or a `STAGING=1` env var
- Optionally bump sample rate to 1.0 in staging (everything is interesting there)
4. Migration promotion workflow
Update `.github/workflows/migrate.yml` so the safe path is staging-first:
```
push to main
→ CI passes
→ migrate.yml runs:
step 1: wrangler d1 migrations apply dmarcheck-db-staging --remote
step 2: GET https://staging.dmarc.mx/health (must 200)
step 3: wait for manual approval (environments: prod)
step 4: wrangler d1 migrations apply dmarcheck-db --remote
```
Implementation notes:
- Use GitHub Environments with required reviewers on the `prod` environment for the approval gate. Cory is the only reviewer; one click promotes.
- Steps 1-2 run unattended; if step 2 fails, step 4 never runs and main stays partially deployed (which is fine because of the additive-only migration discipline already in CLAUDE.md).
- The Cloudflare Git integration deploys the code to prod independently — the migration workflow only gates the schema. Code-vs-schema ordering still relies on the existing additive-only discipline.
5. README + CLAUDE.md updates
- Add a "Staging" section under Database migrations describing the new flow
- Document that `staging.dmarc.mx` is a non-public testing surface (`noindex` enforced) and that anyone who finds it should not file issues against it
- Add a `<meta name="robots" content="noindex,nofollow">` injection on staging via an env-var check in `src/views/html.ts`
6. CI: deploy staging on every main commit
The Cloudflare Git integration currently deploys main → prod. Add a parallel auto-deploy for staging via either:
- A second connection in the Cloudflare dashboard targeting the `env.staging` config, or
- A GitHub Actions workflow that runs `wrangler deploy --env staging` on push to main (uses a Workers Edit token, scope-restricted)
The first option is lower-friction; the second gives us versioned-deploy headroom if we want `wrangler versions deploy` later.
Acceptance criteria
Estimate
~30–45 min of one-time Cloudflare/WorkOS/GitHub config + ~50–80 lines across `wrangler.toml`, `migrate.yml`, `html.ts`, README, CLAUDE.md. Single PR.
Follow-ups (separate issues, not part of this)
Why
Today every change — including D1 schema migrations — flies straight to prod via the Cloudflare Git integration. With Pro subscriptions about to go live (#185 unblocks), the cost of a broken migration goes from "Cory notices and fixes" to "paying customer sees an outage." The expand-contract migration discipline already in CLAUDE.md is good, but it's a convention, not a backstop.
This issue stands up a minimum-viable staging environment that costs ~$0/mo and adds ~30 min of one-time setup, then makes migrations land in staging before prod.
Out of scope: gradual / versioned rollouts (`wrangler versions deploy`) — that's a separate track for risky code changes, filed separately if/when needed. This issue is specifically about the schema safety net.
Scope
1. Cloudflare resources
2. `wrangler.toml`
Add an `[env.staging]` block:
```toml
[env.staging]
name = "dmarcheck-staging"
route = { pattern = "staging.dmarc.mx/*", zone_name = "dmarc.mx" }
[[env.staging.d1_databases]]
binding = "DB"
database_name = "dmarcheck-db-staging"
database_id = ""
```
Wrangler secrets to set on the staging env (separate from prod):
3. Sentry
4. Migration promotion workflow
Update `.github/workflows/migrate.yml` so the safe path is staging-first:
```
push to main
→ CI passes
→ migrate.yml runs:
step 1: wrangler d1 migrations apply dmarcheck-db-staging --remote
step 2: GET https://staging.dmarc.mx/health (must 200)
step 3: wait for manual approval (environments: prod)
step 4: wrangler d1 migrations apply dmarcheck-db --remote
```
Implementation notes:
5. README + CLAUDE.md updates
6. CI: deploy staging on every main commit
The Cloudflare Git integration currently deploys main → prod. Add a parallel auto-deploy for staging via either:
The first option is lower-friction; the second gives us versioned-deploy headroom if we want `wrangler versions deploy` later.
Acceptance criteria
Estimate
~30–45 min of one-time Cloudflare/WorkOS/GitHub config + ~50–80 lines across `wrangler.toml`, `migrate.yml`, `html.ts`, README, CLAUDE.md. Single PR.
Follow-ups (separate issues, not part of this)