name: new-tanstack-app
description: Orchestrate scaffolding a new TanStack Start app on the canonical stack (TanStack Start + Drizzle + Neon Postgres + Cloudflare Workers + shadcn/ui). Dispatches to sub-skills for DB (Neon default; D1 via --db sqlite), auth (Better Auth by default: identity in your own DB, agent-readable, default sign-in = email OTP via Resend (iOS one-tap autofill, no OAuth-app setup), Google + passkeys opt-in; Clerk as the hosted-UI consideration; WorkOS AuthKit at B2B 100K+ MAU; Cloudflare Access for single-user), observability (PostHog, Sentry, UptimeRobot), DNS, ship; plus optional agentic runtime (XState + Vercel AI SDK, LangGraph Phase-2 POA) and Knock notifications. Use when user wants to start, create, scaffold, bootstrap, or kick off a new TanStack Start project / small app / side project.
category: project-setup
argument-hint: [--db sqlite] [--auth] [--posthog] [--sentry] [--uptime] [--agent xstate|langgraph] [--ai-sdk] [--knock] [--domain ] [--skip-deploy] [--skip-ci] [--skip-styleguide] [--interactive]
allowed-tools: Bash(pnpm *) Bash(pnpx *) Bash(wrangler *) Bash(git *) Bash(corepack *) Bash(mkdir *) Bash(cd *) Bash(cp *) Read Write Edit
New TanStack App (orchestrator)
Scaffold a new TanStack Start app, then dispatch to sub-skills for the pieces the user wants. Target: $0/mo at small scale, one evening to a working deploy.
Usage
/ro:new-tanstack-app my-app # baseline: Neon Postgres, no auth, no observability, deploy
/ro:new-tanstack-app my-app --interactive # asks what to wire (uses AskUserQuestion)
/ro:new-tanstack-app my-app --db sqlite # D1 (SQLite) instead of Neon, for edge-cache / CLI shapes
/ro:new-tanstack-app my-app --auth # + Better Auth (default); --auth=clerk for hosted UI; --auth=workos for B2B-at-scale
/ro:new-tanstack-app my-app --posthog --sentry --uptime # + full observability
/ro:new-tanstack-app my-app --agent xstate --ai-sdk # + XState decision machine + Vercel AI SDK (Anthropic/OpenAI/Gemini)
/ro:new-tanstack-app my-app --knock # + Knock (multi-channel notifications: Slack + email + in-app)
/ro:new-tanstack-app my-app --domain api.ronan.dev # + custom domain via /ro:cloudflare-dns
/ro:new-tanstack-app my-app --skip-deploy # scaffold only, no deploy
/ro:new-tanstack-app my-app --auth --agent xstate --ai-sdk --knock --posthog --sentry --uptime --domain app.ronan.dev # full agentic app
What it actually does
This skill is an orchestrator — it owns the baseline scaffolding (scaffold / UI / testing / hygiene) and delegates everything else to sibling skills. That keeps each piece evolvable on its own.
/ro:new-tanstack-app <app> [flags]
1. scaffold + CF adapter + wrangler binding (inline)
2. DB wiring:
default (Neon) → /ro:neon install + project + push-secret
--db sqlite → inline D1 wiring
3. UI: tailwind + shadcn + lucide (inline)
4. Testing + API docs → /ro:testing-stack install
5. Code hygiene: prettier + eslint + husky + commitlint (inline)
6. --auth → /ro:better-auth install (default); --auth=clerk → /ro:clerk install (hosted UI); --auth=workos → /ro:workos install (B2B at 100K+ MAU)
6a. Design system + /styleguide route → /ro:design-system-create --showcase (default-on; consumes requireRole() if --auth, dev-only fallback otherwise)
7. --ai-sdk → install `ai` + `@ai-sdk/anthropic` + `@ai-sdk/openai` + `@ai-sdk/google`; scaffold `lib/models.ts`
8. --agent xstate → install `xstate` + `@xstate/react`; scaffold a reference `machines/exampleMachine.ts` + actor using AI SDK
8b. --agent langgraph → Phase-2 POA (not yet auto-scaffolded) — prints migration POA instead
9. --knock → install `@knocklabs/node` + `@knocklabs/react`; scaffold `/api/notify` route stub
10. --posthog → /ro:posthog install --both
11. --sentry → /ro:sentry install --tanstack + project create
12. --uptime → /ro:uptimerobot monitor create (post-deploy)
13. --domain → /ro:cloudflare-dns add <host> (post-deploy)
14. deploy → /ro:cf-ship (unless --skip-deploy)
15. GitHub CI → add .github/workflows/ci.yml (quality gate + auto-deploy)
15a. GitHub branch protection + squash-only merges (see /ro:stacked-prs for the rebase flow)
16. final commit → /ro:commit (emoji format)
Prerequisites
- Node 20+
pnpm(install:corepack enable pnpm)wrangler4.x —pnpm add -g wrangler(skill checks and offers to install)CLOUDFLARE_API_TOKENin~/.claude/.envwith Workers Scripts + Account Settings + Zone DNS scopesNEON_API_KEYin~/.claude/.env(required for the default Neon DB path; create at console.neon.tech)- Git configured
- For optional flags, the corresponding env vars must be set (skill checks):
--db sqlite→ no extra env var needed (D1 is managed via wrangler)--posthog→POSTHOG_PERSONAL_API_KEY,POSTHOG_HOST,POSTHOG_INGEST_HOST--sentry→SENTRY_AUTH_TOKEN,SENTRY_ORG,SENTRY_REGION_URL--uptime→UPTIMEROBOT_API_KEY--ai-sdk→ at least one ofANTHROPIC_API_KEY,OPENAI_API_KEY,GOOGLE_GENERATIVE_AI_API_KEY(pushed to Worker as a secret, not~/.claude/.env-only)--knock→KNOCK_API_KEY(pushed as a Worker secret)--domain→CLOUDFLARE_API_TOKENwithZone:DNS:Edit
Interactive mode (--interactive)
Runs an AskUserQuestion preamble to collect:
- Database — Neon Postgres (default) or D1 SQLite (use
--db sqlitefor edge-cache / CLI shapes)? - Auth: Cloudflare Access + WARP (recommended for single-user / internal / personal apps, edge-gated, phishing-resistant, no in-app login), Better Auth (default for multi-user small SaaS: identity in your own DB, agent-readable, default sign-in = email OTP via Resend with iOS one-tap autofill, Google + passkeys opt-in), Clerk (the hosted-UI consideration: drop-in components, managed dashboard, fastest first sign-in), WorkOS (alt-at-scale: 100K+ MAU, Admin Portal, SAML SSO), or none? In all cases offer passkeys; see the authentication-hardening playbook.
- Agent runtime — None / XState (MVP: prescriptive decision machine) / LangGraph POA (Phase 2 migration notes only)?
- LLM provider abstraction — Install Vercel AI SDK + provider packs?
- Notifications — Knock (multi-channel) / Resend-only / none?
- Observability — Which of [PostHog, Sentry, UptimeRobot]?
- Custom domain —
<host>or skip? - Deploy now — yes (via
/ro:cf-ship) or scaffold-only?
Answers are converted to flags and the non-interactive flow proceeds. Use this as the default when a user invokes without flags AND without --skip-interactive.
Process
1. Baseline scaffold (always)
pnpm create tsrouter-app@latest <app-name> --template start
cd <app-name>
pnpm install
git init && git add -A && git commit -m "🧹 chore: scaffold tanstack start"
1a. Supply-chain hardening (always) → /ro:harden-npm
Run immediately after the first pnpm install, before any other step adds packages. This locks in pnpm 11 defaults (minimumReleaseAge=1440, strictDepBuilds=true, blockExoticSubdeps=true), pins packageManager in package.json, writes a per-repo .npmrc with the same settings as defence-in-depth, and runs pnpm approve-builds to set the pnpm.onlyBuiltDependencies allowlist.
/ro:harden-npm
If pnpm < 11 on the host machine, the skill auto-upgrades via corepack before applying. Idempotent — safe to re-run after later pnpm add calls.
Background: triggered by Mini Shai-Hulud v2 (CVE-2026-45321) supply-chain attack on TanStack. Full context in llm-wiki-security/wiki/incidents/mini-shai-hulud-v2-tanstack.md.
2. Wire Cloudflare adapter (always)
pnpm add -D @cloudflare/workers-types wrangler
Set app.config.ts → preset: 'cloudflare-module'. Create wrangler.toml with app name + compatibility date.
3. Database — dispatch
Default DB is Neon Postgres. Use --db sqlite to opt into D1 instead. The canonical pick for SaaS-shape apps is Neon: standard Postgres at any scale, no migration-count API-rate-limit risk, and the HTTP driver works in Cloudflare Workers without TCP sockets. See the db-pick-decision-rule in llm-wiki for when to deviate.
- Neon (default):
/ro:neon installwires Drizzle +@neondatabase/serverlesswithdrizzle-orm/neon-http. Then/ro:neon project create <app-name>and/ro:neon push-secretto writeNEON_DATABASE_URLas a wrangler secret. Template files atskills/new-tanstack-app/templates/src/db/neon.ts,drizzle/neon/, anddrizzle/neon/drizzle.config.tsare copied into the new app. Post-scaffold: create a Neon project at console.neon.tech, copy the connection URI, runwrangler secret put NEON_DATABASE_URL --env production. --db sqlite: inline D1 wiring. Add[[d1_databases]]binding inwrangler.toml, thenwrangler d1 create <app-name>_db, patchdatabase_id. Installdrizzle-orm+drizzle-kitwithdialect: 'sqlite',driver: 'd1-http'. Use this for edge-cache, CLI tools, or apps that truly need SQLite semantics.
Either way, create src/db/schema.ts (Neon: pgTable, D1: sqliteTable) with a minimal example table.
4. UI stack (always)
pnpm add -D tailwindcss @tailwindcss/vite
pnpm add lucide-react
pnpm dlx shadcn@latest init
pnpm dlx shadcn@latest add button dialog input form
Add @tailwindcss/vite plugin. Add @import "tailwindcss"; to the root CSS.
5. Testing + API docs stack (always) → /ro:testing-stack install
Delegate to /ro:testing-stack install. That sub-skill scaffolds the full seven-layer pattern:
- Vitest unit tests
- Vitest integration tests against real upstreams
- Playwright e2e (Chromium, with visual-regression opt-in)
- Bruno API collection with
local/production/mockenvironments - Zod +
@asteasolutions/zod-to-openapiserved at/api/openapi, Scalar docs at/api/docs - Prism mock server on :4010 via
pnpm mock - k6 ad-hoc load tests (
scripts/loadtest.js+pnpm loadtest:smoke|local|prod|burst); requires one-timebrew install k6. Not in CI by default.
Plus package.json scripts, three CI jobs (e2e, integration, api-contract) gating deploy, and documented anti-patterns (no blanket coverage, no global .strict(), no Redoc, no x-faker).
Reference: connections-helper/docs/adr/0001-testing-and-docs-stack.md.
6. Code hygiene (always) — pre-commit + pre-push hooks are MANDATORY
Every app ships with the standard two-hook set from day one. This is not optional and not deferred. GitHub Actions billing is capped (the user's CI policy skips remote CI), so the local pre-push hook IS the real CI gate — a green push means the code is good. The two hooks:
- pre-commit → husky + lint-staged running Prettier (and
eslint --fixwhere cheap) on STAGED files only. Stops the "CI fails on formatting" class of PR. - pre-push → the full local CI gate (
pnpm quality= typecheck + lint + format-check + build + test). Nothing reaches the remote that would fail CI locally.
pnpm add -D prettier eslint typescript \
@typescript-eslint/parser @typescript-eslint/eslint-plugin \
eslint-config-prettier prettier-plugin-tailwindcss \
husky lint-staged \
@commitlint/cli
pnpm exec husky init
.prettierrc.json, flateslint.config.jswithstrictTypeChecked+prettierlast.tsconfig.jsonstrict (strict,noUncheckedIndexedAccess,exactOptionalPropertyTypes).package.jsonscripts:"format": "prettier --check ."(CI gate)."format:write": "prettier --write ."(one-shot baseline, run once after scaffolding to set the repo-wide baseline)."typecheck": "tsc --noEmit","lint": "eslint .","test": "vitest run"."quality": "pnpm run format && pnpm run lint && pnpm run typecheck && pnpm run build && pnpm run test"(the single local-CI gate, shared by the pre-push hook AND GitHub Actions — see § 13).typecheckis part ofquality— do not drop it."prepare": "husky"(bootstraps hooks on everypnpm install).
lint-stagedblock inpackage.jsonthat runsprettier --writeon the usual source globs (*.{ts,tsx,js,jsx,json,css,md}). Addeslint --fixahead ofprettier --writeon*.{ts,tsx}where the lint pass is cheap..husky/pre-commit→pnpm exec lint-staged(auto-format staged files before every commit: eliminates the class of "CI fails on formatting" PRs)..husky/commit-msg→pnpm exec commitlint --edit "$1"..husky/pre-push→pnpm quality(typecheck + lint + format-check + build + test — the full local CI gate). Bypassable withgit push --no-verifyfor real emergencies only. This hook is load-bearing: because remote CI is billing-capped and skipped, the pre-push gate is the real gate. It also lets/ro:planner-workerand/ro:ralphdefault to--trust-local-cifor this repo — a successful push means CI has effectively passed locally, so workers squash-merge immediately instead of waiting on GitHub Actions to re-run the same gauntlet. See/ro:planner-worker§ "Lessons from live runs" lesson #5 and the user's CI & Shipping Policy.commitlint.config.mjsenforcing the emoji + conventional format fromCLAUDE.md(✨ feat / 🐛 fix / 🧪 test / 📝 docs / 🧹 chore / ♻️ refactor / 🚀 deploy / 🔧 config / ⚡ perf / 🔒 security). Use a custom parser-preset + two rules (emoji-allowed,emoji-type-matches). Do not use@commitlint/config-conventional: it doesn't know about the emoji requirement, so it'd half-enforce the convention. Copy the config verbatim fromconnections-helper/commitlint.config.mjs.
After scaffolding, run pnpm format:write once to set the baseline so subsequent pre-commit hooks have nothing to change on untouched files.
Reference: connections-helper/docs/adr/0002-github-branch-protection-squash-only-merges.md.
7. --auth → /ro:better-auth install (default), /ro:clerk install (hosted-UI consideration), or /ro:workos install (alt-at-scale)
Security canon (applies to every auth path). Auth is the main attack surface once data is encrypted, so make it secure by default, per the authentication-hardening playbook (llm-wiki-security/wiki/playbooks/authentication-hardening.md):
- Always offer a phishing-resistant factor (passkey / FIDO2 / WebAuthn). It's the NIST 800-63B AAL2+ and CISA gold standard; SMS/TOTP are phishable. Whichever provider is chosen below, enable passkeys and don't ship SMS-only MFA.
- Single-user / internal / personal apps: prefer gating at the edge with Cloudflare Access + WARP device posture instead of a public login form, the app stays unreachable to the internet and the attack surface collapses to "CF edge + IdP". (This is the Tailscale-equivalent on Workers; Tailscale itself only gates self-hosted boxes.) For these, auth may need no in-app provider at all, just Access + a JWT-verify in the Worker. Surface this as the recommended option when the app is described as "just me" / internal / personal.
- Short sessions + step-up re-auth before sensitive actions; auth/signing secrets in a secret store.
Default (multi-user / public SaaS): delegate to /ro:better-auth install. Better Auth (since the Settle build, 2026-06-07) is the canonical pick for small SaaS / personal apps: identity tables live in your own Postgres so an agent or API can join user to domain data (agent-readable), self-issued access tokens (PAT) for app-to-app + agent access, passwordless email OTP via Resend as the default sign-in method (a 6-digit code; autocomplete="one-time-code" on the input gives iOS one-tap autofill; no Google Cloud Console / OAuth-app setup, so it's the fastest login to ship). Resend is a day-one dependency (send from a verified domain, NOT the onboarding@resend.dev test sender, which delivers to the account owner only). Google OAuth + passkeys are opt-in add-ons (/ro:better-auth add-provider). $0 self-hosted, no vendor lock-in, identity in your own Postgres (agent-readable).
Flip to /ro:workos install when any of these is true:
- MAU is expected to cross 100K within 12 months (Clerk's per-MAU cost ramps; WorkOS is free to 1M MAU).
- A non-engineer partner needs the WorkOS Admin Portal for user-management visibility.
- Enterprise SSO via per-connection SAML is on the near-term roadmap.
Trigger via --auth=workos flag, or via the interactive picker (Question 2) above.
Flip to /ro:clerk install (the hosted-UI consideration) when any of these is true:
- You want drop-in hosted UI components out of the box (
<SignIn />,<UserButton />,<OrganizationSwitcher />) for the fastest possible first sign-in. - A non-technical partner wants a managed dashboard to see and manage users without you building an admin page.
- You do NOT need DB-local identity or agent/API access to the user table (Clerk keeps identity off-DB behind its API).
Trigger via --auth=clerk flag, or via the interactive picker (Question 2) above.
Afterwards (Clerk path):
- Remind user:
CLERK_SECRET_KEY,CLERK_PUBLISHABLE_KEY,CLERK_WEBHOOK_SECRETlive in.dev.vars+wrangler secret put, NOT in~/.claude/.env. The publishable key ships to the browser bundle (safe).
Afterwards (WorkOS path):
- Remind user:
WORKOS_API_KEY,WORKOS_CLIENT_ID,WORKOS_COOKIE_PASSWORD,WORKOS_REDIRECT_URIlive in.dev.vars+wrangler secret put, NOT in~/.claude/.env.
Afterwards (Better Auth path):
- Remind user:
BETTER_AUTH_SECRETgenerated viaopenssl rand -base64 32lives in.dev.vars+wrangler secret put, NOT in~/.claude/.env.
6a. Design system + /styleguide route (always, unless --skip-styleguide)
Delegate to /ro:design-system-create --showcase. Runs after auth so the role helper (src/lib/auth/roles.ts) emitted by the chosen auth skill (/ro:better-auth by default, /ro:clerk if picked) is on disk by the time the styleguide route gets wired.
What you get:
src/design-system/tokens.ts— typed TYPOGRAPHY, SPACING, RADIUS, ELEVATION, ZDESIGN_SYSTEM.mdat repo root — rules + state tables + review checklist- cva-based variants on the shadcn primitives (Button, Input, Card)
src/routes/styleguide.tsx— the role-gated showcase page:- With
--auth: gated viarequireRole('superadmin', 'staff'). Returns 404 to anyone else. Superadmin email is hardcoded (admin@simplicitylabs.ioby default — editSUPERADMIN_EMAILSinsrc/lib/auth/roles.tsper app). Staff = Clerk org members with the customorg:staffrole (one-time dashboard setup, see/ro:clerk§ add-roles). - Without
--auth: dev-only fallback. The route renders inpnpm devand 404s in production builds.
- With
Skip with --skip-styleguide if (and only if) the user explicitly doesn't want it. Default is on because the styleguide is the cheapest design-system audit surface and the natural landing pad for any future admin panel.
Mention to the user post-scaffold: once they create the first Clerk org and want a teammate (e.g. Taskforce employee) to view /styleguide on production, promote them to org:staff in the Clerk dashboard. No deploy needed.
7a. --ai-sdk → Vercel AI SDK (provider abstraction)
pnpm add ai @ai-sdk/anthropic @ai-sdk/openai @ai-sdk/google zod
Scaffold src/lib/models.ts:
import { anthropic } from "@ai-sdk/anthropic";
import { openai } from "@ai-sdk/openai";
import { google } from "@ai-sdk/google";
export const models = {
primary: anthropic("claude-opus-4-7"),
fast: anthropic("claude-haiku-4-5-20251001"),
alternate: openai("gpt-5"),
cheap: google("gemini-2.5-flash"),
} as const;
Scaffold src/routes/api/chat.ts as a reference streamText endpoint using toDataStreamResponse(). Prompt-caching breakpoints via providerOptions: { anthropic: { cacheControl: { type: "ephemeral" } } }. Document in ARCHITECTURE.md that provider swap = change one import in lib/models.ts.
For downstream feature work, point the user (and future agents) at /ro:vercel-ai-sdk. It covers every Core primitive (streamText, generateText, generateObject, streamObject, embed, tool() agentic loops, wrapLanguageModel middleware), every UI hook (useChat, useCompletion, useObject), the v6 UIMessage parts[] wire protocol, provider-specific tricks (Anthropic prompt caching + extended thinking, OpenAI reasoning effort + structured outputs, Google grounding), edge-runtime gotchas, and v5→v6 migration. Append this line to the scaffolded AGENTS.md and CLAUDE.md:
## AI SDK
This project uses the Vercel AI SDK (v6). For patterns, primitives, recipes, and provider-specific tricks, load `/ro:vercel-ai-sdk` before adding or modifying any AI feature.
That cross-reference is what auto-loads the AI-SDK skill the next time someone asks for a chat / streaming / structured-output / tool-calling feature in this project.
Push provider keys as Worker secrets: wrangler secret put ANTHROPIC_API_KEY (and/or OPENAI/GOOGLE).
7b. --agent xstate → XState decision machine
pnpm add xstate @xstate/react
Scaffold src/machines/exampleMachine.ts — a prescriptive state machine with one fromPromise actor calling generateObject (if --ai-sdk also set) for typed classification. Scaffold src/components/Machine.tsx using useMachine. Wire a reference route src/routes/machine.tsx.
If --auth is also set, the example machine reads client_id from auth.api.getActiveMember() and passes it in machine context for future RLS-scoped tool calls.
7c. --agent langgraph → Phase-2 POA (no auto-scaffold)
Don't install LangGraph today on Cloudflare Workers — the stock @langchain/langgraph-checkpoint-postgres uses pg TCP and will not run. Instead print a migration POA to stdout covering:
- Use XState at the top level; invoke a LangGraph workflow from an XState actor when a sub-tree needs free-form agentic planning.
- Checkpointer options on Workers: D1 adapter, Neon-HTTP custom checkpointer, or Durable Object per-session storage (preferred).
- Add LangSmith (
LANGSMITH_API_KEY) when the first LangGraph workflow ships; before that, Sentry + PostHog telemetry is sufficient.
7d. --knock → Knock notifications (multi-channel)
pnpm add @knocklabs/node @knocklabs/react
Scaffold src/routes/api/notify.ts:
import { Knock } from "@knocklabs/node";
export const APIRoute = createAPIFileRoute("/api/notify")({
POST: async ({ request }) => {
const knock = new Knock(env.KNOCK_API_KEY);
const { workflow, recipients, data } = await request.json();
await knock.workflows.trigger(workflow, { recipients, data });
return new Response(null, { status: 204 });
},
});
Push wrangler secret put KNOCK_API_KEY. Document expected workflow IDs in ARCHITECTURE.md so product/design can create them in Knock's UI.
Note: Resend for transactional email is still installed via the baseline scaffold when --knock is set alongside — Knock can delegate the email channel to Resend.
8. --posthog → /ro:posthog install --both
Delegate. Client + server SDK. For public-facing apps, prefer runtime config injection over VITE_* (see "Runtime-injected observability" below) — the key still ships to browsers either way, but runtime injection means forks don't ship your key and rotations don't need a rebuild.
9. --sentry → /ro:sentry install --tanstack + project create
Delegate install. Then /ro:sentry project create <app-slug> --platform javascript-react creates a Sentry project and returns the DSN. For public-facing apps, prefer runtime config injection (see below) over VITE_SENTRY_DSN.
Runtime-injected observability (recommended default)
Instead of baking Sentry DSN + PostHog key into the bundle via VITE_* vars, store them as Cloudflare Worker vars and expose them via an /api/config endpoint the client fetches on load. Scaffold:
wrangler.jsonc→vars: { SENTRY_DSN: "", POSTHOG_PROJECT_KEY: "", POSTHOG_INGEST_HOST: "https://eu.i.posthog.com" }src/routes/api/config.ts→ GET returns{ sentryDsn, posthogKey, posthogHost }fromenvsrc/lib/runtime-config.ts→ memoised client-sidefetch('/api/config')initSentry()/initPostHog()are async, read from runtime-config, no-op if keys are empty
Benefits: keys rotate without rebuilds, CI builds without observability secrets, forks don't ship your keys. Cost: one extra fetch before analytics init (fine for non-critical-path analytics). Document the flow in ARCHITECTURE.md.
10. --uptime → /ro:uptimerobot monitor create (post-deploy)
Deferred to post-deploy — needs the Worker URL first. After /ro:cf-ship prints the URL:
/ro:uptimerobot monitor create <worker-url> --name "<app-name>"
11. --domain <host> → /ro:cloudflare-dns (post-deploy)
Deferred to post-deploy. After the Worker is live:
- Add custom domain binding via
wrangler.toml→routesorwrangler custom-domains add /ro:cloudflare-dns add <host>adds a CNAME to the Worker (proxied/orange-cloud)
12. Deploy — /ro:cf-ship (always, unless --skip-deploy)
Run /ro:cf-ship for the full pre-flight gate: typecheck, lint, format, test, D1 migrations, secrets diff, build, deploy, smoke check. This replaces the inline wrangler deploy from the old version of this skill — the pre-flight gate is a big value-add and shouldn't be duplicated.
13. GitHub CI + auto-deploy (always, unless --skip-ci)
Every app ships with CI from day one. Two jobs: a test job that runs on every push and PR (typecheck + lint + format-check + build + test, collapsed into a single pnpm quality script — the SAME command the pre-push hook runs in § 6, so local and remote share one gate), and a deploy job that runs only on push to main, gated on test, deploying to Cloudflare with secrets from the production environment. Note: per the user's CI policy remote CI is billing-capped and usually skipped, so the § 6 pre-push hook is the gate that actually runs; this workflow stays committed so it's there when remote CI is re-enabled.
HARD RULES for the deploy workflow:
- Neon migrations (default):
pnpm db:migrate(runsdrizzle-kit migrate --config drizzle/neon/drizzle.config.ts) before the wrangler deploy step. ReadsNEON_DATABASE_URLfrom theproductionenvironment secret. Drizzle applies only unapplied migrations tracked indrizzle/neon/meta/_journal.json. - D1 migrations (
--db sqliteonly):wrangler d1 migrations apply <db-name> --remote. NEVER afor f in drizzle/*.sql; do wrangler d1 execute --file=$floop. Drizzle'smeta/_journal.json+ the remoted1_migrationstracking table are how D1 knows what's already applied. The brute-force loop runs every file every deploy and trips CF Workers API rate-limit 10429 on repos with active merge cadence. Real incident: lekkertaal 2026-05-19 (PRRonanCodes/lekkertaal#169was the cleanup). See [[canon:d1-migrations]]. paths-ignoreonpush: docs-only / retro / chore-artefact pushes should NOT trigger deploys. Filter at least**/*.md,docs/**,.nightshift/**,.ralph/**,.completion-reports/**. PRs still run the full workflow regardless of paths so reviewers see CI status.- Use
cloudflare/wrangler-action@v3rather than rawpnpm wranglershell calls. The action handles auth + retry + output formatting better and is the canonical pattern (matches dataforce, lekkertaal post-#169, factory).
Create .github/workflows/ci.yml:
name: CI
on:
push:
branches: [main]
# docs / retro / chore-artefact pushes must NOT trigger deploys, see HARD RULE 2 above
paths-ignore:
- "**/*.md"
- "docs/**"
- ".nightshift/**"
- ".ralph/**"
- ".completion-reports/**"
pull_request:
concurrency:
group: ci-${{ github.ref }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
jobs:
test:
name: Quality checks (typecheck + lint + format + build + test)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
with: { version: 9 }
- uses: actions/setup-node@v4
with: { node-version: 22, cache: pnpm }
- run: pnpm install --frozen-lockfile
- run: pnpm quality
deploy:
name: Deploy to Cloudflare Workers
needs: test
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
runs-on: ubuntu-latest
environment: production
concurrency:
group: deploy-production
cancel-in-progress: false
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
with: { version: 9 }
- uses: actions/setup-node@v4
with: { node-version: 22, cache: pnpm }
- run: pnpm install --frozen-lockfile
- run: pnpm build
# Apply only new Neon migrations. drizzle-kit reads drizzle/neon/meta/_journal.json
# and applies only migration files that haven't run yet.
# NEON_DATABASE_URL must be set in the production environment secret.
- name: Apply Neon migrations
env:
NEON_DATABASE_URL: ${{ secrets.NEON_DATABASE_URL }}
run: pnpm db:migrate
- name: Deploy worker
uses: cloudflare/wrangler-action@v3
with:
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
command: deploy
If --posthog / --sentry are set, add --var flags to the wrangler deploy step (reading from secrets.SENTRY_DSN and secrets.POSTHOG_PROJECT_KEY), matching the runtime-config pattern from step 8-9.
Add a collapsing quality script to package.json so local (pre-push hook) + CI share one command:
"scripts": {
"quality": "pnpm run format && pnpm run lint && pnpm run typecheck && pnpm run build && pnpm run test"
}
format is prettier --check . (not --write), so a formatting drift fails the gate rather than silently mutating files. typecheck is tsc --noEmit. This is the exact command the .husky/pre-push hook runs (§ 6).
Push secrets to the production environment.
Gotcha — GITHUB_TOKEN in ~/.claude/.env shadows gh's keychain auth. If the script sources ~/.claude/.env to read CLOUDFLARE_API_TOKEN (etc.), GITHUB_TOKEN from that file takes priority over the keychain-stored gh token, and gh secret set fails with HTTP 401: Bad credentials on the public-key endpoint — even though gh api works on the same URL. Fix: unset GITHUB_TOKEN GH_TOKEN right after sourcing, before any gh call.
Needs a gh token with repo scope and admin on the environment — if it 401s despite the unset, run gh auth refresh -h github.com -s admin:repo_hook and pass --repo <owner>/<name> explicitly:
set -a && source "$(ro context env)" && set +a
unset GITHUB_TOKEN GH_TOKEN # required — see gotcha above
REPO=<owner>/<repo>
gh secret set CLOUDFLARE_API_TOKEN --env production --repo $REPO --body "$CLOUDFLARE_API_TOKEN"
gh secret set CLOUDFLARE_ACCOUNT_ID --env production --repo $REPO --body "$CLOUDFLARE_ACCOUNT_ID"
gh secret set NEON_DATABASE_URL --env production --repo $REPO --body "$NEON_DATABASE_URL"
# observability (if wired):
gh secret set SENTRY_DSN --env production --repo $REPO --body "$SENTRY_DSN"
gh secret set POSTHOG_PROJECT_KEY --env production --repo $REPO --body "$POSTHOG_PROJECT_KEY"
Why environment: production and not repo-level secrets: preview branches / PRs never see the deploy token. Required status checks can gate deploys per-environment. Audit log shows which env a secret was used in.
Skip with --skip-ci if (and only if) the user explicitly doesn't want CI. Default is on.
13a. GitHub branch protection + squash-only merges (always, unless --skip-ci)
After CI is wired and the first push lands so GitHub knows the check contexts exist, lock main down. Two API calls, both idempotent.
1. Branch protection on main:
REPO=<owner>/<repo>
gh api -X PUT "repos/$REPO/branches/main/protection" --input - <<'JSON'
{
"required_status_checks": {
"strict": true,
"contexts": [
"Quality checks (typecheck + lint + format + build + test)"
]
},
"enforce_admins": true,
"required_pull_request_reviews": null,
"restrictions": null,
"required_linear_history": true,
"allow_force_pushes": false,
"allow_deletions": false,
"required_conversation_resolution": false,
"lock_branch": false,
"allow_fork_syncing": false
}
JSON
Extend the contexts list with every CI job name that should gate merging. The job name is what appears (look at gh pr checks <PR> output), not the workflow name. If /ro:testing-stack is wired, add: Playwright e2e, Integration tests (real external APIs), API contract (Bruno), Playwright visual diff, Accessibility + performance budget, Secret scan (gitleaks + trufflehog), Dependency audit (pnpm). Do not include the deploy job: it only runs on push to main, never on PRs, and would permanently block merges.
2. Repo-level squash-only merge settings:
gh api -X PATCH "repos/$REPO" \
-f allow_squash_merge=true \
-F allow_merge_commit=false \
-F allow_rebase_merge=false \
-F delete_branch_on_merge=true \
-f squash_merge_commit_title=PR_TITLE \
-f squash_merge_commit_message=PR_BODY
Why these settings, including the research backing squash-only: connections-helper/docs/adr/0002-github-branch-protection-squash-only-merges.md. tl;dr: 14 of 18 surveyed TS/JS flagship projects (TypeScript, Next.js, React, Vite, Astro, Svelte, tRPC, Prettier, ESLint, Playwright, TanStack Query, Tailwind, Vue core, Hono) use squash-only on main. The outliers are the less disciplined ones.
Stacked-PR workflow: once protection is on, stacked PRs need rebase-after-parent-merges. Codified in /ro:stacked-prs.
14. Final commit — /ro:commit
Delegate to /ro:commit so the emoji format and weekday-timestamp rule are enforced.
Output summary
Print the following after everything runs:
- App name + directory
- DB: Neon project ID + branch (connection URI set as wrangler secret), OR D1 database ID (if
--db sqlite) - Auth: enabled / disabled (Better Auth / Clerk / WorkOS)
- Design system: tokens + DESIGN_SYSTEM.md emitted,
/styleguideroute at gate=requireRole(superadmin,staff)/dev-only - Agent runtime: XState (scaffolded reference machine) / LangGraph POA printed / none
- LLM provider abstraction: Vercel AI SDK installed + configured providers
- Notifications: Knock workspace wired / Resend-only / none
- Observability wired: PostHog flag, Sentry project slug + DSN source, UptimeRobot monitor ID
- Deployed URL + custom domain (if
--domain) - Next-step suggestions: add more shadcn components, write first Server Function,
pnpm dev
Safety
- Every sub-skill has its own safety rules — this orchestrator does not override them.
- If a sub-skill's required env var is missing, skill fails fast at the top with "Missing: X. Add to
~/.claude/.env" — does NOT attempt partial setup. --skip-deployimplies--uptimeand--domainare also skipped (they're post-deploy).- If
wrangler whoamishows an insufficient-scope token, skill fails fast before anywrangler deploycall. - If
NEON_DATABASE_URLis absent at runtime,getNeonClientthrows a clear error with setup instructions rather than passing an empty string to the Neon driver.
Anti-patterns it guards against
- Inlining sub-skill logic here (drifts from the sub-skill's source of truth)
- Silently continuing when a sub-skill fails (bad state + partial deploy)
- Assuming a token has full Workers scope without verifying
- Using TCP drivers for Postgres inside Workers (Neon HTTP driver only — enforced by
/ro:neon) - Defaulting new SaaS apps to D1: D1 is SQLite-on-edge, not Postgres. Migration-count API rate limits (CF 10429) hit active repos at ~100 migrations. Use D1 only via explicit
--db sqlitefor edge-cache / CLI shapes where SQLite semantics are the right fit.
See also
/ro:migrate-to-tanstack— port an existing app to this stack (the migration sibling)/ro:neon— Postgres wiring/ro:better-auth(default auth),/ro:clerk(hosted-UI consideration),/ro:workos(alt-at-scale auth),/ro:posthog,/ro:sentry,/ro:uptimerobot,/ro:cloudflare-dns/ro:setup-logging— wire the diagnosable-by-default observability baseline (logtape structured logging that EMITS, request context with trace_id/userId/orgId, trace_id FE→BE, CFobservability.enabled). Run it during scaffold so the app is debuggable from day one; pairs with/ro:diagnose.canon/auth-guards.md(MANDATORY when the app has auth) — every login-gated page MUST have a server-sidebeforeLoadguard that redirects signed-out users to sign-in; a signed-out visitor must never render a gated page. Classify every route gated/public + run the audit grep. (Lesson: dataforce shipped a guard pass that missed onboarding + install routes → broken/500 pages for signed-out users.)/ro:design-system-create— emits/styleguideshowcase route + DESIGN_SYSTEM.md spec + cva variants (invoked by step 6a)/ro:cf-ship— the deploy pipeline/ro:commit— emoji conventional commits