Skip to content
Back to skills

Deploy

ASecurity

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/.

  • 10 stars
  • 0 votes
  • 0 copies
  • 1 view
  • Added September 12, 2026
toolsgoshellbashsqlnodenodejsgitapidatabase

Works with

  • cli
  • api
  • mcp

Security analysis

A92/100
  • mediumInstalls packages at runtime which could introduce malicious dependencies

Pro shows the line behind each finding and how to fix it

Scanned September 12, 2026

npx -y skills add FriendlyInternet/nuxt-crouton --skill deploy --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Deploy?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Deploy
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/friendlyinternet-deploy/badge)](https://www.skillsdirectory.com/skills/friendlyinternet-deploy)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
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`**.

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…