Skip to content

How Nimbus works

Nimbus is a source-available reactive document backend that ships as one binary. That binary is simultaneously the server, the protocol surfaces, the function runtime, the storage layer, and the CLI you operate it with. This page is the conceptual map: what lives inside the binary, how a request moves through it, and why “drop-in compatible” is an architectural property rather than a marketing one.

Inside the single process, responsibilities are split into layers that mirror the codebase’s crate boundaries:

  • Server / transport (nimbus-server) owns all network I/O: the native HTTP and WebSocket API, plus the routes and listeners that carry each compatibility protocol. It binds to loopback by default and is the integration point that wires every other layer together.
  • Protocol adapters (nimbus-convex, nimbus-firebase, nimbus-cloud-functions, nimbus-mongodb, nimbus-dynamodb) each own one foreign dialect — wire formats, auth handshakes, error shapes — and translate it into engine operations. Adapters are transport-agnostic libraries; the server decides how each one is exposed.
  • Engine (nimbus-engine) is the central coordinator. Every read, write, subscription, and scheduled job — from any surface — flows through the Engine type. It owns validation, authorization, query planning, the mutation path, and subscription fan-out.
  • Runtime (nimbus-runtime) executes your functions in-process in V8 isolates. It is deliberately standalone: it knows nothing about the rest of Nimbus and declares what it needs from the host through a single HostBridge contract. Bundles are SHA-256 integrity-checked before invocation.
  • Storage (nimbus-storage) is the persistence layer behind one engine-facing contract with multiple providers: embedded SQLite is the default, redb is a second embedded option, and Postgres, MySQL, and libSQL are external options. See storage backends.

A small shared-types crate (nimbus-core) defines the vocabulary — documents, tables, mutations, schemas, queries — used across all of them, and nimbus-bin wraps the whole stack in the CLI: nimbus start boots the server; nimbus dev runs the local development loop.

Every data operation, regardless of which protocol delivered it, ends up on the same path:

client → transport → adapter → engine → storage

The transport accepts the connection and admits the request — including resolving which tenant it belongs to. The adapter for that protocol decodes the wire format and authenticates the caller. The engine authorizes the operation, validates it against the table’s schema if one exists, executes it, and the storage layer makes it durable. Results travel back out through the same adapter, re-encoded in the dialect the client expects.

Function invocations add one loop to this picture, not a second path. When a request invokes one of your functions, the server hands the bundle to the runtime executor, which runs it in a V8 isolate. Every host call the function makes — ctx.db.insert(...), a query, scheduling work — crosses the HostBridge into a server-owned bridge that calls the same engine methods a direct HTTP request would. There is no privileged side channel for code running inside the database: a write from a function and a write from an HTTP client converge on the identical engine-owned mutation path, with the same validation, authorization, and subscription fan-out. Data and mutations covers that path in detail.

The runtime’s isolation from the rest of the system is structural. The runtime crate has no dependency on the engine or storage; it only defines the HostBridge trait, and the server supplies the implementation. Guest code can reach exactly what the bridge exposes — nothing else. Functions can also opt into Node.js compatibility targets while keeping the same in-process invocation model; see how the Node runtime works.

A single Nimbus server hosts many tenants, and the tenant boundary is built into every layer rather than filtered in at the edges:

  • The server admits each request to exactly one tenant before any lower layer sees it.
  • Every engine operation is tenant-scoped — there is no engine API that reads or writes across tenants.
  • Storage gives each tenant its own namespace: a separate database file on the embedded providers, a separate schema on Postgres, a separate database on MySQL, a separate namespace on libSQL. A cross-tenant query is not merely forbidden; it is inexpressible at the storage layer.
  • Function invocations are bound to their tenant before any JavaScript runs; the host bridge they call through carries that binding.

This is also the scaling model: Nimbus scales by distributing tenants, not by sharding inside one tenant’s data. The full trust model — runtime tiers, sandbox boundaries, auth — is in tenant isolation, with operational procedures in the tenant isolation operator guide.

Nimbus does not embed five databases. It runs one engine with one data model, one mutation path, and one subscription system — and each adapter teaches that engine to speak a foreign protocol natively enough that existing clients work unchanged:

  • Convex: HTTP function routes and the Convex WebSocket sync protocol, executing Convex-style queries, mutations, and actions in the V8 runtime.
  • Firestore: the Firestore v1 API — REST document and query endpoints plus the gRPC surface, including Listen streams for realtime updates.
  • Cloud Functions: HTTP-triggered and callable function workloads served over the same HTTP surface.
  • MongoDB: a dedicated listener speaking the MongoDB wire protocol with SCRAM authentication, so official drivers connect with a normal connection string.
  • DynamoDB: a dedicated HTTP endpoint accepting SigV4-signed, X-Amz-Target-dispatched requests from unmodified AWS SDK clients.

The architectural consequence is the interesting part: because adapters translate at the boundary and share the engine underneath, every surface inherits the same guarantees. A document written through the MongoDB adapter is durably journaled, indexed, and pushed to subscribers exactly like one written through Convex or the native API — the adapters own dialect, never data semantics. Adapters authenticate callers and normalize their identities; authorization happens once, in the engine, for all of them.

Each adapter has a front door under Developers and a compatibility matrix under Reference, so you can see precisely which operations each dialect supports today.