Architecture and services
A Nix deployment is a handful of small services around one PostgreSQL database. Each has one job, and only Core decides who may see or change what.
The services#
Service | Image | What it does |
|---|---|---|
Core API |
| The ASP.NET Core service on port 8080. It owns items, properties, views, permissions, sign-in, jobs and search, and signs every storage link. It is the only permission authority. |
Migrator |
| A one-shot job that applies Core's database migrations. Core never migrates its own schema at start-up. |
Collaboration |
| The Node service on port 8100 that holds live document editing (Yjs), document history and version checkpoints, and assembles imports and exports. It asks Core to authorise every session. It must run as exactly one instance: a second one refuses connections rather than splitting a document's state. |
Web |
| The installed web app, served by Caddy on port 8090. Caddy also routes the API, collaboration and storage paths, so the whole product sits behind one origin. |
Workers |
| One Go binary, |
Sites worker |
| Optional. Builds published websites from the build manifest Core freezes, and writes them to the sites bucket. |
Sites serve |
| Optional. Serves published websites to anonymous readers. It has no database, no broker account and no Core secret, only a key to read the sites bucket. |
PostgreSQL |
| All durable state, with row-level security, and full-text search. |
RabbitMQ |
| The job transport between Core and the workers. Each component has its own account. |
Object store |
| An S3-compatible gateway that stores file bytes on disk. Any S3-compatible store can replace it. |
Identity provider |
| Sign-in through OpenID Connect. The Kubernetes manifests include Zitadel; with Compose you bring your own provider. |
Tunnel |
| Optional public ingress without opening ports. |
The six images Nix builds itself, plus a release-tools image used by release scripts, are published for every commit.
Worker roles#
The worker binary runs one or more of five roles, chosen with NIX_WORKER_ROLES as a comma-separated list:
Role | Queue | Work |
|---|---|---|
|
| Document imports and file inspection |
|
| Native |
|
| Signed WebAssembly plugin runs |
|
| Two-way Google and Outlook calendar sync |
|
| Web Push delivery |
When the variable is unset, a worker runs import,export,plugin-events. Listing several roles runs them in one process, which is combined mode. Docker Compose runs five containers with one role each, so a slow export cannot hold up a reminder. The Kubernetes manifests run a single combined worker with all five roles, to fit a small single node.
How the services talk#
Browser sign-in goes through Core. Core runs the OpenID Connect authorisation code flow with PKCE as a backend-for-frontend. Provider tokens stay on the server. The browser holds an opaque, HttpOnly session cookie and receives short-lived access tokens that Core signs itself with a P-256 key. Those tokens carry identity, never roles: permission is decided by Core on every request. nixctl and personal access tokens use a token exchange at /public/v1/auth/token.
Services call each other over /internal. Collaboration, the workers and the sites worker call Core's internal routes with a shared secret in a request header. A missing or wrong secret is answered with 404, not 401, so the routes do not advertise themselves, and they are left out of the public API description. Routes that act for a person also need that person's forwarded token; worker routes take their workspace from the job they are running.
Jobs travel through RabbitMQ, but PostgreSQL is the record. Core writes each job to a durable outbox in the same transaction as the change that caused it, then publishes it. Workers lease and complete jobs through Core. If the broker is lost, nothing durable is lost with it.
File bytes never pass through Core. Core signs short-lived capability URLs on the object store's public origin, and browsers and workers upload and download directly. Workers only accept storage URLs on origins listed in their configuration, and access tokens are never sent to storage.
Public routes#
Caddy in the web container routes one public origin:
Path | Destination |
|---|---|
| Core |
| Collaboration, with the prefix removed; WebSocket upgrades must be allowed |
| The object store, with the signed |
everything else | The web app, falling back to |
With Sites enabled, requests for one-label hosts under the sites domain go to the sites serving process instead. Anything else on that listener is refused with 421.