Back to the overview

architecture · wire level · one machine

How the parts actually talk to each other.

Three server processes, ten workers, the MantleOS web app, a handful of sidecars, and one Postgres that does far more than store rows. It is the queue, the journal, and the event bus as well. Below is every process and, more to the point, every wire between them: what the transport is, and what rides it.

This page is written for the reader who has to sign off on it. Nothing is drawn that isn’t in the Compose files, and the most important fact is at the bottom of the stack rather than the top: one front door listens on the network, and every kind of traffic that leaves the machine is listed.

Topology

Five tiers, one box

Read it downward. Each band between two tiers names the transport that crosses it. The labels are the point, not decoration. Everything below the front door is reachable on the internal Docker network and nowhere else.

Edgeapps, clients, channels and sources, outside the box
MantleOS web

The owner UI in any browser. Your box serves it at your brain’s address, so there is nothing to install.

HTTPSHTTP/3SSE
MantleOS desktop

The same app for macOS (Apple Silicon), Windows and Linux, with notifications, a tray icon and several brains in one window.

Installers on GitHub Releases

.dmgSetup.exe.deb.AppImage
MantleOS mobile

Your chat, files and approvals on the phone, with push notifications. On the App Store for iPhone. Android is not on Google Play yet.

App Store

iOStoken per device
AI apps

Claude, Codex, ChatGPT, Gemini, GitHub Copilot, Grok and Perplexity can each add your brain as a remote MCP connector today. Some need a paid plan or a setting turned on.

remote MCPOAuth
MCP clients

Any other MCP client. Remote over OAuth, or local over stdio on the server through SSH.

OAuthstdio JSON-RPC
HTTP API

The public, versioned /api/v1, called with a per-login API key (read only or read and write, scoped, expiring). The other /api routes are the client’s own and can change with any release.

HTTPSBearer token30 days
Telegram

Per-responder bots. Text and voice, both directions.

Bot API
Mail · Microsoft 365 · calendars

IMAP/SMTP on your own mailbox, Graph for SharePoint, OneDrive and Outlook, iCal feeds.

IMAPSMTPMS GraphiCal
Host filesystem

A watched directory. Edit with vim or Syncthing and the brain follows.

bind mountinotify
  • HTTPS + HTTP/3 → :443
  • session cookie or Bearer
  • SSE (EventSource)
  • stdio JSON-RPC (local)
  • account APIs (outbound, started by the box)
Front doorthe trust boundary: everything below is internal-only
caddy

The only container that listens on the network. Automatic Let’s Encrypt TLS. By default one domain, routed by path: the server paths go to web and everything else to MantleOS web. A second shape gives the owner UI its own domain.

:80:443:443/udpACME256 MB cap
  • reverse_proxy web:3000
  • reverse_proxy client-web:3000
  • internal Docker network
Processesthe server runs TypeScript on tsx, with no build step
client-web

MantleOS web, the owner UI. A Next.js app built in the client repo and run as a small stack of its own beside the server. Zero secrets: no database and no session key. Every byte arrives from server/web over bearer. A headless box leaves it out.

Next.jsno DBbearer768 MB cap
server/web

The backend. A Hono HTTP server behind an http-compat shim, carrying the whole /api surface, the remote MCP endpoint, the retrieval stack, files, and the SSE endpoints. Owns the DB connection.

Honotsx~470 routes3 GB cap
server/apiawake

The durable runner. Every assistant and Telegram turn runs here as a DBOS workflow journaled to its own Postgres system database, so a crash replays from the journal instead of re-running the turn. The background agents live here too: extractor, summarizer, reflector, and the heartbeat tick that makes the brain proactive. No HTTP surface at all: reached through Postgres by enqueue and LISTEN, answers over NOTIFY.

DBOSno HTTPresumableheartbeats3 GB cap
server/mcp

The stdio MCP server. packages/mcp-core registers one tool set for both this and the HTTP /api/mcp OAuth endpoint, so a local client and a remote client see the same tools.

stdioJSON-RPC
workers ×10

Separate processes off the same image: email, telegram, microsoft, files, docs, events, calendar, maintenance, runs, push. A core box keeps four of them: files, docs, events and maintenance. Coordinated through Postgres, with pg-boss queues and LISTEN/NOTIFY; no broker to run.

pg-bossone image1 GB cap each
sandboxd

