Code Conventions
Language & runtime
- TypeScript everywhere (frontend, backend, tools, scripts). No raw JS in app code.
- ESM modules —
"type": "module" in package.json. Use import, never require.
- Node.js 22+ is the target runtime. Use modern syntax (top-level await,
Array.prototype.flatMap, structuredClone, etc.).
tsx for executing TS in dev — no compile step. Production runs the same way (tsx watch for API/WS, Next.js for web).
Project layout
- One Hono route per file under
services/api/src/routes/. Mounted from routes.ts.
- One query module per topic under
packages/db/src/queries/ — e.g. projects.ts, chat.ts. Re-exported from index.ts.
- Shared utilities that >1 service needs go in
@doable/shared. Don't import across services/* boundaries.
- AI provider plugins live in
services/api/src/ai/providers/.
- Integrations live in
services/api/src/integrations/registry/<category>.ts.
Networking
- Bind to
127.0.0.1 in every service. Never 0.0.0.0 or a public IP.
- Inter-service URLs use
127.0.0.1 not localhost (avoids IPv6 surprises).
- Public exposure only via Cloudflare Tunnel or a documented reverse-proxy on a separate host.
Database
- Tagged-template SQL:
await sql\SELECT * FROM users WHERE id = ${userId}``. Never string-concatenate values into queries.
- No ORM. The
postgres-js library handles parameterization.
- All schema changes go through
packages/db/migrations/NNN_description.sql — see Migrations.
- Indexes: add them when you add a query that filters/sorts on a non-PK column at scale. Document the rationale in the migration's comments.
- Soft delete with
deleted_at TIMESTAMPTZ. Reads filter WHERE deleted_at IS NULL.
- UUIDs for primary keys, generated by
gen_random_uuid().
API style
- REST for CRUD, RPC-shaped POST endpoints for actions (
/projects/:id/publish, /projects/:id/duplicate).
- Errors: respond with
{ error: { code, message } } and an HTTP 4xx/5xx.
- Auth: middleware reads JWT from
Authorization: Bearer <token> (or session cookie for web). Routes assume c.get('userId') is present.
- Rate-limit any endpoint that hits an LLM, sends email, or writes to the filesystem.
Streaming
- Server-Sent Events for chat (
text/event-stream) — see services/api/src/routes/chat.ts.
- WebSockets only for collaboration (Yjs doc sync) and live presence — handled by
services/ws.
Frontend
- React Server Components by default; opt into
'use client' only when needed.
- State: prefer
useState + URL params + server actions; only use a store (zustand) for editor-wide ephemeral state.
- Styling: Tailwind 4 utilities. Reach for a CSS file only for animations or third-party overrides.
- Components: shadcn/ui base layer + project-specific composites in
apps/web/components/.
- Forms: react-hook-form + zod schemas reused with the API.
- Prettier formats all TS/TSX/JSON/MD.
pnpm format runs it everywhere.
- ESLint with the Next.js + TypeScript presets.
pnpm lint runs it. Warnings should be fixed; blocking errors break CI.
- Imports: absolute paths inside a workspace (
@doable/shared/...), relative paths inside the same package.
- File names:
kebab-case.ts for non-component files, PascalCase.tsx for React components.
Testing
- Vitest in every package.
pnpm test runs everything in parallel.
- Co-locate tests as
foo.test.ts next to foo.ts.
- Mock at the boundary (DB, HTTP, filesystem). Avoid mocking your own internal modules.
- For DB tests, prefer a throwaway Postgres container over mocking.
Logging
console.log is fine for top-of-bracket scripts; for API/WS, use the project logger so log lines have request context.
- Never log secrets, tokens, or full chat content. Log structured fields, not free-form prose.
Security & PII
- Treat any string from a user — chat, file content, project name — as untrusted. Escape on output, validate on input.
- Sanitize HTML on the rendering side (
sanitize-html).
- Encrypt sensitive at rest with
ENCRYPTION_KEY (oauth tokens, BYO API keys). Use the helpers in @doable/shared.
Don't
- Don't add
0.0.0.0 binding for "ease of testing".
- Don't introduce a new state-management library without discussion.
- Don't add a build step to a service that doesn't need one.
- Don't run shell commands from the API process without going through
dovault.
- Don't commit
node_modules, .env, *.pem, or anything in services/api/projects/.