Each SDK keeps its own wire format and port. One process on one host answers all of them.
.env.local# written by nimbus devNIMBUS_DEPLOYMENT=http://localhost:3210/convex/demoNIMBUS_MONGODB_URL=mongodb://127.0.0.1:27017/…NIMBUS_DYNAMODB_ENDPOINT=http://127.0.0.1:8000# you set the variable your Convex client reads:NEXT_PUBLIC_CONVEX_URL=http://localhost:3210/convex/demo# written by nimbus devNIMBUS_DEPLOYMENT=http://localhost:3210/convex/demoNIMBUS_MONGODB_URL=mongodb://127.0.0.1:27017/…NIMBUS_DYNAMODB_ENDPOINT=http://127.0.0.1:8000# you set the variable your client reads:NEXT_PUBLIC_CONVEX_URL= http://localhost:3210/convex/demo
:3210 HTTP + WS · :27017 MONGODB · :8000 DYNAMODB
BACKEND ADAPTERS
Every SDK protocol is an adapter.
Each adapter turns its protocol into engine operations and passes an authenticated identity, not a raw token. Every SDK meets the same rules and the same database.
The handler runs on V8 inside the binary. Queries and mutations reach the database with no network hop. Only actions reach the network.
convex/agent.tsimport { Nimbus } from"@nimbus/nimbus";// the SDK is one more HTTP client, so only an action may hold itconst nimbus = new Nimbus({ endpoint: process.env.NIMBUS_URL, tenantId: "demo", token: process.env.NIMBUS_TOKEN,});import { Nimbus } from"@nimbus/nimbus";// one more HTTP client: actions onlyconst nimbus = new Nimbus({ endpoint: process.env.NIMBUS_URL, tenantId: "demo", token: process.env.NIMBUS_TOKEN,});
V8 IN-PROCESS · ONLY ACTIONS REACH THE NETWORK
NODE
One directive moves an action to Node.
Add "use node" to an action to run it on Node 22, 24, or 26 with npm packages. Native addons and subprocesses need a sandbox.
convex/agent.ts"use node";import OpenAI from"openai";import { action } from"./_generated/server";import { v } from"convex/values";const openai = new OpenAI(); // fetch, inside the tenant's egress policy"use node";import OpenAI from"openai";const openai = new OpenAI();// fetch, inside the tenant's egress policy
NODE 22 · 24 · 26 · ACTIONS ONLY · NO NATIVE ADDONS
WRITES
Every write is one database transaction.
A mutation and a driver insertOne take the same path. One transaction writes the document, its indexes, and the commit log. Nimbus acknowledges only durable writes.
A MongoDB write updates a live Convex query the moment it commits. There is no polling and no pub/sub service to run.
app/Chat.tsxconst messages = useQuery(api.messages.list, {});// re-renders the moment the commit landsreturn messages?.map((m) => <li key={m._id}>{m.body}</li>);const messages = useQuery(api.messages.list, {});// re-renders the moment the commit landsreturn messages?.map((m) => <li key={m._id}>{m.body}</li>);
CONVEX WS · onSnapshot · runAfter AT-LEAST-ONCE
DATABASE
Pick the database. SQLite is the default.
SQLite runs inside the process with zero setup. Point one flag at Postgres, MySQL, or libSQL when you already run one. Every API works on every backend.
shell$ nimbus start # SQLite in ./data$ nimbus start --tenant-provider postgres \ --postgres-url postgresql://db:5432/nimbus$ nimbus kv # RESP on 127.0.0.1:6380$ nimbus start # SQLite in ./data$ nimbus start \ --tenant-provider postgres \ --postgres-url postgresql://…/nimbus$ nimbus kv # RESP on 127.0.0.1:6380
SQLITE DEFAULT · POSTGRES · MYSQL · LIBSQL · REDB
FILES
One blob store for S3 objects, files, and volumes.
Each tenant has one encrypted, content-addressed store. Uploads, the S3 endpoint, the function filesystem, and sandbox volumes read the same bytes.
compose.yamlservices: agent: image: python:3.12 volumes: - scratch:/work # a named tenant volumevolumes: scratch: {} # no host bind mounts · S3 API on :9000services: agent: image: python:3.12 volumes: [scratch:/work]volumes: { scratch: {} }
BLAKE3 · ENCRYPTED · S3 API :9000
SANDBOX
Agent sandboxes run outside the Nimbus process.
Compose gives each agent an OCI image, a microVM or container, and a volume at /work. No daemon runs. The sandbox has no path back to the engine.
A service is a name that other code depends on. A sandbox runs it. Replace the sandbox and the name still resolves.
app/agent.tsconst agent = await nimbus.services.get({ name: "agent" });// every write fences on the generation it readawait nimbus.services.restart({ name: "agent", sourceGeneration: agent.metadata.generation,});const agent = await nimbus.services.get({ name: "agent" });await nimbus.services.restart({ name: "agent", sourceGeneration: agent.metadata.generation,});
LIKE A K8S SERVICE · SAME VERBS IN COMPOSE AND API
COMPOSE
Run an app as services on one host.
A compose file names each service. Each one runs in its own microVM or container next to the engine. One service runs one sandbox today. Replicas are on the roadmap.
A session is an audited connection to a running service.
Plain service use needs no session. Open one for stdio, file exchange, or browser control. Nimbus authorizes the lease at open, and it expires at its TTL.
Planned cluster mode joins hosts over a QUIC mesh. Each node is an Ed25519 key, not an IP address. A host joins with a token as a learner, and each tenant gets one owner node. None of this ships today.
shell · planned# planned · not in any release yet$ nimbus cluster init # the first host$ nimbus cluster join-token create # on a member$ nimbus cluster join <token> # on the new host · a learner first$ nimbus cluster promote <id> # then a voter$ nimbus cluster members # nodes by key · role · tenant owner# planned · not in any release yet$ nimbus cluster init$ nimbus cluster join-token create$ nimbus cluster join <token> # a learner$ nimbus cluster promote <id> # a voter$ nimbus cluster members # by key
IROH + OPENRAFT · QUIC UDP/7842 · RELAY TCP/443
OPERATORPlanned
Reach the cluster from a laptop.
The planned operator plane is the same mesh. The CLI dials any node by key, through NAT, with an operator key that has admin and port-forward scopes only. It is never a voter.
shell · planned# planned · not in any release yet$ nimbus cluster members # from a laptop · by key$ nimbus cluster status$ nimbus forward …# a local TCP port to a port on one node# an agent takes the same path with a scoped key# planned · not in any release yet$ nimbus cluster members # by key$ nimbus cluster status$ nimbus forward …# a local port# an agent takes the same path
OPERATOR KEY · ADMIN · OP-FORWARD · NEVER A VOTER
LAPTOP
Start local with three commands.
Install the binary and point the app at localhost:3210. Nimbus is in beta. APIs can break between releases. Do not use it in production yet.
shell · macOS and Linux
$ brew install nimbus/tap/nimbus$ nimbus init convex my-app && cd my-app$ nimbus dev
BREW INSTALL · NIMBUS INIT · NIMBUS DEV · LOCALHOST:3210