Architecture
Ce contenu n’est pas encore disponible dans votre langue.
Kept is one Node.js image and one Postgres database. The same image serves the web app and the
API, runs the background jobs, and carries the kept command line. What a process does is set by
KEPT_ROLE. Everything a person owns lives in Postgres, except file bytes, which live on the
server’s disk or in an S3 bucket.
The design this page summarises is the engineering spec’s §7 Foundations (engineering spec). Where this page and the code disagree, the code is right; please open an issue.
The processes and what they talk to
Section titled “The processes and what they talk to”| Role | What runs | Started by |
|---|---|---|
web |
Fastify: the API under /api/v1, MCP at /mcp, the built web bundle, /healthz, /readyz, /version (and /metrics when KEPT_METRICS_TOKEN is set) |
node dist/main.js with KEPT_ROLE=web |
worker |
pg-boss’s supervisor, cron and every job handler; no HTTP | KEPT_ROLE=worker |
all (the default) |
both, in one process | KEPT_ROLE=all |
kept migrate |
the migrations, the pg-boss schema and the reference rows, under an advisory lock | the migrate service in Compose, a hook Job in the Helm chart |
A web-only process still starts pg-boss, without its supervisor or schedule, because a request
enqueues jobs inside its own transaction. A worker-only process serves no /readyz; it writes a
heartbeat file the image’s health check reads
(jobs/liveness.ts).
The startup order is in
apps/server/src/main.ts:
the release guard, pg-boss, file storage, the AI layer, then either the job registry, the HTTP
app, or both.
One request, start to finish
Section titled “One request, start to finish”- Hooks (
http/app.ts). Fastify gives the request a fresh UUIDv7 as its id and answers it inx-request-id. Then, in order: a cookie-bearing write from another site is refused (csrfHook), aAuthorization: Bearer kpt_…token becomes the scope (bearerHook), and otherwise the Better Auth session does (sessionHook). A route isrequiredunless it saysoptionalornone, so an anonymous call to a protected route stops here with 401. See authentication. - Validation. zod schemas on params, query and body (
fastify-type-provider-zod), then the module gate: a route withconfig.moduleanswers 404module_offfor a read and 409 for a write when that module is off in the target location (http/modules.ts). - The scope.
scopedRead()andscopedWrite()(http/write.ts) run the handler insidewithScope()(db/scope.ts): one transaction on akept_appconnection that first setsapp.user_id,app.mfaandapp.token_idtransaction-locally. Row-level security reads them. See row-level security. - The handler queries through Drizzle. It sees only what the policies let that person see; anything else is a 404, the same as a missing row.
- Audit. Every write calls
audited(tx, event)(audit/audited.ts) in the same transaction, and the route catalogue test fails any non-GET route that can finish without an audit row. - Commit and reply. An undoable write answers
x-kept-audit-event. Work that needs another login or its own transaction runs inafterCommit, and its failure is logged, not answered. Any error becomes the one error shape (see API conventions).
The components
Section titled “The components”Web app (apps/web). React 19,
TanStack Router and Query, React Aria, Lingui, Tailwind v4, built by Vite into static files the
server serves with a strict CSP (default-src 'self', no inline scripts but the pre-paint one by
hash). A Serwist service worker precaches the shell, the fonts and the scanner’s wasm, and never
caches /api or /f. The phone keeps a read-only snapshot and a queue of offline changes in
IndexedDB: offline and sync.
Server (apps/server/src).
Fastify 5 with zod, Drizzle over pg, Better Auth for sign-in. One directory per feature; the
monorepo page lists them.
Postgres 18 with pgvector (spec §7.1, §7.2). Four login roles, one pool each for the three
runtime ones, row-level security forced on every table. pg-boss keeps its queue in the same
database, in schema pgboss; Better Auth keeps its tables in schema auth; Kept’s functions live
in schema kept. The vector, pg_trgm and unaccent extensions are created at database
bootstrap (docker/initdb).
Jobs (apps/server/src/jobs).
Every job is declared with defineJob() as system (cross-tenant, on kept_system, the only
kind that may be scheduled) or tenant (re-assumes the sending request’s user inside
withScope()). A request enqueues with sendInTx() on its own connection, so a job exists only
if the write commits. Retry and timeout policies are in jobs/policies.ts.
File storage (apps/server/src/storage).
One blob-store interface, two drivers: local.ts under KEPT_DATA_DIR and s3.ts
(KEPT_STORAGE=s3). Originals stay byte-identical; display and thumbnail derivatives are made in
the upload request. Every file is fetched through a five-minute signed URL: /f/<token> on local
storage, a presigned URL on S3 (spec §7.11). See file storage.
Mail (apps/server/src/mail).
Nodemailer over KEPT_SMTP_URL. Without it, mail is logged as due and reminders reach people in
the app only.
AI (apps/server/src/ai).
The Vercel AI SDK behind one call door with budgets, caps and a call ledger (llm_calls). Provider
kinds: openai, anthropic, google, openai_compatible, openrouter, groq. Outbound
requests go through the SSRF guard in net/ssrf.ts. KEPT_AI_MOCK=1 answers every call from
ai/mock.ts and is refused in production. See AI providers.
Backups (apps/server/src/backup).
The worker runs restic nightly (KEPT_BACKUP_TIME, default 02:30 UTC), dumping as kept_owner
on its own connection: the one time a long-running process uses the owner login. See
backups.
MCP (apps/server/src/mcp,
packages/mcp). POST /mcp
takes a personal token or an OAuth access token bound to the /mcp resource. Tools run through
the same runTool() the in-app assistant uses (tools/). See MCP.
Observability. Pino logs (KEPT_LOG_FORMAT), Prometheus-format gauges at /metrics (served
only when KEPT_METRICS_TOKEN is set, behind that bearer token), and optional OpenTelemetry tracing and error reporting, both off by
default. See observability.