1 What kind of change is it? · the key habit
Type your change — get instant guidance
Before writing code, figure out if your change is about how the app looks (frontend) or how it works (backend). Type it below:
Run this for the real, AI-powered answer (and the exact next steps):
npx tsx scripts/triage.ts "describe your change"The command always works in your terminal too:
npx tsx scripts/triage.ts "make the contact status show on mobile"- Frontend (how it looks) → just edit the screen. No spec. Go to step 2.
- Backend / Both (data, rules, saving) → write a short spec first (step 3), then build.
2 Build it the right way
Five rules we always follow — they keep the app safe and consistent:
orgId.useAccess().can(...); the server re-checks.docs/BUILDING.md.Before building any UI, browse the Component Library. It shows every shared component rendered on the page — search/filter them, interact, and copy the code. Reuse one instead of making a new version.
3 Write a spec first · only for Backend / Both
Spec → TypeScript → finished, in one loop. The spec.yaml is the source of truth; the in-repo generator turns it into typed code; make spec builds & verifies it.
idea → triage → spec-new / spec-draft → author spec.yaml → spec-gen → fill <spec:impl> + tests → make spec → PR
(scaffold) (your contract) (gen TS) (your logic) (build+test+drift)
Backend / Both → use the generator below. The repo's own make spec-gen IS the generator (you can author the files in Kiro, a spec-driven IDE, but that's optional). Frontend → your normal editor (VS Code / Cursor) + Claude Code — no spec.
1 · Scaffold the folder — from a plain-English idea (AI drafts spec.yaml), or blank:
make spec-draft FEATURE=warranties DESC="warranties with an expiry date"make spec-new FEATURE=warrantiesEither creates specs/warranties/ with the four files below. (No Docker? use npm run spec:draft / npm run spec:new.)
2 · Author them — fill spec.yaml (the contract the generator reads) + the prose. Templates:
spec.yaml — the machine-readable contract (the generator reads this)
# Machine-readable contract for this feature — the generator reads THIS file.
# Run `npm run spec:gen` after editing.
feature: __FEATURE__
module: operations # sales | operations | finance | hr | timesheets | core | agent | integrations
entities:
- name: __FEATURE__ # ERP resource key — camelCase plural, unique
label: __FEATURE__ # human label (sidebar / bulk import)
typed: false # false = schemaless JSONB (no migration); true = a Drizzle table
importable: true # adds it to bulk import
fields:
# type: text | number | boolean | date | enum(values) | ref(resource) | json | string[]
- { name: name, type: text, required: true }
# - { name: amount, type: number }
# - { name: status, type: enum, values: [open, closed], default: open }
# actions: # optional → governed MCP tools (agent ⊆ user); logic goes in <spec:impl>
# - { name: do_something, permission: operations.edit, undoable: true, args: { id: number } }
# screens: # optional → sidebar-discoverable screen scaffolds
# - { id: __FEATURE__, label: __FEATURE__, module: operations }requirements.md — what it must do (numbered, testable)
# __FEATURE__ — Requirements
WHAT & WHY in 1–2 sentences. Then numbered, **testable** acceptance criteria.
- R1. <a record/field/rule, stated so it can be asserted in a test>
- R2. <who can do what — maps to a permission, e.g. `operations.edit`>
- R3. <a derived rule — implement by hand in <spec:impl>, cover with a unit test>design.md — the data, the API, and where each rule lives
# __FEATURE__ — Design
The shape lives in `spec.yaml` (the generator reads it). This file is the human narrative:
the data model, the API contract, and — crucially — **where each non-trivial rule lives**.
## Data model
See `spec.yaml`. (Schemaless ERP resource unless an entity is `typed: true`.)
## API
Governed generic CRUD via `/api/erp/__FEATURE__` (GET→view, POST/PATCH→edit, DELETE→delete).
Add a dedicated route only for behavior CRUD can't express.
## Rules → location
| Rule (from requirements.md) | Where it's implemented |
|---|---|
| R3 (example derived rule) | hand-written in the `<spec:impl>` region of the service/tool stub |
## UI hints
<list / form fields / status chips, etc.>tasks.md — your build checklist
# __FEATURE__ — Tasks
- [ ] Fill in `spec.yaml` (entities, fields, module, actions, screens).
- [ ] `npm run spec:gen` — generate the contract (registry, types, permissions, scaffolds).
- [ ] Implement business logic in the `<spec:impl>` regions of any generated tool/service stub.
- [ ] Write unit tests mapping each acceptance criterion (`*.test.ts`); `npm test` green.
- [ ] If any entity is `typed: true`: `npx tsx scripts/pg/migrate.ts` to apply the migration.
- [ ] `npm run spec:check` + `npm run build` green; open the PR (spec + diff).3 · Generate the TypeScript, then 4 · finish it out:
make spec-genmake specspec-gen writes the registration regions + a screen scaffold; make spec regenerates, type-checks, builds, tests & runs the drift gate — all in the dev container.
Generated vs yours. The generator owns the // <generated:spec> regions; you own spec.yaml, the <spec:impl> logic, and the tests. Never hand-edit a generated region — CI's spec:check drift gate will fail. After generating, spec-gen prints the few manual hookups it can't place for you (the sidebar entry, the Artie tool, a typed-table migration).
See a real, finished example: specs/org-chart.
4 Where do I paste the code?
Each kind of change has a home. Put your code here:
| What you're adding | Where it goes |
|---|---|
| A new screen / page | components/screens/YourScreen.tsx + a route at app/dashboard/your-screen/page.tsx, then wire it into components/AppSidebar.tsx + lib/features.ts |
| A reusable UI piece | components/ (shared) or components/ui/ (shadcn primitives) |
| Business logic / a calculator / a rule | libs/ (pure functions — put a *.test.ts right next to it) |
| A new kind of record (data type) | register it in libs/erp/resources.ts + libs/erp/types.ts + libs/access.ts — CRUD, API & Artie access come for free (usually no migration) |
| A custom API endpoint | app/api/.../route.ts — wrap it with withRoute(...) and check can(...) |
| Let Artie do an action | add a tool in libs/artie/tools.ts |
| Tests | next to the file, named thing.test.ts |
| Your spec | specs/your-feature/ |
Not sure which file? Re-run the triage command from step 1 — its full output names the exact files and the workflow.
5 Test & check
Before you share, both of these must pass (green):
npm testnpm run buildAdded a calculation or rule in libs/? Add a quick *.test.ts next to it.
Need the real app (backend/AI)? Make a .env.local with DATABASE_URL, BETTER_AUTH_URL, BETTER_AUTH_SECRET, and ALLOW_DEV_NO_AUTH=true. Full list in README.md §7.
6 Ship it
- Make a branch — never work directly on
main. - Open a Pull Request on GitHub.
- Name your commit like
feat: add warranty trackingorfix: mobile button overflow— starting withfeat:/fix:is what adds it to the release notes.
Merged PRs go to staging automatically; a maintainer cuts a release to send it to production.
Where to go next
These open in the app's built-in doc viewer — rendered straight from the repo's Markdown files at build time, so they're never a stale copy (run the app first):
| Browse every shared component (live "Storybook") | localhost:4000/dev/components — run the app first |
| Understand the big picture | README |
| Build a feature / tool / integration | BUILDING.md |
| Learn the spec process | SESSION_SUMMARY.md |
| Understand permissions | ACCESS.md |
| The full contributor map | CONTRIBUTING.md |
Tip: each doc page links across to the others and to the component library.