Architecture overview
Subspace is built on one deliberate bet: write everything in one language, and keep all
durable state in one database. Server, web UI, desktop shell, mobile app, Chrome
extension, agent runtime, and sandboxed user functions are all TypeScript. Every byte that
must survive a restart, pages, mail, calendar, tasks, agent runs, embeddings, lives in one
embedded PostgreSQL instance. The result is a system one person can hold in their head and
grep end to end.
One language everywhere
Section titled “One language everywhere”The incumbent design this replaces paid for a Clojure server talking to a Python agent sidecar over RPC, a Rust shell over another RPC, and hiccup templates hydrating into JS islands. Every one of those seams is a place where a schema can drift and a device can break at runtime instead of at compile time.
Subspace collapses those seams into imported functions and shared types. There is one
pnpm monorepo, one type system, and one pure reducer (@subspace/kb-core) that runs
identically on the client (optimistically) and the server (authoritatively). Two surfaces,
the browser and the extension, must be JavaScript no matter what; the bet is that unifying
everything else is worth more than any per-component “best tool for the job.”
The stack is deliberately boring and pinned, with a written no-churn policy: Node LTS, Postgres, React, Electron, Expo. Novelty is spent only where the problem genuinely demands it (see where the philosophy is stretched).
System topology
Section titled “System topology”One Node process, subspace-server, owns all durable state. It holds that state in one
supervised embedded Postgres instance reached over a unix socket, never a network
listener. Every surface is a client of that one server over two channels: tRPC (typed
request/response, zero codegen) and a WebSocket topic bus (live updates).
┌──────────────────────────────────────────────┐ browser SPA ───┤ subspace-server (single Node process) │ (React) │ http: Hono · rpc: tRPC · live: ws topic bus │ desktop shell ─┤ kernel: kb reducer + index maintenance │ (Electron) │ dispatcher: LISTEN/NOTIFY → outbox → bus │ mobile (Expo) ─┤ engines: scheduler · agents · triage · sync │ chrome ext ────┤ pg-embed: supervises Postgres (unix socket) │ └───────────────────────┬──────────────────────┘ ▼ PostgreSQL + pgvector (bundled binaries, ALL durable state)Most surfaces only ever pull work from the server. Two satellites also receive work over their persistent WebSocket:
Mobile reaches the server over Tailscale; push goes through Expo’s relay. The server runs on your laptop first (supervised by the shell) and moves to an always-on box by moving one Postgres data directory plus the blob directory. See self-hosting for the move.
Stack highlights
Section titled “Stack highlights”Every choice below flows from the two rules (one language, one database):
| Layer | Choice | Why |
|---|---|---|
| Language | TypeScript, strict, ESM |
one type system end to end |
| Runtime | Node.js LTS | boring, strong native-module ecosystem (pty, zmq) |
| Database | PostgreSQL + pgvector, embedded | one transactional store; MVCC means a Gmail backfill never queues behind a keystroke |
| Driver | postgres.js (pure JS, pooled, pipelined) | removes the last native module from the storage path, no ABI rebuild churn |
| Query layer | Drizzle ORM + drizzle-kit migrations | typed SQL, plain transactional SQL migration files |
| Validation | Zod | one schema is runtime validation plus an inferred static type, shared by every surface |
| HTTP / RPC | Hono + tRPC + TanStack Query | end-to-end types with no codegen |
| Live | ws, topic fan-out fed by the outbox dispatcher |
one socket per window, sub/unsub per topic |
| Web UI | React + Vite SPA (served by the server) | a local single-user app has no SEO or cold-load problem, so no SSR |
| Desktop | Electron (main process is Node/TS) | node-pty, hotkeys, supervision all in-language |
| Mobile | Expo (React Native) | capture-first native UX plus a WebView for full pages |
| Agent runtime | Workflow DevKit over a self-hosted Postgres World | durable waits, sub-agents, arbitrary topologies, steps stay Postgres rows |
| Search | Postgres FTS (tsvector + GIN) plus pgvector HNSW |
lexical index updates in the same transaction as the write; real ANN in the same database |
Monorepo packages
Section titled “Monorepo packages”The workspace is a handful of packages consumed as typed imports, with no build
orchestrator beyond tsc -b and Vite:
packages/schema, Zod schemas and shared types (the command envelope lives here)packages/kb-core, the pure reducer and tokenizer/parserpackages/formula, the table-formula parser and evaluator (see tables and formulas)packages/okf, the OKF codecpackages/api, tRPC router typespackages/ui, shared React componentsapps/server,apps/web,apps/desktop,apps/mobile,apps/extension
Only two native modules remain in the whole tree: node-pty (server and shell PTYs) and zeromq (server-side Jupyter kernel client). The storage path is pure JavaScript.
Where the philosophy is stretched
Section titled “Where the philosophy is stretched”The one-language rule is genuinely weak in exactly three places, and each has an explicit, designed answer rather than a hand-wave:
System-audio capture (a Swift helper)
Electron cannot capture macOS system audio, so meetings use a small
Swift ScreenCaptureKit helper that pipes PCM over stdout to the shell. This is a
deliberate purity break, kept to about 200 lines and confined to one channel of one
feature. Microphone capture stays in a hidden getUserMedia window.
Jupyter kernels (foreign processes)
Code cells run real Jupyter kernels, which can be any language a
user installs. Subspace never runs Python of its own: the server implements the
language-neutral Jupyter wire protocol directly over zeromq and spawns kernels from
their kernelspecs. The foreign code is the user’s, not ours.
Embedded Postgres under Node
Node has no well-trodden “Postgres in a pip package” equivalent, so this is the design’s
novelty budget, spent on purpose. Subspace builds its own per-platform Postgres and
pgvector binaries and supervises them from a small pg-embed module. See
Storage and Postgres for the full rationale and the
major-version upgrade path.
Bundled binaries (Postgres, pgvector, WAL-G) count as dependencies, not seams: none of them carries code of ours in another language.