Deploy a crouton app to Cloudflare Workers STAGING (auto-provisioning) — the DEFAULT deploy, staging only, never production. Handles the staging bootstrap (auto-creates D1+KV, syncs ids, migrates), wiring CI, routine staging deploys, and Pages→Workers migration. For production use the separate /deploy-production skill. Use when deploying any app in apps/.
Scanned 9/12/2026
Install to Claude Code
npx -y skills add FriendlyInternet/nuxt-crouton --skill deploy --agent claude-codeInstalls into .claude/skills of the current project.
Are you the author of Deploy?
Add the live security badge to your README — it updates automatically with every re-scan.
[](https://www.skillsdirectory.com/skills/friendlyinternet-deploy)More formats (shields.io, HTML) on the badges page.
---
name: deploy
layer: stack
description: Deploy a crouton app to Cloudflare Workers STAGING (auto-provisioning) — the DEFAULT deploy, staging only, never production. Handles the staging bootstrap (auto-creates D1+KV, syncs ids, migrates), wiring CI, routine staging deploys, and Pages→Workers migration. For production use the separate /deploy-production skill. Use when deploying any app in apps/.
allowed-tools: Bash, Read, Grep, Glob, Edit, Agent, AskUserQuestion
---
# Deploy Skill — Cloudflare Workers
Deploys a crouton app to **Cloudflare Workers (static assets)** — the crouton
deploy standard (#108). Wrangler **auto-provisions** the app's D1 + KV on the
first deploy, so there's no manual resource/project creation, no id-juggling.
> **🟦 STAGING ONLY — this skill never deploys to production.** "Deploy" defaults to
> **staging** here. Shipping to production is a deliberate, human-initiated action handled
> by the separate **`/deploy-production`** skill. Never deploy production as part of routine work.
> **Not Cloudflare Pages.** We do NOT use `wrangler pages …`, `pages_build_output_dir`,
> or the Pages "strip env" step anymore. If you find those, the app is on the old
> Pages path — see **Migrating a Pages app → Workers** below.
## Environment & domain convention (#133)
Two environments, two domains — kept on **separate registrable domains** so a
staging session can never authenticate against production (cookie isolation):
| Env | wrangler env | Worker | Domain |
|-----|--------------|--------|--------|
| **production** | top-level | `<app>` | `<app>.friendlyinter.net` |
| **staging** | `env.staging` | `<app>-staging` | `<app>.pmcp.dev` (public) |
The deploy-env is named **`staging`** (not `preview`): scripts are `cf:staging` /
`db:migrate:staging`, deploys use `--env staging`. (The general `crouton` CLI stays
domain-agnostic via `--domain <zone>`; the friendlyinter.net/pmcp.dev split is this
monorepo's convention, applied per app at its production cutover — #136 for triage.)
## Usage
```
/deploy # Deploy current app to STAGING (auto-detected from cwd)
/deploy velo # Deploy a specific app to STAGING
# production → use the separate /deploy-production skill
```
## Rules
1. **STAGING ONLY.** This skill deploys to **staging**, never production (that's the separate `/deploy-production` skill). Always confirm the target app first.
2. **Workers, not Pages** — `NITRO_PRESET=cloudflare_module`, output in `.output/`, deploy with `wrangler deploy` (never `wrangler pages deploy`).
3. **NEVER manually create D1/KV** — they auto-provision from the **id-less** `wrangler.jsonc` on first deploy. After provisioning, run `sync:ids` and **commit** the written-back ids (remote `d1 migrations apply` needs them — workers-sdk#13632).
4. **NEVER skip `nuxt prepare` before build** in CI — rolldown tsconfig bug. (Locally, the `cf:*` scripts assume `node_modules`/`.nuxt` are prepared from `pnpm install`.)
5. **`hub: { db: 'sqlite' }`** — never `hub: { database: true }`.
6. **`postinstall` must be guarded** — `nuxt prepare 2>/dev/null || true`, never bare (a bare prepare aborts the whole-monorepo install and fails every app's deploy).
## How the pipeline works (one source of truth)
The deploy logic lives in the app's **`package.json` scripts** — the same commands
you run locally and that CI runs. Don't reinvent them step-by-step:
- **`cf:deploy`** (production — run **only** via the `/deploy-production` skill): `build → wrangler deploy (auto-provision) → sync:ids → d1 migrations apply --remote`
- **`cf:staging`** (isolated staging env): `build → inject-wrangler-env → wrangler deploy --env staging → sync:ids → inject-wrangler-env → d1 migrations apply --env staging --remote`
- **`sync:ids`** — queries wrangler, writes provisioned ids back into `wrangler.jsonc`
- **`db:migrate` / `db:migrate:prod` / `db:migrate:staging`** — D1 migrations (local / remote / staging-remote)
A freshly scaffolded app (`crouton init`) already ships all of this:
`wrangler.jsonc` (id-less), `scripts/sync-wrangler-ids.mjs`,
`scripts/inject-wrangler-env.mjs`, `drizzle.config.ts`, the chained scripts, the
CF stubs + nitro aliases, and the guarded postinstall.
## Workflow
### Step 1: Detect app
- arg → `apps/{arg}/`; else if cwd is inside an app → that app; else ask.
- Verify it has `wrangler.jsonc` + `package.json`.
### Step 2: Pre-flight (run in parallel)
Confirm the app is Workers-ready:
1. **`wrangler.jsonc`** present, **Workers-style** (has `compatibility_flags: ["nodejs_compat"]`, `d1_databases`/`kv_namespaces`; **no** `pages_build_output_dir`).
2. **Scripts** `scripts/sync-wrangler-ids.mjs` + `scripts/inject-wrangler-env.mjs` exist.
3. **`drizzle.config.ts`** exists (so `db:generate` works).
4. **Package scripts** — `cf:deploy` is the Workers chain; `postinstall` is guarded.
5. **CF stubs** — `server/utils/_cf-stubs/` exists; `nuxt.config.ts` has `nitro.alias` for passkey/webauthn/papaparse stubs and pins **no** preset.
6. **Auth** — `CLOUDFLARE_ACCOUNT_ID` + `CLOUDFLARE_API_TOKEN` available in the environment (see **Credentials**).
If anything is missing and the app is on the old Pages setup → **Migrating a Pages app → Workers**. If it's just missing files, copy them from `apps/velo` (the reference) or re-run the scaffolder.
### Step 3: First (bootstrap) staging deploy
The id-less bindings auto-provision here. Confirm with the user, then deploy the
isolated **staging** environment (its own auto-provisioned D1+KV):
```bash
cd apps/{app}
pnpm cf:staging # provisions + deploys the *-staging worker, migrates --env staging
```
Then **commit the written-back ids** (bootstrap → committed):
```bash
git add apps/{app}/wrangler.jsonc && git commit -m "chore({app}): commit provisioned staging D1/KV ids"
```
> The **production** bootstrap (`cf:deploy`, prod D1+KV, `<app>.friendlyinter.net`) is a
> deliberate, separate step — see the **`/deploy-production`** skill. This skill stops at staging.
> If you're an agent **without Cloudflare egress** (sandbox), you can't run these —
> verify what's verifiable (config, `pnpm sync:ids --dry-run` logic) and have the
> user run the CF-gated steps, pasting output (the #109/#113/#114 loop).
### Step 4: Wire CI (opt in via `deploy.config.json`)
There is **one generic workflow for all apps** — `.github/workflows/deploy-apps.yml`
(#481/#638; the old per-app `deploy-<app>.yml` callers are retired — don't create one).
An app opts in by adding a `deploy.config.json` next to its `package.json`. **Model on
`apps/velo/deploy.config.json`.** Set: `stagingUrl`, `productionUrl`, `layerPackages`,
and `watchPaths` (the app + its extended `crouton*` packages + lockfile). The workflow's
`detect` job matches changed files against `watchPaths` and fans out one reusable
`deploy-app.yml` call per affected app. Merge to `main`/open a PR → isolated staging
with the URL commented on the PR; manual dispatch (app + environment inputs) →
production (#347). The fan-out uses `secrets: inherit`.
Ensure **repo-level** secrets `CLOUDFLARE_ACCOUNT_ID` + `CLOUDFLARE_API_TOKEN` exist
(Settings → Secrets and variables → Actions).
### Step 4.5: App (Worker) secrets
The app's own secrets (`BETTER_AUTH_SECRET`/`BETTER_AUTH_URL`, `NUXT_*`, etc.) live
on the **Worker**, NOT in `wrangler.jsonc`. **Worker secrets persist across deploys**,
so this is a one-time bootstrap per worker (prod + staging), not a per-deploy step.
Two ways:
- **Manual (one-time):** `npx wrangler secret bulk secrets.json` (prod) /
`… --env staging` (staging). `BETTER_AUTH_URL`/`BASE_URL` must be the **production
domain** (not localhost). Pages secrets do NOT carry over — re-provide the values.
- **Automatic (CI):** store the whole bundle as a **repository-level** Actions secret
`WORKER_SECRETS_JSON` (a JSON object of `{ "NAME": "value", … }`). It MUST be
repo-level, NOT an Environment secret — the deploy job is reached via
`secrets: inherit` from caller jobs that declare no `environment:`, so an
Environment-scoped secret resolves EMPTY with no error and the Worker deploys
without secrets (#1094). The reusable `deploy-app.yml` runs `wrangler secret bulk`
from it on every deploy (`--env staging` for non-prod). Omit it to manage secrets
manually. If the app depends on the bundle, set `"secrets": { "required": true }`
in its `deploy.config.json` — an empty resolution then FAILS the deploy instead
of silently skipping. Automation can't invent values — they must live in that
secret once.
### Step 5: Routine deploys (staging)
- **CI (preferred):** merge to `main` (or open a PR) → the caller runs the staging pipeline (#347).
- **Local:** `pnpm cf:staging` from the app dir.
- **Production is never routine** — ship it deliberately via the **`/deploy-production`** skill.
### Auto-seeded review login on staging previews (#608)
Every **staging** deploy auto-seeds a throwaway, loginable test account on the
preview's isolated D1 so a reviewer can open the URL and be **inside the app in one
step** — no register → create-team wall. `deploy-app.yml` runs
`scripts/seed-review-login.mjs` against the deployed Worker (the app's own
`/api/auth/sign-up/email` + a team via `organization/create` when the app doesn't
auto-make one), then prints a `🔑 Test login` block in the PR's staging comment.
Creds are **deterministic per preview** (so redeploys reprint the same working login,
no user pile-up) and the step is best-effort (never fails the deploy). Optional repo
secret `REVIEW_SEED_SECRET` salts the password; **production seeds nothing**.
## Migrating a Pages app → Workers
For an app still on the Pages setup (`wrangler.toml`, `pages_build_output_dir`,
`wrangler pages deploy`):
1. **`wrangler.toml` → `wrangler.jsonc`** in the Workers shape (see `apps/velo`):
drop `pages_build_output_dir`; keep `name`/`compatibility_*`; `d1_databases` (reuse
the existing prod `database_id`), `kv_namespaces`; add an `env.staging` block with a
**separate** `{app}-staging-db` + KV (id-less to auto-provision, or existing staging ids).
2. **Add** `scripts/sync-wrangler-ids.mjs`, `scripts/inject-wrangler-env.mjs`,
`drizzle.config.ts` (copy from `apps/velo`).
3. **package.json** — replace the Pages `cf:*` scripts with the Workers chain
(`NITRO_PRESET=cloudflare_module`, `sync:ids`, `db:migrate:staging`); keep the
guarded `postinstall`.
4. **nuxt.config.ts** — remove `nitro.preset: 'cloudflare-pages'` (keep the `nitro.alias` stubs).
5. **CI** — replace `deploy-{app}.yml` (+ any `-preview.yml`) with the thin caller from Step 4; delete the Pages strip-env step (not needed on Workers).
6. **Deploy + commit ids** as in Step 3.
## Credentials
The job/shell needs **`CLOUDFLARE_ACCOUNT_ID`** + **`CLOUDFLARE_API_TOKEN`**.
- **`CLOUDFLARE_ACCOUNT_ID`** — dashboard → Workers & Pages → Account ID (also the hex in the dashboard URL). Not secret.
- **`CLOUDFLARE_API_TOKEN`** — My Profile → API Tokens → **Create Custom Token**. For Workers + **auto-provisioning** the token needs (Account-scoped):
- **Workers Scripts: Edit**
- **D1: Edit**
- **Workers KV Storage: Edit**
- (**Workers R2 Storage: Edit** if the app uses blob)
Cloudflare shows a token's value **only once**, and GitHub never reveals a saved
secret — so mint a fresh dedicated token rather than reusing one.
> Note: this differs from the old Pages token (which used *Cloudflare Pages: Edit*).
> A Pages-only token will fail to auto-provision D1/KV.
## Troubleshooting
### `Couldn't find a D1 DB … missing database_id` (on migrate)
The first deploy provisioned the DB but the id isn't in `wrangler.jsonc` yet. Run
`pnpm sync:ids` (after a deploy) and commit the result. `cf:deploy`/`cf:staging` do
this automatically.
### `Configuration file does not support "env"` / redirected config rejects env
Wrangler 4.64+ rejects `env` in a *redirected* config. `scripts/inject-wrangler-env.mjs`
(run by `cf:staging`) re-injects `env` into `.output/server/wrangler.json` and removes
the redirect so `--env staging` deploys read it directly. No manual strip step.
### `papaparse` RollupError / passkey/tsyringe errors
Add the CF stubs + `nitro.alias` (see scaffolder output / `apps/velo`).
### KV namespace not found by `sync:ids`
It matches the auto-provisioned title `<worker-name>-<binding>` (e.g.
`{app}-KV`, `{app}-staging-KV`). The script logs the available titles if no match —
adjust only if your account names them differently.
### Build OOM
Set `NODE_OPTIONS='--max-old-space-size=8192'` (CI sets this).
## Deploy Learnings Location
Per-app deploy gotchas: `docs/projects/{app}/{app}-deploy.md`. Append new fixes there.
Reference implementation for everything above: **`apps/velo`**.
Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.
No comments yet. Be the first to comment!