Architecture
Tech stack
| Layer | Choice |
|---|---|
| Runtime | Node.js >= 22 (ESM modules throughout) |
| Language | TypeScript 5.x, strict mode, verbatimModuleSyntax |
| Package manager | pnpm 9.x workspaces |
| Backend | Hono + OpenAPI (@hono/zod-openapi) |
| Frontend | Next.js 16 App Router + React 19 |
| Database | PostgreSQL via Drizzle ORM |
| Auth | Supabase Auth (JWT + service accounts) |
| Testing | Vitest + Playwright |
| Linting/Formatting | ESLint 9 + typescript-eslint + Prettier |
Project structure
client-os/
apps/
api/ Hono OpenAPIHono -- /health, /api/me, auth+rate-limit middleware, org-scoped DB
web/ Next.js App Router -- login + authed shell + stub landing/dashboard
cli/ Commander `client` -- health command stub + SEA/npm build pipelines
desktop/ Tauri 2.x shell -- placeholder frontend
e2e/ Playwright -- health + auth smoke tests
packages/
auth/ JWT + PAT verify
auth-store/ Zustand auth state
config/ env preload
core/ shared types/enums
db/ Drizzle client + identity migrations
environment/ env var schema
eslint-config/ shared lint
fetch-client/ HTTP client wrapper
react-query/ TanStack Query provider
object-store/ S3/Supabase Storage wrapper
telemetry/ event schemas + redaction
api-client/ kubb-generated types/hooks
Each package exposes "." via its exports field with types + default,
and inter-package references use workspace:*. Every package builds via its
own tsc --build; the root pnpm typecheck builds the whole project graph.
Request flow
apps/webcalls the API through@client/fetch-clientand@client/react-query, using types generated by kubb intopackages/api-clientfrom the API's own OpenAPI spec — the browser/RSC layer never queries Supabase directly.apps/api(Hono + OpenAPIHono) validates the request against its Zod schemas, verifies the caller's Supabase-issued JWT, and resolves org membership/role via a DB lookup.- Data access goes through Drizzle ORM against Postgres.
The clientos CRM domain
client-os ships a working, non-trivial domain — a personal consulting CRM — to demonstrate the full pattern end to end, not just a todo-list demo.
- Schema: Postgres schema
clientos, 39 tables (clients, contacts, projects, tasks, promises, invoices, companies, deployments, servers, domains, contracts, retainers, time_entries, and more) — seepackages/db/src/schema-clientos.ts. There's also an emptyresumeschema (schema-resume.ts) reserved for a separate, not-yet-built migration. - Org scoping: every table carries
org_id. Unlike a typical Supabase app, this is not enforced by Postgres Row-Level Security — the connection model this repo uses doesn't support RLS policies keyed off the caller's JWT. Instead, org scoping is enforced entirely at the application layer: every route inapps/api/src/routes/clientos/*resolves the caller's org memberships (callerOrgIds()) and filters queries against that list before returning or mutating data. If you add a newclientostable, the org-scoping check belongs in your route handler, not in a database policy. - API:
apps/api/src/routes/clientos/*— 14 route files covering ~37 resources, mounted under/api/clientos/*. Role gating follows a three-tier model:org:viewer+ can read,org:contributor+ can write,org:admincan delete. - Web:
apps/web/src/app/dashboard/{clients,projects,money,tech,timeline}— CRM pages living under the existing dashboard shell/nav, calling the API above through@client/fetch-client/@client/react-query. - CLI:
apps/cli/src/commands/clientos/*— 16 commands (invoked asclient clientos <cmd>) that talk directly to Supabase using the service-role key. This is a deliberate exception to the "always go through the API" rule above: the CLI is a trusted local operator tool, not a client-facing surface, so it doesn't need the API's auth/org-scoping layer in front of it.
Data note: only a demo seed (acme-corp) ships with the repo. Real CRM
data entered before this scaffold's migration was lost along with an
ephemeral local Postgres instance — expected, not a bug you'll hit in your
own usage.