تخطَّ إلى المحتوى
اختر لغة:العربية

Architecture

هذا المحتوى غير متوفر بلغتك بعد.

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.

Kept's processes, the database and the services around them Browser or PWA service worker offline queue (Dexie) MCP clients token or OAuth Kept image · KEPT_ROLE=all runs both Web: Fastify KEPT_ROLE=web /api/v1 · /mcp · the web bundle Worker: pg-boss jobs KEPT_ROLE=worker reminders, AI, purge, backup kept migrate · kept admin one-shot, as kept_owner Postgres 18 + pgvector data, jobs, sessions File storage local disk or S3 SMTP (optional) Mailpit in development AI provider (optional) or the mock, in tests restic repository a directory, S3 or SFTP
One image in three roles: the web serves people and MCP clients, the worker runs jobs, and the one-shot commands migrate and administer the database as its owner.
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.

The path of one API request HTTP request our own request id onRequest hooks CSRF · token · session Validation zod · module gate scopedWrite() Idempotency-Key one transaction on kept_app: withScope() BEGIN, set_config app.user_id · app.mfa Route handler RLS filters every row audited() audit_events row COMMIT, reply then afterCommit()
The hooks decide who is calling; the write, its audit row, any job it enqueues and its idempotency record commit or roll back together.
  1. Hooks (http/app.ts). Fastify gives the request a fresh UUIDv7 as its id and answers it in x-request-id. Then, in order: a cookie-bearing write from another site is refused (csrfHook), a Authorization: Bearer kpt_… token becomes the scope (bearerHook), and otherwise the Better Auth session does (sessionHook). A route is required unless it says optional or none, so an anonymous call to a protected route stops here with 401. See authentication.
  2. Validation. zod schemas on params, query and body (fastify-type-provider-zod), then the module gate: a route with config.module answers 404 module_off for a read and 409 for a write when that module is off in the target location (http/modules.ts).
  3. The scope. scopedRead() and scopedWrite() (http/write.ts) run the handler inside withScope() (db/scope.ts): one transaction on a kept_app connection that first sets app.user_id, app.mfa and app.token_id transaction-locally. Row-level security reads them. See row-level security.
  4. 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.
  5. 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.
  6. Commit and reply. An undoable write answers x-kept-audit-event. Work that needs another login or its own transaction runs in afterCommit, and its failure is logged, not answered. Any error becomes the one error shape (see API conventions).

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.