Skip to main content

Getting started

This guide takes you from a clean clone to a running API + frontend with migrations applied and the test suite green. It assumes a UNIX-style shell; the commands are identical on Windows under Git Bash or PowerShell.

Prerequisites

  • Node.js ≥ 22 — the workspace root pins "engines": { "node": ">=22" } in package.json (repo root).
  • npm — the repo is an npm workspaces monorepo (no pnpm/yarn). npm ci is what CI runs; use it when you want a lock-faithful install.
  • A Postgres / Neon connection string — the platform targets Neon Postgres, but any reachable Postgres works locally. You need a DATABASE_URL with privileges to create roles and schemas (migrations create the app_tenant / app_admin roles — see Multi-tenancy & RLS).

You do not need Cloudflare credentials, Hyperdrive, R2, or Queues to run locally — the Worker falls back to a direct DATABASE_URL, and the queue/object-store adapters no-op or stub when their bindings are absent.

Monorepo layout

The repo is one npm workspace tree (packages/*, api, db, frontend):

dts-voc/
├── api/ # Cloudflare Worker (Hono): routes, middleware, services, scheduled jobs
│ ├── src/ # app.ts, composition-root.ts, env.ts, index.ts, routes/, middleware/
│ └── test/ # Vitest service/route tests (run against the in-memory fake DB)
├── frontend/ # Vite + React + TS + Tailwind admin SPA + public survey runtime
├── db/ # Migrations + scripts (no compiled output)
│ ├── migrations/ # 0001_init.sql … 0118_make_deletion_records_append_only.sql
│ ├── scripts/ # migrate/rehearse/verify-rls + demo seed/generator tooling
│ └── src/ # Drizzle schema + client.ts (pinTenant / withTenant)
└── packages/
├── core/ # Pure domain types + logic (domain.ts, permissions.ts, errors.ts)
├── contracts/ # Zod request/response contracts shared by api + frontend
├── ports/ # Port interfaces (Database, Email, Sms, Voice, Ai, Identity, Queue, …)
└── adapters/ # Real adapters (Neon, Resend, Twilio, Gemini, R2, cf-queues) + stubs

See Monorepo packages for each package's responsibility and the dependency direction.

Clone & install

git clone <origin> dts-voc
cd dts-voc
npm install # installs every workspace

Configure secrets — api/.dev.vars

The Worker reads local secrets from api/.dev.vars (Wrangler's local secret file). Never commit it — it is git-ignored, and the canonical list of every variable and where it lives (local .dev.vars ↔ Wrangler secret) is in SECRETS.md at the repo root. The shape the Worker expects is declared in api/src/env.ts:

# api/.dev.vars — local only, do not commit. Values from SECRETS.md.
DATABASE_URL=postgres://… # local fallback when HYPERDRIVE is not bound
RESEND_API_KEY=…
EMAIL_FROM=…
GEMINI_API_KEY=…
GEMINI_MODEL=gemini-3.1-flash-lite
CLERK_SECRET_KEY=sk_… # Clerk secret key — verifies session JWTs (fetches the instance JWKS)
# CLERK_JWT_KEY=… # optional PEM public key for fully networkless verification
# CLERK_AUTHORIZED_PARTIES=… # optional comma-separated allowed azp (SPA) origins

Optional providers (Twilio SMS/IVR, Sentry) light up only when their variables are present; absent, the composition root wires a stub that fails loudly rather than sending.

Apply migrations

Migrations live in db/migrations and are applied by the runner in the db workspace. The runner reads DATABASE_URL from the environment (or api/.dev.vars) and never echoes it:

npm run db:migrate:rehearse --workspace db # validate every pending migration, then roll back
npm run db:migrate --workspace db # apply pending migrations, tracked in schema_migrations
npm run db:verify-rls --workspace db # prove tenant isolation (default-deny + FORCE)
npm run db:seed-demo --workspace db # optional: Black Cat Bank demo org + surveys + responses
npm run db:seed-demo-full --workspace db # optional: ^ + badges + DEEP data for every feature/screen

Rehearse first and do not apply if it reports a SQL, ordering, catalogue-assertion, or live-data preflight failure. db:migrate is idempotent — already-applied files (recorded in public.schema_migrations) are skipped. Run the live RLS verifier after apply. See Migrations for how the runner works and how to add one.

Run the API and frontend

# Worker API (Hono) — Wrangler dev server, default http://127.0.0.1:8787
cd api && npx wrangler dev

# Admin SPA — Vite dev server, default http://localhost:5173
cd frontend && npm run dev

The API's CORS layer automatically allows any localhost / 127.0.0.1 origin, so vite dev (5173) and vite preview (4173) work without extra config. A quick liveness check:

curl http://127.0.0.1:8787/health # { ok: true, service: "voc-api", … }
curl http://127.0.0.1:8787/health/ready # { ok: true, db: "up" } — confirms Postgres reachable

Run tests

Service and route tests run against an in-memory fake of the Database port (api/test/helpers/fake-database.ts), so the suite needs no live Postgres:

npm test # vitest run across api/ + packages/ + frontend/ (root vitest.config.ts)
npm run typecheck # tsc --noEmit across all workspaces
npm run lint # eslint (incl. the withTenant boundary rule)

Integration tests that need real Neon read DATABASE_URL and skip themselves otherwise. Before committing, the project's green gate is: typecheck + lint + build + tests, with migrations applied and db:verify-rls passing (see AGENTS.md).