Slice development pattern
Each slice is built end-to-end before the next slice begins. This page is the recipe for "how do I build a slice."
The seven steps
- Spec — what does the slice deliver, end to end?
- Schema — migration files for any new tables/columns
- Bible — entity pages, lifecycle (if applicable), slice page
- Backend — endpoints, helpers, audit-wiring
- Frontend — screens, in-situ editing, server-rendered fallbacks
- Tests — unit + integration; the cron path if applicable
- Migration — AIM ETL update for inbound data
Then deploy, smoke test, ship.
Step 1: Spec
In the slice page (docs/domain/slices/slice-N-{name}.md), write:
- Scope: what's in / what's out
- The endpoints (HTTP routes)
- The UI (which screens, what changes)
- Acceptance criteria
This forces clarity before writing code. ~30-60 minutes of writing saves 1-2 hours of "I don't actually know what I'm building" later.
Step 2: Schema
Migration file: migrations/0NN-{slice-name}.sql. Include:
- All new tables for the slice
- Indexes (FK indexes mandatory; covering indexes where queries justify)
- Any FK additions to existing tables
- Seed rows for stable enumerations
# Verify locally
wrangler d1 migrations apply helm-dev --local
# Inspect
wrangler d1 execute helm-dev --local --command "SELECT * FROM {new_table} LIMIT 3;"
Step 3: Bible
Update or create:
docs/domain/entities/{entity}.md— for each new entity in the slicedocs/domain/lifecycles/{lifecycle}.mdx— if a new state machinedocs/domain/slices/slice-N-{name}.md— the slice page
Write these before the code. They double as the design doc.
Step 4: Backend
In src/index.js:
- Add the handler functions (named for the endpoint)
- Add the switch entries
In src/lib/:
- Helpers shared across handlers
- Adapters for new external services
Every mutation goes through withAudit. Every external write uses an idempotency key. Every endpoint emits a structured log line.
async function apiTicketsCreate(req, env, ctx) {
const body = await req.json();
validateTicketCreate(body);
const ticket = await db.run(`INSERT INTO service_tickets ...`).bind(...);
await withAudit({ table: 'service_tickets', row_id: ticket.id, action: 'ticket.created', after: ticket, ctx });
return json(ticket);
}
Step 5: Frontend
In public/index.html (or a new partial):
- The screen markup
- Server-rendered fallback form for progressive enhancement
In public/js/{slice}.js:
- The screen's enhancer module
- In-situ editing handlers (if applicable, per in-situ editing)
Test in the browser at localhost:8787. Click through the golden path. Try the error paths. Check the audit log in D1 after each mutation.
Step 6: Tests
In test/{slice}.test.js:
- Unit tests for the helpers
- Integration tests for the endpoints (via
wrangler dev's in-process testing, or Vitest with@cloudflare/vitest-pool-workers) - Smoke tests for the cron paths
Run: npm test.
Step 7: Migration (if applicable)
If the slice has inbound AIM data, update migrate_aim.py:
- Add a
slice_N_*function - Add the
--slice NCLI flag - Update
migrate_aim.py --allto include it
Test against a fresh local D1:
wrangler d1 execute helm-dev --local --command "DROP TABLE IF EXISTS {tables};"
wrangler d1 migrations apply helm-dev --local
python migrate_aim.py --slice N --target=local
python verify_schema.py --slice N
Smoke and ship
After 1-7:
wrangler dev --env=development --localand click through the slice- Run the audit chain verification (planned daily cron, but trigger manually for this PR)
npm run build && npm testclean- Commit on a feature branch; PR; review; merge
- Deploy (deploy to production)
When a slice takes longer than a week
Stop. Reassess. Either:
- The slice is too big — split it
- The spec was wrong — fix the spec first
- The pattern is fighting you — flag a tech-debt note and proceed with the right amount of mess
Don't let a slice drift for two weeks. Better to ship a smaller slice and start the next one.