Code style
The conventions used across src/, public/, migrations/, and scripts/. Most are Prettier defaults plus a handful of opinions.
TL;DR
- Prettier defaults (2-space, semi, double quotes via the Prettier config in the repo)
- ESLint with sensible defaults; lint passes before merge
- Vanilla JavaScript; TypeScript types via JSDoc when worth it
- File names:
kebab-case.js - Function names:
camelCase - Constants:
SCREAMING_SNAKEonly for module-level true constants - Avoid framework abstractions; the Worker is plain JS
File layout
src/
index.js # Worker entry — the switch-statement router
lib/ # extracted helpers
audit.js # withAudit() helper
auth.js # session resolution, PIN hashing
stripe.js # Stripe adapter
twilio.js # Twilio adapter
claude.js # Claude adapter
tax-bc.js # BC tax engine
instrumentation.js # request logging wrapper
templates/ # server-rendered HTML helpers (when needed)
public/
index.html # the operator app (large, single file)
js/ # per-screen modules
customers.js
service.js
...
css/
app.css
migrations/
000-init.sql
001-customers.sql
...
service_categories_seed.sql
scripts/
generate-schema-docs.ts
onboard-shop.sh
Routing pattern in src/index.js
// One handler function per endpoint, named for the URL
async function apiCustomersList(req, env, ctx) { ... }
async function apiCustomersCreate(req, env, ctx) { ... }
// The switch sits at the top of fetch()
export default {
async fetch(req, env, ctx) {
return withInstrumentation(handle, req, env, ctx);
}
};
async function handle(req, env, ctx) {
const url = new URL(req.url);
const path = url.pathname;
const method = req.method;
if (path === '/api/customers' && method === 'GET') return apiCustomersList(req, env, ctx);
if (path === '/api/customers' && method === 'POST') return apiCustomersCreate(req, env, ctx);
// ...
return notFound();
}
Sequential. Predictable. Greppable. See ADR-0004.
Endpoint handler shape
async function apiCustomersDetail(req, env, ctx) {
const id = pathParam(req, '/api/customers/');
const customer = await env.DB.prepare(
'SELECT * FROM customers WHERE id = ?'
).bind(id).first();
if (!customer) return notFound();
return json(customer);
}
Short. Each handler tells its story top-to-bottom. Helpers (pathParam, json, notFound) live alongside.
SQL
- Plain SQLite-compatible SQL; no D1-specific extensions
- Always use parameterized queries (
?) - Multi-line queries fine; format with leading whitespace for readability
- Comment why an index exists if it's non-obvious
const rows = await env.DB.prepare(`
SELECT t.id, t.ticket_number, c.first_name, c.last_name
FROM service_tickets t
JOIN customers c ON c.id = t.customer_id
WHERE t.status = ?
ORDER BY t.dropped_off_at DESC
LIMIT 100
`).bind('in_progress').all();
Comments
Default: don't write comments. Naming should carry the meaning. Comment when:
- Non-obvious why (a workaround, an invariant, a specific bug being avoided)
- A regulatory requirement (BC tax rule reference)
- A reference to an external doc or ADR
Don't comment what the code does. The code says what.
Error handling
throw new Error(message)for unexpectedthrow new BlockedByDependents({blocker: ...})for known refusals- Top-level
withInstrumentationcatches and logs; returns a 500 with request_id - Endpoint code rarely tries to recover from errors — fail loudly per fail-quietly-recover-loudly
Logging
// At end of request, automatic via instrumentation
// Within a handler, only when there's something non-routine to record:
console.log(JSON.stringify({ event: 'sms_skipped', reason: 'opt_out', customer_id }));
Never log PII. Never log plain SQL with values (parameters yes, values no). Never console.log for debug in production (use temporary logs gated by env flag and remove before merge).
Frontend (operator app)
Vanilla JS modules, one per screen. Pattern:
// public/js/customers.js
export function init() {
document.querySelectorAll('[data-customer-row]').forEach(row => {
row.addEventListener('click', onRowClick);
});
}
async function onRowClick(e) {
const id = e.currentTarget.dataset.customerId;
const detail = await fetch(`/api/customers/${id}`).then(r => r.json());
renderDetail(detail);
}
Use fetch(), DOM APIs, no library. Templates can be tagged template literals (html\...``) or just string concat.
Migrations
One migration per slice, named 001-customers.sql, 002-service-tickets.sql. Each migration:
- Starts with a header comment: filename + date + slice number
- Has all
CREATE TABLEs for that slice - Has indexes
- Has any seed data (for stable enumerations like screens, default roles)
Migrations are forward-only. No DROP in a migration; if you need to undo, write a new migration that does the inverse.