Code sandboxes. On by default on a fresh full install, off on core, and switched in Settings > Services. Holds the Docker socket, so it is deliberately narrow: bearer auth, a fixed verb set, and only containers it labelled itself. Sandboxes land on an isolated network with egress only, and no route to Postgres, RustFS, or web.

Services switch512 MB cap
  • Drizzle pool
  • pg-boss queues
  • DBOS enqueue + journal
  • pg_notify / LISTEN
  • S3 SDK
Data planeone Postgres does queueing, journalling and the event bus
postgres

The source of truth, and most of the middleware too. pgvector for 768-dim embeddings under HNSW, pg_trgm and ltree, the pg-boss queues, the DBOS journal, the turn-stream replay buffer, the LISTEN/NOTIFY bus that ties every process together, and the row-level security that limits every login and workspace assistant to what its workspaces hold.

pgvectorpg18LISTEN/NOTIFYno memory cap
rustfs

S3-compatible object store for file and attachment bytes. Private bucket, internal network, no published port, no anonymous access.

S3 SDK1 GB cap
host volumes

Files, backups, per-app SQLite for mini-apps, and per-table SQLite for Tables. That last one is mounted into server/web and server/api alike, because the handlers run in both.

bind mountsSQLite
  • HTTP + bearer (internal)
  • WebSocket (internal)
  • Docker socket (updater, sandboxd, autoheal)
  • no published ports
Sidecarsinternal-only, and each one that stays running has its own memory cap
tika

Document parser fallback for the formats in-process parsers miss. Self-hosted, so the bytes never leave the box. Always on a full box; on a core box it starts with the Helpers switch.

HTTP :99981.5 GB cap
browser

A headless Chromium the app drives with puppeteer-core to print its own authenticated page surface, so an exported PDF reuses the live CSS. A bad render dies here, not in web. Same Helpers rule as tika.

WebSocketpuppeteer1.5 GB cap
media

Video and audio through yt-dlp and ffmpeg, for transcripts. CAD drawings too: DWG through LibreDWG, DXF through ezdxf, and DWF plot sets. Each drawing gives a text digest of its layers and labels, a table of the labels, and a rendered image for vision. No database, no secrets, no file store.

HTTP :8095bearerServices switch1 GB cap
ollama

The local embedder: EmbeddingGemma at 768 dimensions over an OpenAI-compatible API. Off by default, because embeddings are online by default. When it is in use, the text being embedded never leaves the machine.

HTTP :11434Services switch2 GB cap
tailscale

Userspace tailscaled with an outbound HTTP proxy, so the brain can reach a GPU box behind NAT by MagicDNS without opening a port. Always running, and idle until an admin activates it from the settings.

proxy :1055256 MB cap
updater

Two fixed verbs: roll the stack to a new release, and switch an optional service on or off. A roll takes a backup first and is refused if the backup fails. No ports; triggered by a file on a private volume.

Docker socket128 MB cap
migrate

A one-shot container from the server image. On each start it runs the database migrations, creates the pg-boss schema, the DBOS system database and the file bucket if missing, then exits. Every app service waits for it to finish.

one-shotrestart: no
autoheal

Restarts a worker whose liveness probe says it has stopped doing work while still running. It touches only containers labelled for it, never Postgres or the front door.

Docker socket64 MB cap
  • HTTPS egress: every kind is listed in the wire table
Outboundmodel calls, your accounts, and the features you turn on
Model providers

Chat, embeddings, vision, speech and images, through OpenRouter or a direct adapter, each route with a backup. Keys live in the encrypted vault and are referenced by handle, so the plaintext never reaches a tool, a trace, or a chat.

HTTPSvault refsprimary + backup
Your accounts and the rest

Your own mail, Microsoft, Telegram and calendars, a push relay for phones, peer brains, web fetch, release checks and image pulls, and Let’s Encrypt. Most start only when you connect an account or turn a feature on.

HTTPSIMAPSMTP

Solid boxes are the default stack on a full box. Dashed boxes are opt-in: they ship today, and start when an admin switches them on in Settings > Services or with an install flag. A memory cap is a guardrail against a leak, not a reservation.

The critical path

One turn, across four processes

