backend-design
Skill0 starsChanged 5 months agoMITView on GitHub
Design and scaffold a backend for an existing frontend. Activate on "/backend-design", "build a backend for this frontend", "generate the API and database for this app", "design a backend from the UI", or similar intent. The user sees two checkpoints — a reviewable design doc (endpoints, DB schema, auth), then scaffolded code after they approve.
- Reads credentials
- Installs packages
2 other projects use a version of this.
Adopted by
Most recently taken up by
- jgamaraalv/delivery-looptheir own version, first seen Jul 4, 2026
- itseffi/agentic-ostheir own version, first seen Jan 23, 2026
Other versions2 projects use a version of this.
Word for word · 0
None found.
Edited · 0
None found.
Wrote their own version · 2
- itseffi/agentic-os★ 114same name
- jgamaraalv/delivery-loop★ 0same name
How matches are found. Word for word: the exact same text. Edited: shares passages with this file (“99% same” means 99% of its five-word phrases also appear here; 10% or more counts). Their own version: similar meaning, or, weakest, the same name. We can’t tell which project had it first. Projects are counted among the 200 most-starred candidates, and mentions are links to the repository we found, up to 50 per site, so both counts are a floor.
The file
Show its rules and text
We split this skill into the rules it gives an agent
22 rules. About 0 are common, 10 are distinctive, and 12 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈10
Distinctive · estimate
This author's own choices. Worth a look.
12
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Very specific: line 27
All design state lives in .backend-design/state/*.json — one file per category, written by Phase-1 agents and the Phase-2 synthesis agent.
- Distinctive: line 35
Run before the pre-flight check.
- Very specific: line 58
After each subsequent phase succeeds, update the checkpoint via Read + Write on .backend-design/checkpoint.json (merge fields.
- Very specific: line 77
Read
.backend-design/config.json: If missing, tell the user: - Very specific: line 85
If <SKILL_DIR>/prompts/patterns/<patterns_key>.md does not exist, stop and tell the user: "Your detected framework <framework> is …
- Distinctive: line 134
After all four agents return, run the validator:
- Distinctive: line 385
After the synthesis agent finishes, run the validator:
- Distinctive: line 393
If the synthesis produced zero entities (e.g.
- Very specific: line 412
If the user asks for design edits in Phase 3, edit the underlying state JSON (with Edit) and re-run render-design.mjs.
- Very specific: line 420
After backend-design.md is rendered and before the review gate, run the deterministic gap detector:
- Very specific: line 439
After it finishes, surface a one-line summary to the user: <N> blocker(s) · <M> wire-up(s) · <K> info item(s).
- Distinctive: line 471
After the skeptic agent finishes:
- Distinctive: line 477
If the skeptic agent writes zero findings, that's a valid outcome — small or simple designs frequently have nothing concrete to flag.
- Distinctive: line 489
Stop: Do not call any more tools.
- Very specific: line 512
Schema-only branch: If config.stack.id === "s2ai-schema", do not spawn a codegen agent.
- Very specific: line 532
Startup smoke test (Node stacks): After the static checks pass, run a startup smoke test to catch errors that tsc doesn't see (missing env …
- Very specific: line 550
Startup smoke test (Python/FastAPI): Run python -c "from app.main import app" (already in the verification list).
- Distinctive: line 552
If a verification step fails, classify before fixing:
- Distinctive: line 555
If both budgets are exhausted, stop and report the remaining failures to the user rather than declaring success.
- Distinctive: line 557
Do not declare the task complete until verification passes.
- Very specific: line 572
Run the validator at every synthesis step: After Phase 1 (4 inventory files) and after Phase 2 (entities/relationships/auth).
- Very specific: line 573
One review gate, not many: Don't pepper the user with AskUserQuestion calls during Phase 1 or 2 unless something is genuinely ambiguous …
≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
backend-design
You are about to design and scaffold a backend for an existing frontend in the current working directory. Supported frontends (auto-detected by the CLI): Next.js (App or Pages Router), React SPA, Vue, Nuxt, Svelte, SvelteKit, Angular 17+, Astro, SolidJS / SolidStart, Qwik, Remix / React Router v7, Gatsby, HTMX, vanilla HTML/JS. The frontend is the source of truth: every button, form, fetch call, and route in the UI implies a backend obligation. Your job is to extract those obligations, get the user's approval on the shape of the backend, and then build it in the stack they chose.
Skill root: locate this skill's directory once at the start. It's the directory containing this SKILL.md. Refer to it as <SKILL_DIR> below — typical values are <project>/.claude/skills/backend-design (project-local install — the default for npx backend-design start), ~/.claude/skills/backend-design (global install via backend-design install), or ~/.claude/plugins/.../skills/backend-design (plugin install). Resolve all <SKILL_DIR>/... references against the actual location, not the literal string.
The stack is not fixed — it's selected by the user via npx backend-design start before activating this skill, and recorded in .backend-design/config.json. Supported stacks:
stack.id | Codegen prompt |
node-express-prisma | prompts/codegen-node-express-prisma.md |
node-fastify-prisma | prompts/codegen-node-fastify-prisma.md |
node-hono-drizzle | prompts/codegen-node-hono-drizzle.md |
nextjs-prisma | prompts/codegen-nextjs-prisma.md |
python-fastapi | prompts/codegen-python-fastapi.md |
s2ai-schema | (no prompt — Phase 4 runs scripts/render-s2ai-schema.mjs to emit ./schema.mmd only) |
Workflow
Execute the phases in order: Pre-flight → Phase 1 (inventory) → Phase 2 (synthesis) → Phase 2.5 (gap detection) → Phase 2.6 (skeptic pass) → Phase 3 (review gate) → Phase 4 (codegen). Phase 3 is a hard stop — do not start Phase 4 until the user approves the design doc.
All design state lives in .backend-design/state/*.json — one file per category, written by Phase-1 agents and the Phase-2 synthesis agent. These JSON files are the single source of truth. The human-readable backend-design.md is rendered from them by scripts/render-design.mjs; the codegen agent in Phase 4 reads them directly.
A validator at <SKILL_DIR>/scripts/validate.mjs checks the state for invariants (FK targets exist, every endpoint has a UI trigger, every entity has a PK, etc.). Run it after each synthesis step and fix errors before continuing. If the validator emits the same error class three runs in a row, stop and surface the unfixable issues to the user — do not loop indefinitely.
Phase 0 — Resumption check
Run before the pre-flight check. Decides whether to start fresh or skip ahead based on what was last completed.
- Read
.backend-design/checkpoint.jsonwith theReadtool. If missing → fresh run; proceed to Pre-flight. - Run the decision script:
It prints one line of JSON:node <SKILL_DIR>/scripts/checkpoint.mjs decide{"action": "...", "reason": "...", "next_phase": ...}. The script computes the current frontend signature (git HEAD + dirty hash if the repo is git-managed, otherwise a content hash over the source tree) and compares it tocheckpoint.frontend_signature. Branch on
action:actionWhat to do freshNo prior state. Proceed to Pre-flight → Phase 1. resume_phase_2Phase 1 inventory is on disk and frontend unchanged. Skip Phase 1; proceed to Phase 2. resume_phase_2_5Phase 2 synthesis on disk. Skip Phases 1+2; run node <SKILL_DIR>/scripts/render-design.mjsto regeneratebackend-design.mdfrom existing state, then proceed to Phase 2.5. Never hand-render — the script is the source of truth.resume_phase_2_6Phase 2.5 gap detection on disk but skeptic pass not yet run. Skip Phases 1+2+2.5; proceed directly to Phase 2.6 (skeptic pass), then re-render the design doc, then Phase 3. resume_phase_3Design, next-steps doc, and skeptic findings exist. Skip everything except the Phase 3 review gate — print the summary and wait for approval. resume_phase_4Design approved but scaffold not generated. Skip to Phase 4 directly. gaps_onlyScaffold complete, frontend unchanged. Run node <SKILL_DIR>/scripts/detect-gaps.mjs, thennode <SKILL_DIR>/scripts/render-env-example.mjs, thennode <SKILL_DIR>/scripts/render-openapi.mjsto refresh./backend-design-next-steps.md,./backend-design.env.example, and./openapi.json. Report and stop — do not re-scaffold or re-render the design doc.fresh_with_gaps_preservedFrontend changed since the last run. Re-run from Phase 1. Preserve .backend-design/gaps.jsonso closure detection still works on this run.- Tell the user one line of context (echo the
reasonfield):Resuming at Phase 4 — design approved, scaffold not yet generated.etc. Forgaps_only, say something likeScaffold already generated and frontend unchanged — refreshing ./backend-design-next-steps.md and ./backend-design.env.example only.so it's clear no design or codegen work is being repeated.
After each subsequent phase succeeds, update the checkpoint via Read + Write on .backend-design/checkpoint.json (merge fields; never overwrite the whole file blindly):
| Phase | Fields to add |
| 1 | phase_1_at: <ISO-8601 now>, frontend_signature: <output of node <SKILL_DIR>/scripts/checkpoint.mjs signature> |
| 2 | phase_2_at: <now> |
| 2.5 | phase_2_5_at: <now> |
| 2.6 | phase_2_6_at: <now> |
| 3 (user approves) | design_approved_at: <now> |
| 4 (verification passes) | phase_4_at: <now> |
The signature is captured once at end of Phase 1 — not re-captured on later phases, because we want to detect "did the frontend change between this run and the last one," not "did the frontend change since the most recent phase."
Pre-flight check
Before Phase 1:
- Read
.backend-design/config.json. If missing, tell the user:Run
npx backend-design startfirst to detect your frontend and pick your stack. Then re-invoke this skill. Stop here. - Confirm to the user in one line:
Targeting <config.stack.label> with <config.auth.strategy> auth on <config.frontend.framework> → <config.output_dir>. Resolve framework patterns file. The patterns live at
<SKILL_DIR>/prompts/patterns/<patterns_key>.md— one small file per framework, drop-in for the<<PATTERNS>>slot. Do not read it yourself. The Phase 1 subagents will eachReadit directly; the orchestrator only needs the path. Call this path<<PATTERNS_FILE>>below.If
<SKILL_DIR>/prompts/patterns/<patterns_key>.mddoes not exist, stop and tell the user: "Your detected framework<framework>is unrecognized. Edit.backend-design/config.jsonto setfrontend.patterns_keyto one of the files inprompts/patterns/."- Create the state directory:
mkdir -p .backend-design/state
Phase 1 — Frontend inventory
Goal: catalog every screen, every component, every interactive element, and every relationship between them — enough that someone could rebuild the UI from the inventory alone. Be exhaustive, not selective. Partial coverage here corrupts every later phase.
Spawn four general-purpose subagents in parallel (single message, four tool calls). Each writes a single JSON file. general-purpose is the correct agent type here — Explore is read-only and cannot use Write, so it can't produce the output files this phase requires.
| Agent | Model | Output file |
| 1. Screens & navigation | sonnet | .backend-design/state/screens.json |
| 2. Component tree & shared state | sonnet | .backend-design/state/components.json |
| 3. Network calls & API contracts | sonnet | .backend-design/state/endpoints.json |
| 4. Forms, buttons, auth surface | sonnet | .backend-design/state/forms.json |
Pass the model parameter to the Agent tool when spawning each agent (e.g. Agent({ subagent_type: "general-purpose", model: "sonnet", prompt: "..." })). All four agents run on Sonnet — Haiku is not reliable enough for the inference required (template-literal URL resolution, validation rule extraction, existing-handler detection).
Each agent writes exactly one JSON file. Do not let them output markdown — only the JSON. If an agent writes markdown by mistake, re-spawn it with the schema reminder.
The four agents and their brief files:
| Agent | Brief file | Output file |
| 1. Screens & navigation | <SKILL_DIR>/prompts/inventory/screens.md | .backend-design/state/screens.json |
| 2. Component tree & shared state | <SKILL_DIR>/prompts/inventory/components.md | .backend-design/state/components.json |
| 3. Network calls & API contracts | <SKILL_DIR>/prompts/inventory/endpoints.md | .backend-design/state/endpoints.json |
| 4. Forms, buttons, auth surface | <SKILL_DIR>/prompts/inventory/forms.md | .backend-design/state/forms.json |
The orchestrator does NOT need to read these brief files. Spawn each subagent with the prompt template below — substituting the brief file path and output file path — and let the subagent Read both its brief and the patterns file directly. This keeps the orchestrator's context lean.
Prompt template (use verbatim, four times, varying only <BRIEF> and <OUTPUT>):
Read
<<PATTERNS_FILE>>first — it has the framework-specific search patterns you'll need. Then read<BRIEF>for your inventory instructions and JSON schema. Then walk the frontend and write your output to<OUTPUT>. Do not output markdown — only the JSON file.
Cache-friendly prompt ordering (do not skip): the prompt template above puts the stable, shared <<PATTERNS_FILE>> read instruction first, then the per-agent brief, then dynamic config. This ordering lets Anthropic's prompt cache reuse the shared prefix across all four parallel subagents — repeated runs cost a fraction of the first. Preserve this order when editing.
Phase 1b — Cross-file self-check
After all four agents return, run the validator:
node <SKILL_DIR>/scripts/validate.mjsIt will fail if any of the four files are missing or unparseable, and report Phase-1 cross-file issues (endpoints without UI triggers, components referenced by screens but missing from components.json, etc.). Fix gaps by re-spawning the relevant agent with a tighter brief — do not paper over gaps in the synthesis step. Apply the same three-iteration cap as Phase 2.
Checkpoint write (end of Phase 1): merge { phase_1_at: <ISO now>, frontend_signature: <run node <SKILL_DIR>/scripts/checkpoint.mjs signature> } into .backend-design/checkpoint.json.
Phase 2 — Design synthesis
Spawn one general-purpose subagent with model: "sonnet". It reads the four JSON files from Phase 1, infers entities and relationships, and refines endpoints. (Plan is read-only and cannot write files; use general-purpose.)
Give it this brief:
Read all four files in
.backend-design/state/(screens.json,components.json,endpoints.json,forms.json). They are the authoritative inventory of the React/Next.js frontend.Produce five output artifacts:
1.
.backend-design/state/entities.json— a JSON array of Postgres entities inferred from the UI. Each entity:{ "name": "Post", "table": "posts", "fields": [ {"name": "id", "type": "uuid", "pk": true, "default": "gen_random_uuid()"}, {"name": "title", "type": "text", "required": true}, {"name": "body", "type": "text", "required": true}, {"name": "user_id", "type": "uuid", "required": true, "fk": "users.id"}, {"name": "created_at", "type": "timestamptz", "default": "now()"}, {"name": "updated_at", "type": "timestamptz", "default": "now()"} ], "indexes": [{"columns": ["user_id"]}, {"columns": ["created_at"]}], "evidence": ["components/NewPostForm.tsx:12", "app/posts/[id]/page.tsx:1"] }Infer entities from: forms (input fields → columns), list/detail screens, and the
entities_displayedfield on each screen. Usesnake_casefor table/column names.Always include a
usersentity if auth is implied. Auth is implied when ANY of the following hold (the strongest signal wins, in this priority order):
forms.json -> auth_surface.signup.present === true→inferred_from: "signup_form"forms.json -> auth_surface.login.present === true→inferred_from: "login_form"- Any screen in
screens.jsonhasauth_required: true→inferred_from: "auth_required_screen"- Any
components.json -> shared_state.storage_keys[].keymatches/token|jwt|auth|session/i→inferred_from: "token_storage_key"- Any endpoint in
endpoints.jsonhas a non-nullauth_header→inferred_from: "auth_header"- Any
screens[].data_fetches[].urltargets/api/auth/*→inferred_from: "auth_path"- (Judgment-call fallback.) None of 1–6 fire AND the inventory shows at least one
forms.standalone_buttons[]orforms.forms[]whoseaction: "api_call"target is not a public-form pattern AND the mutated target entity is not persisted tolocalStorageincomponents.json -> shared_state.storage_keys[]. →inferred_from: "judgment:user_owned_mutations". Trust the inventory'sactionclassification — do not re-examine the button's current wiring. The inventory already applies an intent-over-wiring rule (seeprompts/inventory/forms.md): aSave/Favorite/Like/Heartbutton isapi_calleven when implemented asuseStateoralert(), because vibe-coded sites routinely ship with incomplete wiring. If you seeaction: "api_call"you must treat it as a user-owned mutation; do not reopencomponents/*.tsxto check whetherfetch()is actually called, and do not "reclassify" the button tolocal_statebased on absence of wiring. Signal 7's whole purpose is to commit to a schema-ready posture (placeholder users + nullableuser_idFKs) when the intent is present but the wiring isn't yet — overriding the inventory defeats the signal. If you genuinely believe the user wants ephemeral local state instead, surface it as an Open Question in addition to firing signal 7, not as a replacement for it. Public-form taxonomy (which suppresses signal 7): contact, contact-us, get-in-touch, newsletter, subscribe, subscription, waitlist, wait-list, feedback, support, support-ticket, help, lead, leads, demo, demo-request — match on the button/formslug,id,label, ORsubmits_tomatching/api/(contact|contact-us|contactus|get-in-touch|newsletter|subscribe|subscription|waitlist|wait-list|feedback|support|support-ticket|help|leads?|demo|demo-request)\b/. Wider suppression biases toward NOT emitting a placeholder users entity on borderline public-form apps; the Tier-1 open question is still your signal that the synthesis agent was unsure.When auth is implied without a UI form via signals 3–6, still emit the
usersentity, setauth.json -> signup: trueandauth.json -> inferred_from: "<signal-name>", and emitPOST /auth/signup/POST /auth/login/POST /auth/logout. Setinferred_from_signal: "<signal-name>"on each auth endpoint (use the same value asauth.json.inferred_from) and leavetriggered_by: []— the new field tells the validator the endpoint has no UI source. Do not pointtriggered_byat the winning signal's file:line; that fakes a UI trigger and confuses readers of the rendered design.detect-gaps.mjswill still raisemissing_auth_uias a blocker — that's correct; the user needs the UI even though the backend is built.When signal 7 fires (user-owned mutations but no auth surface at all), emit
usersas a placeholder shape only:id,created_at,updated_at, optionalrequired: false), and anotefield on the entity describing the placeholder state (e.g."note": "placeholder — no auth surface; add password_hash and required email when wiring login"). Nopassword_hashcolumn. Setauth.json.strategy: "none",auth.json.inferred_from: "judgment:user_owned_mutations", andauth.json.signup: false(detect-gaps.mjs readsauth.signupto decide whether a missing signup form is a blocker; signal 7 explicitly does not want one). Do NOT emitPOST /auth/signup//login//logout— v1 ships anonymously; emitting auth endpoints with no auth strategy spoofs an auth flow that doesn't exist. Emituser_idFKs asrequired: falseon owned entities (e.g. nullableuser_idonBooking,Favorite). Add a Tier-1 product-intent open question explaining the judgment call: question — "User-owned data exists but the frontend has no auth surface — promote to real auth or accept anonymous v1?"; recommendation — "Add a signup form before shipping; nullableuser_idis forward-compat but every record will be anonymous until auth lands."Best-practice columns on every entity (in addition to
id,created_at,updated_at):
{"name": "deleted_at", "type": "timestamptz", "required": false, "default": null}— soft delete. List/detail queries will filterWHERE deleted_at IS NULL;DELETEsets this column instead of removing the row.{"name": "version", "type": "integer", "required": true, "default": "1"}— optimistic lock. PATCH handlers increment by 1; if anIf-Matchheader is present they compare first and 409 on mismatch.- If (and only if) a
usersentity exists AND the entity in question is not theusersentity itself:{"name": "created_by", "type": "uuid", "required": false, "fk": "users.id"}and{"name": "updated_by", "type": "uuid", "required": false, "fk": "users.id"}. Self-referential audit FKs onuserscreate chicken-and-egg insert problems — skip them there.Webhook event logs, append-only audit tables, and any entity the agent reasonably judges has no concept of an actor or soft-delete may omit
deleted_at/version/created_by/updated_by. When in doubt, include them —validate.mjswarns if they're missing but does not error.Session entity (cookie sessions only). When
config.auth.strategy === "session"ANDconfig.auth.store === "postgres", emit aSessionentity alongsideusers:{ "name": "Session", "table": "sessions", "fields": [ {"name": "id", "type": "text", "pk": true}, {"name": "user_id", "type": "uuid", "fk": "users.id"}, {"name": "expires_at", "type": "timestamptz", "required": true}, {"name": "data", "type": "jsonb"}, {"name": "created_at", "type": "timestamptz", "default": "now()"} ], "indexes": [{"columns": ["user_id"]}, {"columns": ["expires_at"]}], "evidence": ["config.auth.strategy === 'session' && store === 'postgres'"] }Do NOT add the best-practice columns (
deleted_at,version,created_by,updated_by) toSession— session rows are server-managed, ephemeral, deleted hard on logout/expiry, and have no concept of an "actor" beyond the user the FK already names. The validator's exception list covers this. Whenstore === "redis"do not emitSession(the validator will error if you do).2.
.backend-design/state/relationships.json— a JSON array:{ "from": "Post", "to": "User", "type": "many-to-one", "fk": "Post.user_id", "on_delete": "cascade", "evidence": ["app/posts/[id]/page.tsx:45"] }Cardinalities:
one-to-one,one-to-many,many-to-one,many-to-many. For many-to-many, include ajoin_tablefield. Evidence patterns:
- List view of X showing "by Y" → X belongs to Y (many-to-one)
- Detail page for X rendering a list of Y → X has many Y (one-to-many)
- Form for X with
<select>of Y → X has FK to Y- Multi-select / tag input → many-to-many
- Nested URL
/x/[xId]/y/[yId]→ Y belongs to X3.
.backend-design/state/auth.json— JSON object describing auth. JWT shape:{ "strategy": "jwt", "algorithm": "HS256", "expiry": "7d", "secret_env": "JWT_SECRET", "password_hash": "bcrypt", "bcrypt_cost": 12, "signup": true, "email_verification": false, "password_reset": false, "oauth_providers": [], "rbac_roles": [] }Cookie session shape (when
config.auth.strategy === "session"— pre-set by the CLI inconfig.json, never inferred from the UI):{ "strategy": "session", "store": "postgres", "cookie": { "name": "sid", "httpOnly": true, "secure": true, "sameSite": "lax", "maxAgeDays": 7 }, "csrf": { "enabled": true, "header_name": "X-CSRF-Token" }, "password_hash": "bcrypt", "bcrypt_cost": 12, "signup": true, "email_verification": false, "password_reset": false, "oauth_providers": [], "rbac_roles": [] }Copy
store,cookie, andcsrfshapes verbatim from the above when the strategy is session. Thecookie.secureflag is interpreted "true in production" by codegen (process.env.NODE_ENV === "production"); leave the literaltrueinauth.json. Whenstrategy === "session",POST /auth/logoutis meaningful — it destroys the server-side session row (Postgres) or key (Redis) — not the JWT-strategy no-op. Add atriggered_byevidence pointer to the logout button (or, if the UI doesn't have one yet, the screen whose presence implies authenticated sessions) so the validator's "every endpoint has a UI trigger" invariant holds.Set fields from
forms.json -> auth_surface. Addrbac_rolesifscreens.jsonshows admin-only pages or role-gated UI.4. Refine
.backend-design/state/endpoints.json— read the existing endpoints file, then add implied CRUD endpoints not yet listed:
- List view of X →
GET /x(returns array)- Detail view of X →
GET /x/:id- "New X" form →
POST /x- "Edit X" form →
PATCH /x/:id- "Delete X" button →
DELETE /x/:id- Signup form →
POST /auth/signup- Login form →
POST /auth/login- Logout button →
POST /auth/logout(or stateless)Multipart endpoints. If a form in
forms.jsonhasmultipart: true, the endpoint it submits to must havecontent_type: "multipart/form-data"andrequest_bodyshould list each non-file field by name + type plus each file field as{ "<name>": "file", "accept": "<mime>", "max_size_mb": N }. Other endpoints default tocontent_type: "application/json"(omit when JSON).Optional body fields. Each
request_bodyvalue is either a string type shorthand ("title": "string"— required by default) or an object ("cover_image": {"type": "string", "required": false}). Onlyrequired: falseopts a field out — absent ortrueboth mean required. Use the object form for any form input whose inventory entry hasvalidation.required: false.Path-param scoping (nested resources). For nested URLs like
/orgs/:orgId/posts, setpath_paramson the endpoint:{ "method": "GET", "path": "/api/orgs/:orgId/posts", "path_params": [{"name": "orgId", "type": "uuid", "scopes_query": true, "parent_entity": "Org"}], "response": "Post[]" }
scopes_query: truemeans the handler must filter the list/detail query by this param (e.g.WHERE org_id = :orgId) AND check the authenticated user has access to the parent entity. Apply this to every nested list, detail, create, update, and delete. The "child" FK column name is<parent_table>_idby convention. Leaf params on detail routes (e.g.:idat the end of/api/orgs/:orgId/posts/:id) getscopes_query: false— they identify the record, not scope the query.Auth-flow endpoints (derive from
auth.jsonflags):
password_reset: true→POST /auth/password-reset/request(body:{ email }) +POST /auth/password-reset/confirm(body:{ token, new_password })email_verification: true→POST /auth/verify-email(body:{ token }) +POST /auth/verify-email/resend(auth required)- Each entry in
oauth_providers→GET /auth/oauth/<provider>/start+GET /auth/oauth/<provider>/callbackAll of these should have
auth: "none"(the request is unauthenticated; the token in the body is the authenticator). Settriggered_byto the auth-surface evidence fromforms.json(e.g. the "Forgot password" link's file:line). If no such UI element exists, leavetriggered_by: []and setinferred_from_signal: "<matching-signal>"instead — same convention as signup/login/logout when inferred from signals 3–6.For each added endpoint, set
triggered_byto the UI element file:line that justifies it. Add anauthfield (requiredornone) to every endpoint.RBAC. If
auth.rbac_rolesis non-empty, deriverequired_roleper endpoint:
- If the endpoint is triggered exclusively from a screen that's clearly admin-only (e.g. path under
/admin/, "Admin" in the screen id/label, an admin role gate visible in the component tree), setrequired_role: "admin"(use the matching role fromauth.rbac_roles).- For endpoints triggered from a screen with a per-role gate, set
required_roleto the gating role.- For endpoints with no clear role gate, leave
required_role: null. Don't speculate.- Every
required_rolevalue must already exist inauth.rbac_roles— if you'd write a role that isn't there, surface it as an Open Question instead.Do not invent features the UI does not imply. Do not add admin endpoints unless an admin page exists in
screens.json. Be skeptical of speculative endpoints. Note: signal 7 (judgment-callusersfor user-owned mutations) is not feature invention — it's a codified rule that emits a placeholderusersshape and no auth endpoints. Anything beyond that placeholder still falls under this rule.Placeholder endpoints (vibe-coder mode only). If
config.json -> vibe_coder === true, scanforms.standalone_buttons[]for entries withaction: "api_call"whose target either is missing or doesn't appear inendpoints.json. For each such button, add a placeholder endpoint toendpoints.jsonwith:{ "method": "POST", "path": "/api/<best-guess-from-label>", "request_body": {}, "response": null, "triggered_by": ["<button file:line>"], "auth": "required", "temporary": true, "placeholder_reason": "Scaffolded for orphan button '<label>' at <button file:line>. Path is a best-guess; user must replace or wire up the real endpoint.", "evidence": ["<button file:line>"] }Infer method from the button label: "Delete/Remove" →
DELETE; "Edit/Update/Save" →PATCH; everything else →POST. Treat "Cancel" asPOSTonly if the button is clearly destructive (e.g. "Cancel subscription"); ambiguous labels like "Cancel" inside a modal typically just close the dialog and should not become endpoints. Always setauth: "none"on placeholders, even when the origin screen is auth-gated — codegen strips auth middleware from placeholder routes anyway (codegen-placeholders.md), so settingrequiredwould mislead readers ofbackend-design.md. The user re-adds auth when they replace the stub. Infer path from a slugified version of the label scoped under the screen's URL when possible (e.g. button "Become a host" →/api/hosts/apply). When in doubt, prefix with/api/and slugify the label. Settemporary: trueandplaceholder_reasonon every such endpoint — these fields signal codegen to scaffold a 501 stub rather than a real handler. Do not invent request body fields; leaverequest_body: {}. Do not infer indices or relations from a placeholder. Ifvibe_coderis false or unset, never generate placeholder endpoints — flag orphan buttons as gaps in Phase 2.5 instead.Skip external endpoints. Endpoints with
is_external: trueare calls to third-party services (Stripe, OpenAI, etc.). Do not refine them, do not derive implied CRUD around them, and do not assign them anauthfield. Carry them through unchanged so the design doc can list them under "External integrations".Domain pattern pass. Before writing
open_questions.json, scanscreen.id,screen.path,component.name, andendpoint.path(all lowercased, joined into one corpus) for these keyword classes. A class triggers only when ≥2 distinct tokens match — single-token matches generate too many false positives.
Class Tokens (substring match) Recommended entities ecommercecart,checkout,product,price,sku,order,basketOrderLineItem,Inventory,Address,Paymentchatmessage,conversation,thread,chat,inbox,dmConversation,Message,Participantsocialpost,comment,like,follow,feed,friend,react,reactionLike,Follow,Notificationbookingreserve,listing,book,availability,slot,check_in,check_out,guest,stayBooking,Availabilitycmsarticle,blog,tag,category,draft,publish,revisionTag,ArticleTag(join),RevisionFor each triggered class, write one entry in
open_questions.jsonwithid: "domain-<class>-entities", evidence file:lines from the matching screens/components, and an opinionated recommendation tied to v1 cadence. Do not add these entities toentities.json— the user accepts or rejects in Phase 3. Example:{ "id": "domain-ecommerce-entities", "title": "E-commerce signals detected — scaffold deeper entities?", "question": "Cart + checkout + price tokens are present in the UI. Should the backend include OrderLineItem, Inventory, Address, and Payment entities now, or wait until each is wired to real UI?", "context": "Tokens matched: cart (components/CartDrawer.tsx:14), checkout (app/checkout/page.tsx:1), price (components/PriceTag.tsx:8). Only an Order entity was inferred from the visible checkout form.", "recommendation": "Scaffold OrderLineItem and Address for v1 — they're load-bearing for any real order. Defer Inventory and Payment until a payment provider is picked (each has heavy provider coupling).", "evidence": ["components/CartDrawer.tsx:14", "app/checkout/page.tsx:1", "components/PriceTag.tsx:8"] }5.
.backend-design/state/open_questions.json— a JSON array of product-intent ambiguities that the design surfaces but cannot resolve from the UI alone. These are the questions where the codebase doesn't have a single right answer and the human has to choose. Each question must include a recommendation — your honest best-practice opinion given what you observed in the frontend — so the user has a default to accept rather than a question they have to answer cold. Schema:[ { "id": "reserve-button-behavior", "title": "Reserve button behavior", "question": "Today the Reserve button only fires alert('Reserved!'). Do you want this backend to scaffold a bookings table and POST /api/bookings now, or stay read-only until the booking UI exists?", "context": "The form's date/guests inputs imply a minimal booking { listing_id, check_in, check_out, guests, user_id } shape — but user_id requires auth, which the UI also lacks.", "recommendation": "Stay read-only for v1. Scaffolding bookings before the booking UI + auth exist tends to go stale fast. Add bookings in a follow-up once the user actually navigates the reservation flow.", "evidence": ["components/Reserve.tsx:34"] } ]Surface 3–8 questions for a typical app. Look for: (a) buttons/forms that fire alerts/console.logs (stub UI hinting at unbuilt features), (b) missing admin/CRUD UI that the data model implies, (c) search/filter UI that's wired to nothing, (d) static display strings that look like they should be derived from a real backend (e.g.
distance,dates), (e) auth gaps the design depends on but the UI doesn't reflect. Do NOT include: env vars, missing accounts, or orphan wire-ups — those live inbackend-design-next-steps.md. Recommendations should be opinionated and time-aware ("for v1", "until X exists"). If no real product-intent ambiguities exist, write an empty array[]— don't pad.Write all five artifacts. Do not produce markdown — only JSON.
After the synthesis agent finishes, run the validator:
node <SKILL_DIR>/scripts/validate.mjsIf it exits non-zero, read the errors, fix them by editing the JSON files directly (use Edit), and re-run. Do not proceed to the markdown render or Phase 3 until validation passes. Warnings are OK to leave but should be acknowledged in Open Questions. Cap fixup attempts at three iterations; if the same error class persists after three passes, stop and surface the unfixable issues to the user.
If the synthesis produced zero entities (e.g. a marketing-only frontend with no forms or data), do not proceed to codegen. Tell the user: "No backend obligations detected — the UI doesn't imply any persistent state or API calls. Nothing to scaffold." Stop.
Once validation passes, render backend-design.md at repo root deterministically via the script:
node <SKILL_DIR>/scripts/render-design.mjsThe script reads the state JSON and writes the markdown. Do not hand-render this file — runs become non-deterministic and diffs become noisy. The script emits these 8 sections:
- Overview — one paragraph inferred from the UI.
- Entity map — markdown list of entities and relationships with cardinalities.
- Data model — one section per entity from
entities.json, with column tables. - API endpoints — one row per endpoint from
endpoints.json(Method | Path | Auth | Body | Response | Triggered by), with placeholders called out separately. - Auth model — rendered from
auth.json. - External integrations — endpoints with
is_external: true, grouped byexternal_origin. NOT scaffolded; documentation only. - Coverage check — every screen × data fetch, plus unwired buttons.
- Open questions — product-intent ambiguities only (e.g. "should new accounts be email-verified?"). Environmental gaps (env vars) and wire-up gaps (orphan buttons) belong in
backend-design-next-steps.md— generated in Phase 2.5.
If the user asks for design edits in Phase 3, edit the underlying state JSON (with Edit) and re-run render-design.mjs. Do not hand-edit backend-design.md — your edits will be overwritten next time the script runs.
Checkpoint write (end of Phase 2): merge { phase_2_at: <ISO now> } into .backend-design/checkpoint.json.
Phase 2.5 — Gap detection & next-steps render
After backend-design.md is rendered and before the review gate, run the deterministic gap detector:
node <SKILL_DIR>/scripts/detect-gaps.mjsIt reads the state JSON, config.json, ./.env, and ./package.json; writes .backend-design/gaps.json and ./backend-design-next-steps.md. No agent needed — it's a plain script, same shape as validate.mjs.
Exit code: 0 if no blocker gaps, 1 if blocker gaps exist. This is not a script failure — a 1 exit means the detector ran successfully and found blockers the user needs to resolve. Do not treat it as an error; read the summary line and continue.
What it catches:
missing_env_var— env vars required by the chosen stack/auth that aren't in.env(e.g.DATABASE_URLfor Postgres stacks,JWT_SECRETwhen auth is JWT,STRIPE_WEBHOOK_SECRETfor Stripe webhooks,<PROVIDER>_CLIENT_ID/_SECRETper OAuth provider).missing_auth_ui—auth.jsonhas signup/login butforms.auth_surfaceshows the frontend has no signup/login form. Also: screens markedauth_required: truewhile no auth surface exists.unwired_button—forms.standalone_buttons[]entries withaction: "api_call"whose target doesn't match any endpoint. The script does not invent a path — it surfaces the button as a decision the user must make.external_account_unconfirmed— Supabase / Stripe / OpenAI references detected viapackage.jsondeps oris_externalendpoint origins. Informational only.
The script re-detects from scratch every run and diffs against the persisted gaps.json using (type, evidence_file, specifier) as the natural key. Gaps the user has closed since the last run show up under "Recently closed" in the next-steps doc.
After it finishes, surface a one-line summary to the user: <N> blocker(s) · <M> wire-up(s) · <K> info item(s).
Then render the env template so the user has a copy-pasteable .env to fill in:
node <SKILL_DIR>/scripts/render-env-example.mjsThis writes ./backend-design.env.example at the repo root with every env var the design requires (DATABASE_URL, JWT_SECRET, webhook secrets per source, OAuth <PROVIDER>_CLIENT_ID/SECRET, email provider options, UPLOADS_DIR, NODE_ENV/PORT). Each var has a one-line comment explaining what it's for and where to get the value. The user can cp backend-design.env.example .env and fill in the blanks.
Exception: for the s2ai-schema stack, render-env-example.mjs returns without writing — there is no server to configure, so don't surface a "wrote env template" message. detect-gaps.mjs also skips its missing_env_var checks for this stack; the remaining unwired_button, missing_auth_ui, and external_account_unconfirmed gaps still apply and are still surfaced in backend-design-next-steps.md.
Then render the OpenAPI spec so consumers can preview the API contract without booting the backend:
node <SKILL_DIR>/scripts/render-openapi.mjsThis writes ./openapi.json at the repo root — OpenAPI 3.1 with one operation per endpoint, a top-level webhooks block for incoming webhooks, and components.schemas per entity. The Phase 4 codegen prompts copy this file into the scaffolded backend so the Swagger UI at /docs serves the same contract design-time reviewers see. Placeholder endpoints (temporary: true) and external endpoints (is_external: true) are skipped — the spec describes the real contract, not stubs. The s2ai-schema stack also skips this step (no server, no API to document).
Checkpoint write (end of Phase 2.5): merge { phase_2_5_at: <ISO now> } into .backend-design/checkpoint.json.
Phase 2.6 — Skeptic pass
Spawn one general-purpose subagent with model: "sonnet". This agent re-reads the synthesized design from an adversarial stance and surfaces concrete-pattern concerns as additional Open Questions. It runs after gap detection so it can also see gaps.json. It does NOT pause for input — the user sees its findings in the design doc at the Phase 3 review gate.
Spawn it with this prompt (do NOT read the brief yourself — the subagent reads it directly, keeping the orchestrator's context lean):
Read
<SKILL_DIR>/prompts/skeptic.mdfor your full brief and output schema. Then run the skeptic pass against.backend-design/state/*.json,.backend-design/config.json, and.backend-design/gaps.json. Write your augmented array to.backend-design/state/open_questions.jsonand your raw findings to.backend-design/state/skeptic_findings.json. Cap output at 8 questions.
After the skeptic agent finishes:
- Re-run the validator (
node <SKILL_DIR>/scripts/validate.mjs) to ensure the appended questions are well-formed. - Re-render the design doc so the new findings appear in the Open Questions section:
node <SKILL_DIR>/scripts/render-design.mjs. - Report a one-line summary to the user:
Skeptic pass: <N> finding(s) — see Open Questions in backend-design.md.
If the skeptic agent writes zero findings, that's a valid outcome — small or simple designs frequently have nothing concrete to flag. Don't re-spawn to "find something."
Checkpoint write (end of Phase 2.6): merge { phase_2_6_at: <ISO now> } into .backend-design/checkpoint.json.
Phase 3 — Review gate (do not skip)
After Phase 2.5 completes:
- Read
.backend-design/state/design-summary.json(emitted byrender-design.mjsalongside the markdown — ~500 bytes of pre-computed counts) and.backend-design/gaps.json. Do not readbackend-design.md— it's for the user, not for the model summary step. Print a short summary to the user using fields from these two files: entity count, table count, endpoint count, whether auth is included (auth_enabled),coverage_check.coveredvs.coverage_check.flagged,open_question_count(split as<X> product-intent · <Y> skeptic-passusingskeptic_countfrom the same summary file), and<N> blocker(s) · <M> wire-up(s) · <K> info item(s)fromgaps.json. - Tell the user: "Four files are ready for review:
./backend-design.md(the design, including a recommended answer to every open question),./backend-design-next-steps.md(env vars, wire-up TODOs, accounts to set up),./backend-design.env.example(copy to.envand fill in), and./openapi.json(OpenAPI 3.1 contract — load it into Stoplight, Swagger Editor, ornpx openapi-typescriptto preview before scaffolding). Let me know to proceed, or describe any changes." - Stop. Do not call any more tools. Wait for the user's next message.
If the user asks for design edits, edit the underlying state JSON (e.g. .backend-design/state/entities.json) with Edit, then re-run node <SKILL_DIR>/scripts/render-design.mjs AND node <SKILL_DIR>/scripts/render-openapi.mjs so both the design doc and the OpenAPI spec stay in sync with the state. Never hand-edit backend-design.md or openapi.json directly — they're overwritten by the renderers. For substantial redesigns, re-spawn the Phase 2 synthesis agent with the revised requirements. Do not hand-edit backend-design-next-steps.md either — it's regenerated by detect-gaps.mjs. If the user closes a gap (e.g. sets an env var, adds an auth form), re-run node <SKILL_DIR>/scripts/detect-gaps.mjs instead.
Only proceed to Phase 4 when the user gives explicit approval ("looks good", "proceed", "ship it", "go", etc.). Blockers in gaps.json are not hard gates for Phase 4 (codegen can still run with a missing DATABASE_URL), but warn the user that the scaffold won't boot until they're resolved.
Checkpoint write (on user approval): merge { design_approved_at: <ISO now> } into .backend-design/checkpoint.json before starting Phase 4.
Phase 4 — Code scaffolding
The codegen prompt is stack-specific. Read .backend-design/config.json to get stack.id, then read the matching prompt file from <SKILL_DIR>/prompts/:
stack.id | Prompt file to read |
node-express-prisma | <SKILL_DIR>/prompts/codegen-node-express-prisma.md |
node-fastify-prisma | <SKILL_DIR>/prompts/codegen-node-fastify-prisma.md |
node-hono-drizzle | <SKILL_DIR>/prompts/codegen-node-hono-drizzle.md |
nextjs-prisma | <SKILL_DIR>/prompts/codegen-nextjs-prisma.md |
python-fastapi | <SKILL_DIR>/prompts/codegen-python-fastapi.md |
s2ai-schema | (no prompt — see schema-only branch below) |
Schema-only branch. If config.stack.id === "s2ai-schema", do not spawn a codegen agent. Instead run:
node <SKILL_DIR>/scripts/render-s2ai-schema.mjsIt reads .backend-design/state/*.json and writes ./schema.mmd at the repo root (the chosen output_dir is ignored for this stack). No verification commands apply — there is no compile step, no startup smoke test. Report to the user: file path, entity count, relationship count, and how many non-CRUD endpoints were emitted as commented hints. Skip directly to the Phase 4 checkpoint write. The user supplies @dictionary regex blocks and the @service auth block by hand, then runs s2ai's own make pipeline against the file.
Determine whether placeholders apply: scan .backend-design/state/endpoints.json for any endpoint with temporary: true. Do not read the codegen prompt yourself — pass its path to the subagent and let it read directly. Spawn one general-purpose subagent with model: "sonnet" and the following prompt (substitute the bracketed values):
The authoritative spec is in
.backend-design/state/*.json. Stack:<config.stack.id>(<config.stack.label>), auth strategy:<config.auth.strategy>, output directory:<config.output_dir>.Read
<SKILL_DIR>/prompts/codegen-<config.stack.id>.mdfor your scaffolding brief and follow it exactly. The brief now includes a## Tests (required)section and a## Security baselinesection — implement both. After scaffolding, run the verification commands listed at the bottom of that file (Node stacks:<PM> install, ORM generate,<PM> tsc --noEmit,<PM> test; Python:pip install -e .,alembic check, import smoke,pytest). Tests run against a real ephemeral Postgres via testcontainers — Docker must be running on the host. If Docker is not available, report it and skip the test step rather than failing the whole verification. Report any other failures.When done, output the exact "README run commands" section verbatim from the codegen brief — the orchestrator forwards it to the user.
[Include this line ONLY when any endpoint has
temporary: true:] Also read<SKILL_DIR>/prompts/codegen-placeholders.md— it overrides the brief for endpoints markedtemporary: true(scaffold them as 501 stubs, strip auth, leave bodies empty).
This keeps the codegen prompt content (6–9 KB) out of the orchestrator's context — the subagent reads, scaffolds, verifies, and reports back the run commands.
Startup smoke test (Node stacks). After the static checks pass, run a startup smoke test to catch errors that tsc doesn't see (missing env vars, plugin order, runtime imports):
cd <output_dir>
JWT_SECRET=test DATABASE_URL=postgresql://postgres:postgres@localhost:5432/none \
timeout 8 <PM> dev 2>&1 | tee /tmp/bd-smoke.log &
SMOKE_PID=$!
sleep 6
kill $SMOKE_PID 2>/dev/null || true
grep -Eqi "listen(ing)?|started server|ready" /tmp/bd-smoke.log || {
echo "Startup smoke test failed — server did not announce 'listening' within 6s."
cat /tmp/bd-smoke.log
exit 1
}The DB host is intentionally unreachable; Prisma/Drizzle connect lazily on the first request, so startup should still announce "listening". If your stack instead opens a connection at boot, swap in a real local Postgres or skip this step and document why.
Startup smoke test (Python/FastAPI). Run python -c "from app.main import app" (already in the verification list). This catches model/router import errors at the import level, which is the most common failure mode.
If a verification step fails, classify before fixing:
- Local mistakes (typo, missing import, wrong type) — fix directly with
Edit. Budget ≤5 such edits. - Structural issues (missing files, route/schema mismatch, multiple TypeScript errors across files) — re-spawn the codegen agent with a focused follow-up brief that quotes the failing output. Budget at most one re-spawn.
- If both budgets are exhausted, stop and report the remaining failures to the user rather than declaring success.
Do not declare the task complete until verification passes.
Checkpoint write (end of Phase 4): merge { phase_4_at: <ISO now> } into .backend-design/checkpoint.json.
Then report to the user: stack used, output directory, entity count, endpoint count, and the run commands the subagent reported back from the codegen brief's "README run commands" section.
Rules
- Do not skip Phase 3. The user must see and approve the design doc before any backend code is written. This is the whole point of the skill.
- Frontend is the source of truth for product behavior. Never invent endpoints, tables, or fields that represent product features the UI doesn't imply (e.g. don't add an admin dashboard's endpoints unless an admin page exists; don't add a notifications feed unless the UI shows one). Best-practice infrastructure is scaffolded automatically — auth from non-UI signals (
auth_requiredscreens, token storage keys, bearer headers), soft-delete (deleted_at), optimistic-lock (version), and audit columns (created_by/updated_by) on every entity. Domain patterns (e-commerce, chat, social, booking, CMS keyword hits) go through Open Questions with recommendations — surfaced for the user to accept or reject, never silently added. If the UI is missing something obvious (e.g., login form but no signup form), surface it in Open Questions instead of silently adding it. - Respect the chosen stack.
.backend-design/config.jsonrecords the user's choice. Do not deviate — if they pickedpython-fastapi, do not generate TypeScript. Ifconfig.jsonis missing, tell them to runnpx backend-design startfirst. - Don't touch the frontend unless
stack.framework === "nextjs". For the Next.js stack, you may add files underapp/api/,lib/, andprisma/in the existing project. For all other stacks, write only toconfig.output_dir(default./backend),./backend-design.md, and./.backend-design/. Exception:s2ai-schemawrites a single file./schema.mmdat the repo root regardless ofoutput_dir. - State JSON is the source of truth. Codegen reads from
.backend-design/state/*.json.backend-design.mdis rendered deterministically from the JSON byscripts/render-design.mjs— any hand-edits to it will be overwritten. When the user wants design changes, edit the JSON and re-render. - Run the validator at every synthesis step. After Phase 1 (4 inventory files) and after Phase 2 (entities/relationships/auth). Never proceed past errors.
- One review gate, not many. Don't pepper the user with
AskUserQuestioncalls during Phase 1 or 2 unless something is genuinely ambiguous about their intent for the skill (not about the UI — that goes in Open Questions in the design doc).
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

