skills/new-tanstack-app/SKILL.md
--- 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 100
npx skillsauth add RonanCodes/ronan-skills skills/new-tanstack-appInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
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.
Performance baseline: wire [/ro:performance-first] from the start — navigation must never block on data (TanStack Router deferred loaders + pendingComponent, TanStack Query cache + prefetch-on-intent, layout-matched skeletons). It's a standard for every app on this stack, not a later optimisation.
/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
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)
pnpm (install: corepack enable pnpm)wrangler 4.x — pnpm add -g wrangler (skill checks and offers to install)CLOUDFLARE_API_TOKEN in ~/.claude/.env with Workers Scripts + Account Settings + Zone DNS scopesNEON_API_KEY in ~/.claude/.env (required for the default Neon DB path; create at console.neon.tech)--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 of ANTHROPIC_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_TOKEN with Zone:DNS:Edit--interactive)Runs an AskUserQuestion preamble to collect:
--db sqlite for edge-cache / CLI shapes)?<host> or skip?/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.
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"
/ro:harden-npmRun 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.
pnpm add -D @cloudflare/workers-types wrangler
Set app.config.ts → preset: 'cloudflare-module'. Create wrangler.toml with app name + compatibility date.
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.
/ro:neon install wires Drizzle + @neondatabase/serverless with drizzle-orm/neon-http. Then /ro:neon project create <app-name> and /ro:neon push-secret to write NEON_DATABASE_URL as a wrangler secret. Template files at skills/new-tanstack-app/templates/src/db/neon.ts, drizzle/neon/, and drizzle/neon/drizzle.config.ts are copied into the new app. Post-scaffold: create a Neon project at console.neon.tech, copy the connection URI, run wrangler secret put NEON_DATABASE_URL --env production.--db sqlite: inline D1 wiring. Add [[d1_databases]] binding in wrangler.toml, then wrangler d1 create <app-name>_db, patch database_id. Install drizzle-orm + drizzle-kit with dialect: '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.
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.
/ro:testing-stack installDelegate to /ro:testing-stack install. That sub-skill scaffolds the full seven-layer pattern:
local / production / mock environments@asteasolutions/zod-to-openapi served at /api/openapi, Scalar docs at /api/docspnpm mockscripts/loadtest.js + pnpm loadtest:smoke|local|prod|burst); requires one-time brew 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.
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:
eslint --fix where cheap) on STAGED files only. Stops the "CI fails on formatting" class of PR.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, flat eslint.config.js with strictTypeChecked + prettier last.tsconfig.json strict (strict, noUncheckedIndexedAccess, exactOptionalPropertyTypes).package.json scripts:
"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). typecheck is part of quality — do not drop it."prepare": "husky" (bootstraps hooks on every pnpm install).lint-staged block in package.json that runs prettier --write on the usual source globs (*.{ts,tsx,js,jsx,json,css,md}). Add eslint --fix ahead of prettier --write on *.{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 with git push --no-verify for 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-worker and /ro:ralph default to --trust-local-ci for 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.mjs enforcing the emoji + conventional format from CLAUDE.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 from connections-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.
--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):
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 [email protected] test sender, which delivers to the account owner only). For a personal app use the personal Resend account RESEND_API_KEY_RONAN and a ronanconnolly.dev sender, never the bare RESEND_API_KEY / a simplicitylabs.io sender — see /ro:env § "Multiple accounts for one service". 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:
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:
<SignIn />, <UserButton />, <OrganizationSwitcher />) for the fastest possible first sign-in.Trigger via --auth=clerk flag, or via the interactive picker (Question 2) above.
Afterwards (Clerk path):
CLERK_SECRET_KEY, CLERK_PUBLISHABLE_KEY, CLERK_WEBHOOK_SECRET live in .dev.vars + wrangler secret put, NOT in ~/.claude/.env. The publishable key ships to the browser bundle (safe).Afterwards (WorkOS path):
WORKOS_API_KEY, WORKOS_CLIENT_ID, WORKOS_COOKIE_PASSWORD, WORKOS_REDIRECT_URI live in .dev.vars + wrangler secret put, NOT in ~/.claude/.env.Afterwards (Better Auth path):
BETTER_AUTH_SECRET generated via openssl rand -base64 32 lives in .dev.vars + wrangler secret put, NOT in ~/.claude/.env./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.
shadcn is the default component layer for this scaffold (step 3 above installs it). A custom Tailwind-token component layer is acceptable only when the app is already token-driven AND its tests pass AND shadcn primitives cannot pixel-match without churn; a rival component framework (Chakra, MUI) is never acceptable. The canonical rule lives in /ro:design-system-create § "shadcn vs custom (the canonical rule)" — defer there rather than re-deciding per app.
What you get:
src/design-system/tokens.ts — typed TYPOGRAPHY, SPACING, RADIUS, ELEVATION, ZDESIGN_SYSTEM.md at repo root — rules + state tables + review checklistsrc/routes/styleguide.tsx — the role-gated showcase page:
--auth: gated via requireRole('superadmin', 'staff'). Returns 404 to anyone else. Superadmin email is hardcoded ([email protected] by default — edit SUPERADMIN_EMAILS in src/lib/auth/roles.ts per app). Staff = Clerk org members with the custom org:staff role (one-time dashboard setup, see /ro:clerk § add-roles).--auth: dev-only fallback. The route renders in pnpm dev and 404s in production builds.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.
--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).
--agent xstate → XState decision machinepnpm 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.
--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:
LANGSMITH_API_KEY) when the first LangGraph workflow ships; before that, Sentry + PostHog telemetry is sufficient.--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.
--posthog → /ro:posthog install --bothDelegate. 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.
--sentry → /ro:sentry install --tanstack + project createDelegate 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.
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 } from envsrc/lib/runtime-config.ts → memoised client-side fetch('/api/config')initSentry() / initPostHog() are async, read from runtime-config, no-op if keys are emptyBenefits: 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.
--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>"
--domain <host> → /ro:cloudflare-dns (post-deploy)Deferred to post-deploy. After the Worker is live:
wrangler.toml → routes or wrangler custom-domains add/ro:cloudflare-dns add <host> adds a CNAME to the Worker (proxied/orange-cloud)/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.
--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:
pnpm db:migrate (runs drizzle-kit migrate --config drizzle/neon/drizzle.config.ts) before the wrangler deploy step. Reads NEON_DATABASE_URL from the production environment secret. Drizzle applies only unapplied migrations tracked in drizzle/neon/meta/_journal.json.--db sqlite only): wrangler d1 migrations apply <db-name> --remote. NEVER a for f in drizzle/*.sql; do wrangler d1 execute --file=$f loop. Drizzle's meta/_journal.json + the remote d1_migrations tracking 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 (PR RonanCodes/lekkertaal#169 was the cleanup). See [[canon:d1-migrations]].paths-ignore on push: 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.cloudflare/wrangler-action@v3 rather than raw pnpm wrangler shell 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.
--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.
/ro:commitDelegate to /ro:commit so the emoji format and weekday-timestamp rule are enforced.
Print the following after everything runs:
--db sqlite)/styleguide route at gate=requireRole(superadmin,staff) / dev-only--domain)pnpm dev~/.claude/.env" — does NOT attempt partial setup.--skip-deploy implies --uptime and --domain are also skipped (they're post-deploy).wrangler whoami shows an insufficient-scope token, skill fails fast before any wrangler deploy call.NEON_DATABASE_URL is absent at runtime, getNeonClient throws a clear error with setup instructions rather than passing an empty string to the Neon driver./ro:neon)--db sqlite for edge-cache / CLI shapes where SQLite semantics are the right fit./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, CF observability.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-side beforeLoad guard 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 /styleguide showcase route + DESIGN_SYSTEM.md spec + cva variants (invoked by step 6a)/ro:cf-ship — the deploy pipeline/ro:commit — emoji conventional commitstesting
--- name: linear-pipeline description: The Fable orchestrator for a single dispatched Linear ticket. Holds almost no context itself; it receives `--issue <ID> --detached`, decides the stage sequence, and fans out a sub-agent per stage, passing forward only each stage's artifact (never re-derived, never inlined into its own context). Step zero, before any planning or stage routing, is a boundary triage against `canon/security-boundary.md` (#199): a match tags Ronan Connolly and stops the run, no
development
--- name: in-your-face description: Capture a chat-only answer into a durable artifact (markdown + HTML, PDF when cheap) and launch it automatically so the user cannot miss it. Use when user says "in your face", "don't let me lose this", "save that answer", "make that durable", or right after answering a substantive side question (a recipe, comparison, how-to, or generated prompt) that would otherwise die with the context. category: workflow argument-hint: [--no-open] [--vault <short>] [hint of
tools
One-shot headless OpenAI Codex CLI calls for background/admin AI tasks — summaries, classification, extraction, admin glue. The default engine for anything that runs AI constantly in the background (daemon-driven, per-event), because it bills the flat ChatGPT subscription instead of Claude usage or per-token API spend, and it keeps working while Claude is rate-limited. NEVER for coding — coding stays Claude. Use when a skill or daemon needs a cheap always-on AI call, when the user says "use codex", "ask codex", "codex as backup", or when building a background summarizer/classifier into a listener or loop. Reads auth from ~/.codex/auth.json (ChatGPT account, no API key).
research
Turn a warranty rejection, repair quote, or RMA email into a cited decision brief — legal read (NL/EU consumer law), is the part user-serviceable, live part and new-unit prices, repair-vs-DIY-vs-new economics, before-you-send-it checklist, deadlines. Use when the user pastes or screenshots a repair quote, warranty rejection, "not covered" email, onderzoekskosten fee, or asks "should I repair or replace this".