The hardest thing in the system is also the most-asked question: the turn runs in server/api, but the browser’s socket is held by server/web. Always two processes. Postgres bridges them, which is why the design works the same way on one box or five.

  1. 01

    The browser posts, and is answered in milliseconds

    POST /api/assistant/turn returns 202 without waiting for the reply. Close the tab; the turn does not care.

  2. 02

    server/web enqueues the workflow

    A DBOS enqueue over Postgres. There is no HTTP route into the runner to secure, because there is no HTTP surface on it at all.

  3. 03

    server/api runs it durably

    Each step is journaled as it completes. Restart the process mid-turn and it resumes from the last good step rather than starting over.

  4. 04

    Tools run under a real grant

    Search, reads, page edits, delegation to sub-agents. Every call is checked against the agent’s tool groups server-side, never against what the model asked for.

  5. 05

    Events publish over NOTIFY

    Grounded status built from actual tool calls, plus reasoning and text deltas. Small messages, because the bus caps payloads at about 8 KB.

  6. 06

    server/web fans out over SSE

    One LISTEN, many sockets. A reconnect sends Last-Event-ID, gets the missed range replayed from the buffer table, then live-tails. Gap-free and duplicate-free.

  7. 07

    The answer lands in the durable row

    Written once, by the runner. The stream was always allowed to be lost; the reply never was.

  8. 08

    The trace stays

    Every step, tool call and cost is recorded and readable afterwards. The same record that makes a citation checkable.

Durability and liveness travel separate paths. The live token stream is never journaled, so a crash mid-stream costs you the animation, never the answer.

Every wire

Named, in full

A diagram that tried to draw all of these would be less precise, not more. So the picture gives the shape and this gives the edges: every connection in a default install, with its actual transport.

Getting in

Caddy is the only container that listens on the network. web also binds a loopback port on the host (3000 by default) for local checks, and that port is not reachable from outside. Everything else is reachable on the internal Docker network and nowhere else.

