Skip to content

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.

Formatting & linting

  • 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/.