Use when building, designing, or scaffolding a backend REST/CRUD API...
jerrycan turns a single declarative design.json into a working, tested,
multi-tenant REST backend: it generates the data layer (SeaORM models, dual-dialect
migrations, CRUD repos), typed handler stubs, tenant guards + isolation tests,
acceptance tests, OpenAPI, and app wiring. You (the agent) author the design and
fill in the handler bodies; jerrycan check is the source of truth for "done."
This skill is a guided process. Work through the phases in order. At each
decision point, ask the user — do not guess. Checkpoint after every phase so
neither of you loses the thread. The docs are complete and accurate (jerrycan docs --list); read the relevant page before each step rather than guessing an API.
jerrycan check and the validator are the truth. Loop them; never claim
green you haven't seen. Never weaken a generated check to pass it. But a few
generated tests are un-greenable BY CONSTRUCTION (a happy-path probe that
posts no credential to a login/webhook/API-key endpoint; the 404-probe sent
as GET to a POST-only route). For those, check will not be fully green and
that is correct — recognize them (see Phase 5), leave them, and move on; do
NOT thrash trying to green them or weaken the handler.jerrycan docs <page> /
jerrycan explain <CODE>. Start every project by reading jerrycan docs designing.jerrycan --version (build it if you're in the
framework repo: cargo build -p jerrycan then use target/debug/jerrycan).jerrycan docs --list to see the page index. Read jerrycan docs designing
now — it is the complete design.json reference (every field, type,
constraint, and the gotchas). You will author the design from it.Goal: a precise list of resources, operations, actors, and cross-cutting needs. Ask one question at a time, prefer multiple-choice. Cover, in roughly this order (skip what's already obvious from the request):
POST /postings/{id}/close)? Which need auth/roles? Which are public?Checkpoint: write back a structured summary (resources + fields + relations + auth + tenancy + jobs + endpoints) and get explicit confirmation before designing.
The frontend already encodes the contract; derive the backend from it instead of guessing.
fetch(, axios, apiClient, an
api//services//hooks/ dir, an OpenAPI/*.d.ts types file, or env vars
like *_API_URL/baseURL.Authorization: Bearer …, an API-key header, a cookie).If the frontend expects a shape jerrycan can't express as an entity (composite /aggregate/nested payloads), note it now — it becomes a hand-written
Json<Value>handler (see Phase 2 + the gotchas).
Before designing, map every requirement onto jerrycan's envelope. If something falls outside it, raise it with the user now and decide: descope, hand-write inside a handler (within the limits), or use an external service.
jerrycan builds well (in-scope): multi-tenant REST/CRUD JSON APIs · relations
(belongs_to + on_delete) · string enums · session/JWT auth + roles · OAuth2
client + scoped API keys · cron + background jobs (retries, dead-letter,
idempotency) · signed webhooks (RawBody + HMAC) · multipart upload parsing +
streaming download · CORS + rate limiting · /healthz + Prometheus /metrics +
OpenAPI · jerrycan package (binaries/containers/k8s/systemd).
Hard walls (jerrycan will NOT design or scaffold these — decide with the user):
| Need | Status | Handling |
|---|---|---|
| WebSockets / SSE / realtime / push | Out of scope | Separate service; not jerrycan |
| GraphQL / gRPC / JSON-RPC | Out of scope | REST only; remodel as REST or separate service |
| Aggregate / filter / search / pagination / reporting queries | Not design-expressible | Hand-write raw SeaORM in the agent-owned repo.rs/handler |
| Composite / nested / computed response shapes | request_body/success are entity-only |
Hand-write a Json<Value> handler (declare success.status only) |
| File / blob / object storage (S3/R2/disk) | No storage primitive | Multipart parses uploads; persist via an external SDK wired in code |
| Custom middleware / interceptors | Fixed kit only (CORS, rate-limit, access-log) | Not extensible per-route in v2 |
| Multi-step workflows / job chains / priorities | Jobs are single-shot | Out (the jobs contract is capped) |
| WebAuthn / SAML / RS256 JWT | Out of scope | session/JWT(HS) + OAuth2-client + API keys only |
std::process / std::fs / raw sockets in handlers |
Forbidden by the JL0007 lint | Go through a framework extension or an allow-hatch (rare) |
Checkpoint: confirm the in-scope design and the agreed handling for any wall.
Build the design incrementally, validating as you go. Reference jerrycan docs designing for every construct. Work in this order, confirming each:
name, contract_version: 1, dependencies (pick from db,
auth, validate, observe, oauth — db switches on SQL mode; oauth
wires the OAuth client and implies auth).tenancy: { entity, member_roles }. Every entity
with a belongs_to aimed at the tenant entity becomes tenant-scoped (scoped
repos + a Tenant guard + generated cross-tenant isolation tests).auth: { model: session|jwt, roles: [...] }. (required_roles on an
endpoint uses these roles, NOT tenancy.member_roles — see gotchas.)type, required, unique, index;
status fields as type: "string" + values: [...] (the enum mechanism);
relations via belongs_to: [{ entity, on_delete }].operation_id, method, path (incl. /{id}),
success: { status, entity?, list? }, errors, auth_required/required_roles,
and public: true for login/register/webhooks.jobs: [{ name, schedule: "<5-field cron>", queue? }] (requires db).Validate frequently by scaffolding into a temp dir and reading the validator's diagnostics — they are precise JSON-pointer-addressed errors. Do not move on with a red design.
Checkpoint: show the user the design (or a plain-English summary of it) before scaffolding for real.
JERRYCAN_FRAMEWORK_DEP='jerrycan = { ... }' # only when testing against a local framework checkout
jerrycan new <app-dir> --design design.json
cd <app-dir>
for m in <each top-level module>; do jerrycan gen-tests --module "$m"; done
This emits, per module: model.rs, repo.rs (TOOL-owned — regenerated, don't edit),
handlers.rs/deps.rs (AGENT-owned — edit freely), migrations, and
tests/acceptance.rs (TOOL-owned, currently failing — green is the goal). Run
jerrycan check to see the red baseline.
jerrycan check to greenFor each module, read the relevant doc page, then implement every handler stub:
jerrycan docs database (+ tenancy for scoped *_for accessors).jerrycan docs auth, jerrycan docs auth-advanced (OAuth, API keys,
token-at-rest).jerrycan docs extractors (Path/Query/Json/Headers/
RawBody/Multipart).jerrycan docs response-types (Json/Created/NoContent/
Redirect/(StatusCode, body)/streaming).jerrycan docs validation. Jobs: jerrycan docs jobs. Errors:
jerrycan docs error-codes / jerrycan explain JCxxxx.Apply the gotchas (below). Loop jerrycan --json check toward ok: true
(build + clippy + tests + lints + audit/deny + schema). The JL0006 lint will catch
cross-tenant leaks — fix by using the scoped repo accessors, don't suppress it.
Expect a few un-greenable generated tests — this is not your bug, and ok:true
may not be fully reachable. The generator emits one happy-path probe per
endpoint that posts a minimal body with no credential/signature/API key. For
an endpoint whose success requires one (a login that 401s bad creds; a signed
webhook that 401/400s a bad signature; an API-key-gated route), that 2xx probe
cannot pass — the handler correctly rejects it. Likewise the 404-missing-id
probe is sent as GET even to a POST-only /{id} action, which the framework
correctly answers 405. Do NOT weaken the handler to make these pass. Leave
the probe, and prove the REAL behavior (success WITH a valid credential, and the
4xx without) in an agent-owned test file you write. Get every test you CAN
green green, then tell the user exactly which generated probes are un-satisfiable
and why. (Two further known generator rough edges: a unique non-PK field on the
tenant entity collides in the two-tenant isolation seed — make it a plain index
instead; and a single-value enum makes its "wrong value → 4xx" branch
unreachable.)
tenant_a_cannot_read_tenant_b_*) must
pass — that's the cross-tenant isolation guarantee.JERRYCAN_ADDR=127.0.0.1:<port> ... cargo run -p app, with JERRYCAN_SECRET
and a DB URL set) and drive register/login, a tenant-scoped create+read, any
webhook/OAuth/api-key flow with real signatures/credentials.// AGENT TODOs — e.g.
webhook bad-signature → 400, scope → 403), write your own tests in an
agent-owned test file.Tell the user what was built: the endpoints, how to run it (jerrycan dev,
env vars), how to test (jerrycan test), and how to package (jerrycan package).
To ship it: jerrycan deploy render generates deploy/render/deploy.sh; run it
with RENDER_API_KEY for a live, secure URL (see jerrycan docs packaging).
Get feedback and iterate from the relevant phase. To change the data model or
endpoints, edit design.json and regenerate (tool-owned files refresh;
agent-owned handlers are untouched — re-implement only new stubs).
string + values: [...] — there is no enum field type. It
becomes a TEXT column with a CHECK constraint and a Valid<T>; the Rust field
is String. Server-assign privileged enum/role fields on create (e.g.
registration sets role = "user", never trusts the client) — generated success
probes post the first declared value, but untrusted input must be defended.datetime/uuid are String at the Rust layer (no native time/uuid type,
no built-in now()→rfc3339). Format/compare time yourself in handlers.required_roles ≠ membership roles. required_roles is the auth.roles
namespace. To gate on a tenancy membership role (e.g. workspace "owner"),
call tenant.require_role("owner")? IN the handler.belongs_to gets no scoped accessors. A grandchild
(Application → Posting → Org) isn't auto tenant-scoped; scope it by joining
through the parent in your handler/repo.lowercase(entity) + "s" (Ticket → tickets, ApiKey →
apikeys) — NOT snake_case, so it differs from the fk column (api_key_id).
You need the exact table name only for hand-written cross-module SQL.public endpoints can't live in a module that owns a tenant-owned entity
(the generator binds the endpoint to that entity → guard bypass). Put webhooks /
login / inbound-ingest routes in their OWN module (entity-less is fine).request_body is entity-only. A public endpoint receives Json<Entity>
(all fields), so the untrusted client can send server-controlled fields — force
them in-handler (take the id from the path, fix the status, etc.).success.status only; the stub is
Result<Json<serde_json::Value>> you hand-write.Headers + RawBody
yourself and verify the HMAC (jerrycan docs auth).Db::from_env() defaults to sqlite::memory: when JERRYCAN_DATABASE_URL
is unset (dev/test). Set a real URL for persistence.oauth dependency in the design (auto-wires the feature);
the in-process MockIdp (for hermetic tests) additionally needs the mock-idp
facade feature.jerrycan check is red and you're tempted to delete/skip a generated assertion
→ don't; fix the handler or the design, or report a genuine framework bug.jerrycan docs --search <term>
/ jerrycan explain <code>; if it's genuinely undocumented, say so rather than
guessing an API.