FromToTransportWhat rides it
MantleOS apps (web, desktop, mobile)caddyHTTPS :443 · HTTP/3 (QUIC) · :80One domain by default, routed by path. A second shape puts the owner UI on its own domain. Certificates are issued and renewed automatically once the brain has a domain name.
caddyclient-web:3000reverse_proxy, internal networkMantleOS web, the owner UI. Every path the server does not own. It reads its server origin from /env.js at request time.
caddyweb:3000reverse_proxy, internal networkThe server paths: /api (with the SSE endpoints and /api/mcp), the share pages, the print surface, the share and app runtime bundles, and the MCP OAuth discovery documents.
MantleOS web and desktopserver/web /api/**HTTPS · session cookie or Authorization: BearerEvery read and write. There are no server actions and no DB code in the bundle. On the default single domain the browser sends its session cookie. A client on another origin sends a bearer token, and only origins an admin lists may call across. The cookie path refuses every purpose-bound token, such as a phone token or an asset link, so none of them can pass as a full session.
MCP client on the serverserver/mcpstdio JSON-RPC (local process)The full owner tool set, no network hop. Starting the process already needs owner access to the box.
Remote MCP clientserver/web /api/mcpHTTPS + OAuthThe same tool set, registered once in packages/mcp-core so the two front doors cannot drift. The one exception is the terminal tool, which stays off over HTTP unless the operator turns it on. The endpoint is off until an admin turns it on. A user who connects gets only what their own workspaces hold.

Running a turn

The turn runs in one process and the browser’s socket is held by another. Always two. Postgres is the bridge, from day one.

FromToTransportWhat rides it
MantleOS appserver/web POST /api/assistant/turnHTTPS → 202 Accepted with a turn idThe route does not wait for the reply. It enqueues the turn and returns, so the turn survives a reload, a navigation, or a backgrounded tab. An attached file is read first, inside the request.
server/webserver/apiDBOS enqueue over PostgresThe workflow input. server/api has no HTTP surface. Everything reaches it through Postgres: this queue, and LISTEN channels for Stop, Telegram, routines and indexing.
server/apiPostgres system DBDBOS journalEvery completed step. A crash mid-turn replays the journal and resumes at the last good step instead of re-running the turn.
server/apiserver/webpg_notify(turn_stream), ~8 KB limit per messageTurn events: turn-start, status, reasoning and text deltas, then done or error. Postgres limits one NOTIFY to about 8 KB, so each event is one small delta and never a batched blob.
server/apiturn_stream_bufferINSERT, 15 min TTL, keyed by (turn, seq)A replay log, written before each NOTIFY. NOTIFY has no backlog, so a reconnect sending Last-Event-ID gets the gap replayed, then live-tails.
server/webMantleOS appSSE on GET /api/assistant/turn/:turnId/streamThe same event contract for the MantleOS web, desktop and mobile clients. If an event cannot be expressed as JSON every client could render, it is modelled wrong.
server/webserver/apipg_notify(turn_cancel)Stop. The runner aborts the model call at once and keeps the part of the reply that already streamed.
server/apiassistant_messagesdurable writeThe answer, written by the runner: a pending row first, then the final text. Durability and liveness travel separate paths on purpose: the live delta stream is never journaled, so a crash mid-stream loses the animation and not the reply.
telegram_messages INSERTserver/apipg_notify(telegram_message_inserted) via triggerA Telegram message starts its own durable workflow in the same runner, and the runner sends the reply to the Bot API. These turns have no live stream.

Acting on its own

The other half of the product. Nothing here is triggered by a person. A clock starts the work, and the work runs through the same tool loop as a chat turn.

FromToTransportWhat rides it
server/api (internal tick)heartbeatsevery 60s; on failure the wait doubles, up to 30 minFind up to 10 routines whose next fire time has passed. The backoff uses the same ticker as the reflector, so a flaky database or provider cannot turn the loop into a hot loop.
Gate checkfire / skipearliest_at → cooldown → quiet hours → idleFour gates, cheapest first. Each gate is off unless the routine sets it, and Fire now skips all four. Quiet hours use the routine’s own timezone, or the owner’s profile timezone. The idle gate asks whether you have actually been quiet, and applies only to a Telegram surface.
A fired heartbeatthe tool loopin process, not a DBOS workflowThe same tool loop as a chat turn, traced, with the agent’s own tool grants plus the routine tools. It is not journaled: if the process stops mid-fire, the routine fires again on the first tick after the restart.
server/webserver/apipg_notify(heartbeat_due, ownerId)The low-latency wake. Creating or editing a routine does not wait up to a minute for the next tick: server/web notifies, the runner listens. Fire now does not use this wire. It runs the routine at once, inside server/web.
The agentits own routinesfive builtin toolscomplete, snooze, update_state, list, fire. The assistant manages its own schedule, which is why a routine can decide it is done, or put itself off until Tuesday.

Keeping the screen honest

The UI repaints because the database said something changed, not because a timer fired.

FromToTransportWhat rides it
nodes INSERTserver/web · server/apipg_notify(node_ingested) via triggerEvery new item in the brain, whatever put it there. Edits send the same signal from code. The extractor in server/api listens too, and that is how indexing starts.
Extractor (server/api)server/webpg_notify(node_indexed)The second beat: a freshly summarised and embedded item. The insert alone has no summary yet, which is why one channel was never enough.
server/webMantleOS appSSE on GET /api/realtime, per ownerA change signal, filtered by type: new and indexed items, approvals, runs, tasks, comments and the file tree. One LISTEN connection per server/web process serves every open tab and every stream. Owner isolation is enforced server-side, so another owner’s change is never emitted.
Postgresworker_pushLISTEN, its own connectionNew replies, approvals, items that need you, and login notices: the events that become a phone notification.

Feeding it

Ten workers on a full box and four on a core box, all from one image, with no message broker. Five of them take jobs from pg-boss queues inside the same Postgres. The others long-poll, watch a folder, run a timer, or LISTEN.

FromToTransportWhat rides it
worker_emailYour mail providerIMAP over TLS, every 2 minInbound sync from your own mailbox. No MTA to run, and the contacts list is the only inbound allowlist. Mail the agent writes leaves from the process that runs the tool.
worker_telegramTelegram Bot APIHTTPS long-pollMessages in, one poll loop per bot. The agent’s replies leave from server/api. This worker sends only pairing and approval prompts.
worker_microsoftMicrosoft GraphHTTPSSharePoint, OneDrive and Outlook mail ingest. Tokens refresh against the Microsoft sign-in service.
worker_filesHost filesysteminotify over a bind mountExternal edits from vim, Syncthing or a plain cp, mirrored back into the brain. Loop-safe: a file whose bytes already match is not written again.
worker_calendariCal feedsHTTPS, every 2 minCalendar ingest from the feeds you add.
worker_eventsTelegram · worker_push30s pollReminders that fire when remind_at passes, to Telegram or to the phone. The same tick runs scheduled backups.
worker_docs · worker_maintenance · worker_runsPostgres · server/apifolder watch · pg-boss cron · pg-boss queuesThe bundled docs indexed for the collections you turn on. A nightly cleanup sweep. Queued runs, which hand agent turns to the runner and stay idle until runs are turned on.
All workersPostgresDrizzle poolEvery row they write. The one-shot migrate container creates the schema, the pg-boss schema, the DBOS system database and the file bucket first, so no worker races to create them.

The supporting cast

Sidecars, none of them published. Each has its own memory cap, so a crash or a leak in one is contained to that container.

FromToTransportWhat rides it
server/api · server/webtikaHTTP :9998, internalParsing for formats the in-process parsers miss, mostly for the extractor. Heap-capped, because a 24 MB deck unzips to many times its size as XML.
server/webbrowser (Chromium)WebSocket, puppeteer-corePDF export and drawings rendered to images. The app drives Chromium to print its own page with a scoped render cookie, so the export reuses the live CSS byte for byte.
server/web · server/apimediaHTTP :8095 + bearer, internalVideo and audio (yt-dlp and ffmpeg) and CAD drawings: DWG through LibreDWG, DXF through ezdxf, DWF plot sets, each with a rendered image. Opt-in. No database, no secrets, no file store.
server/web · server/apisandboxdHTTP :8090 + bearer, internalCreate, run, stop and list code sandboxes. The sandboxes sit on their own network with no route to Postgres, RustFS or web. Each sandbox gets open internet, an allowlist proxy, or no network.
server/web · server/api · workersollamaHTTP :11434, OpenAI-compatibleLocal 768-dim embeddings. Opt-in. When it is in use, the text being embedded never leaves the machine.
server/web · model callstailscaledLocalAPI over a shared unix socket · HTTP proxy :1055Reaching a GPU box behind NAT by MagicDNS. It always runs and stays idle until an admin activates it in the settings.
server/web · server/api · workersrustfsS3 SDK, internal networkFile and attachment bytes. Private bucket, no published port, no anonymous access.
server/api and the ten workerstheir own healthchecka liveness file, touched on a timerConfusingly also called a heartbeat, and unrelated to the routines above: this one is just a process saying it is alive, so autoheal can restart one that has quietly stopped doing work while still running. web, Postgres and the sidecars have their own probes, and autoheal never restarts Postgres, the object store, caddy or web.

Going out

Every kind of traffic that leaves the machine. Most of it starts only when you connect an account or turn a feature on.

FromToTransportWhat rides it
server/api · server/web · workersModel providersHTTPSChat, embeddings, vision, speech, images and web search, direct or through OpenRouter. A route can have a configured backup. Embeddings are online by default. With the local embedder in use, the text being embedded stays on the box.
Workers · server/api · server/webYour own accountsIMAP · SMTP · Graph · Bot API · iCalMail, files, messages and calendars, from services you already use, on your credentials. Telegram replies and mail the agent sends leave from the process that runs the turn. Nothing runs until you connect one.
worker_push · server/webPush relayHTTPSNotifications for a phone that is not open. Each one is sealed per device before it leaves. Nothing is sent until a phone connects.
server/web · server/apiPeer brainsHTTPS + bearerFederation questions and answers, only to peers you added.
Agent tools · media · sandboxesThe webHTTPSWeb fetch (it cannot reach private or metadata addresses), a crawl service when you add its key, the MCP connectors and HTTP tools you add, video downloads on the media sidecar, and sandbox egress at the tier you set.
server/web · updaterGitHub releases · image registriesHTTPSA check for a new release, and the image pulls for an update you start.
tailscaleTailscaleHTTPS · WireGuardCoordination and relay traffic, only after an admin activates the tailnet.
caddyLet's EncryptACME (HTTP-01 / TLS-ALPN-01)Certificate issuance and renewal.

Teams and peers

Two borders, one rule

People reach a brain through workspaces. Other brains reach it through peer grants. The rule is the same at both borders: nothing is readable until a grant says so, and taking the grant back takes effect at once.

People, by workspace

Every person has their own login, from an invite link or Add login. A login gives no rights by itself. What a person reads and changes comes from the workspaces they are in.

A workspace is the unit of access
It has users, a Moderator tick for each user, at most one assistant, and its connectors. A new member login gets only its own private workspace, which nobody else sees.
Items are shared by grant
An item is shared by a grant to a workspace, and each grant has a Write switch. Users read every item granted to their workspace. They change it when Write is on, or when they moderate its home workspace.
One assistant per workspace
It reads only that workspace’s items. It changes an item only for a user who could change it by hand.
Chats stay with their owner
Each user reads only their own chats. Admins are no exception: running the brain does not open another user’s chats or private workspace.
Removal is immediate
Remove a user from a workspace and they lose its items at once. Disabling a login ends every session and API key it holds. There are no separate client logins or emailed sign-in codes to chase down.

Brains, by grant

Federation is deliberately not multi-tenancy. Each brain stays one bounded life with its own owner, and the two negotiate at the border. Your brain asks mine a question; mine answers only from what it was told to share.

Two tokens, one per direction
The token they issued us is sealed and reversible, because we have to replay it when we call them. The token we minted for them is kept only as a hash, verified by hashing what arrives. There is no reversible inbound secret at rest.
Grants are explicit
Per document, or a standing per-category subscription resolved at query time, so granting Pages includes pages written after the grant. Neither is ever flattened into a list of ids.
An allowlist decides what can be category-shared
Pages, notes, files, contacts, tables, drawings, events, tasks. Never secrets. Deliberately never email or journal, which stay the owner’s private corpus and can only be shared one document at a time.
The answer is an intersection
What the peer asked for, intersected with what is granted to that peer. There is no unscoped read path in the codebase for a peer to fall back to.
Ungranted looks like non-existent
A document a peer has no grant for returns a 404 that is indistinguishable from one that was never there, so a peer cannot map what it cannot read.

One cross-brain question, end to end

  1. 01

    The asking brain calls

    Its agent uses a peer tool, which posts the question to the peer’s federation endpoint carrying the outbound bearer.

  2. 02

    The answering brain identifies it

    It hashes the presented bearer and matches a unique index. That resolves which peer is asking before anything is read.

  3. 03

    The read is scoped at the source

    The search runs against the intersection of the request and that peer’s active grants. Nothing is filtered out afterwards, because nothing ungranted is fetched.

  4. 04

    And it is on the record

    Every cross-brain read opens its own trace under the answering owner, so the person whose data it is can see who asked what, and when.

Revoking a peer closes both directions at once, and rotating one side leaves the other untouched. Pairing works without either side going first: each adds the other with a URL, hands over the token it minted, and pastes the one it receives.

The boundary

What holds when something goes wrong

Topology answers “what talks to what”. These answer the question a security review actually asks. What stops a persuaded model, a hostile document, or a compromised sidecar from turning into a data leak?

The database decides who reads what

An item is read only through a grant to a workspace. Postgres row-level security applies that to every login and every workspace assistant, so a route or a tool that forgets a check still cannot return the wrong row. Past that, a second brain is the boundary, and the blast radius of any mistake is one install.

Ingested content is data, never instructions

Every email, page and message that enters is treated as content the agent reads, not as text that can steer it. That is what makes an autonomous brain with access to your inbox safe to leave running.

Tool selection is not permission

Which tool the model reaches for is a suggestion. Tool groups are the hard boundary, checked server-side on every call, so a persuaded model still cannot exceed its grant.

Diagnostics may be sanitised; data never is

Retrieved content is fenced and handed to the model verbatim, never paraphrased into the prompt, which is what makes a citation checkable rather than decorative.

Secrets are referenced, not embedded

API keys live in an encrypted vault and appear in tools as handles. The plaintext never reaches a tool definition, a trace, or a chat transcript.

Three containers hold the Docker socket

The updater, the sandbox supervisor, and autoheal are all root-equivalent on the host, so each is narrowed the same way: no published ports, a fixed verb set, and triggering only over a private volume or a bearer-authed call. The updater has two verbs: roll the stack, and switch an optional service on or off.

Why it's shaped like this

Three decisions you can feel

Every architecture is a set of trades. These are the three that shaped everything above, and what each one bought.

Postgres does the middleware

The queue is pg-boss, the journal is DBOS, the event bus is LISTEN/NOTIFY, the vector index is pgvector. No Redis, no Kafka, no separate vector database. One thing to back up, one thing to restore, one thing that can be down.

The client holds no secrets

The owner UI has no database credentials and no session key. It reads its origin at runtime and talks over bearer. That is what lets one image serve any install, and lets you run the UI locally against a deployed brain with nothing but a token.

The runner has no front door

server/api is reached only through Postgres, by enqueue and LISTEN, and answers over NOTIFY. A process with no HTTP surface has no HTTP attack surface, and it is what makes a turn survive a restart, a deploy, or a week parked on a question waiting for you.