Nimbus · beta
Keep the SDK. Change the address.
Your Convex, Firestore, MongoDB, or DynamoDB code does not change. Point it at one Rust binary on your machine.
app/messages.tsimport { ConvexHttpClient } from "convex/browser";import { api } from "./convex/_generated/api"; const client = new ConvexHttpClient(process.env.NEXT_PUBLIC_CONVEX_URL); await client.action(api.messages.send, { body: "hello" });import { ConvexHttpClient } from "convex/browser";import { api } from "./convex/_generated/api";const client = new ConvexHttpClient( process.env.NEXT_PUBLIC_CONVEX_URL);await client.action(api.messages.send, { body: "hello" });
CONVEX · FIRESTORE · CLOUD FUNCTIONS · MONGODB · DYNAMODBNetwork · 01 / Client protocols
Every SDK has its own endpoint on one host.
Each SDK keeps its own wire format and port. One process on one host answers all of them.
.env.localNIMBUS_DEPLOYMENT=http://localhost:3210/convex/demoNIMBUS_MONGODB_URL=mongodb://127.0.0.1:27017/…NIMBUS_DYNAMODB_ENDPOINT=http://127.0.0.1:8000NEXT_PUBLIC_CONVEX_URL=http://localhost:3210/convex/demoNIMBUS_DEPLOYMENT=http://localhost:3210/convex/demoNIMBUS_MONGODB_URL=mongodb://127.0.0.1:27017/…NIMBUS_DYNAMODB_ENDPOINT=http://127.0.0.1:8000NEXT_PUBLIC_CONVEX_URL= http://localhost:3210/convex/demo
:3210 HTTP + WS · :27017 MONGODB · :8000 DYNAMODBNetwork · 02 / Client 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.
convex/agent.tsexport const send = action({ args: { body: v.string() }, handler: async (ctx, { body }) => { await ctx.runMutation(internal.messages.write, { body }); await nimbus.sessions.open({ target: { service: { name: "agent" } }, channels: ["stdio"], }); },});export const send = action({ args: { body: v.string() }, handler: async (ctx, { body }) => { await ctx.runMutation( internal.messages.write, { body }); },});
nimbus-server · nimbus-adapters · nimbus-engineCompute · 03 / Functions
Functions run next to the data.
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"; const nimbus = new Nimbus({ endpoint: process.env.NIMBUS_URL, tenantId: "demo", token: process.env.NIMBUS_TOKEN,});import { Nimbus } from "@nimbus/nimbus"; const nimbus = new Nimbus({ endpoint: process.env.NIMBUS_URL, tenantId: "demo", token: process.env.NIMBUS_TOKEN,});
V8 IN-PROCESS · ONLY ACTIONS REACH THE NETWORKCompute · 04 / 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(); "use node";import OpenAI from "openai"; const openai = new OpenAI();
NODE 22 · 24 · 26 · ACTIONS ONLY · NO NATIVE ADDONSStorage · 05 / 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.
convex/messages.tsexport const write = internalMutation({ args: { body: v.string() }, handler: async (ctx, args) => { await ctx.db.insert("messages", args); },});export const write = internalMutation({ args: { body: v.string() }, handler: async (ctx, args) => { await ctx.db.insert("messages", args); },});
DOCUMENT + INDEX + COMMIT LOG = ONE TRANSACTIONStorage · 06 / Realtime
Every transaction updates live queries.
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, {}); return messages?.map((m) => <li key={m._id}>{m.body}</li>);const messages = useQuery(api.messages.list, {});return messages?.map((m) => <li key={m._id}>{m.body}</li>);
CONVEX WS · onSnapshot · runAfter AT-LEAST-ONCEStorage · 07 / 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 $ nimbus start --tenant-provider postgres \ --postgres-url postgresql://db:5432/nimbus$ nimbus kv $ nimbus start $ nimbus start \ --tenant-provider postgres \ --postgres-url postgresql://…/nimbus$ nimbus kv
SQLITE DEFAULT · POSTGRES · MYSQL · LIBSQL · REDBStorage · 08 / 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 volumes: scratch: {} services: agent: image: python:3.12 volumes: [scratch:/work]volumes: { scratch: {} }
BLAKE3 · ENCRYPTED · S3 API :9000Agents · 09 / 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.
compose.yamlservices: agent: image: docker.io/library/python:3.12 command: ["python", "agent.py"] volumes: ["scratch:/work"] deploy: { resources: { limits: { cpus: "1", memory: 512M } } } x-nimbus: { backend: krun, egress: { allow: [] } } services: agent: image: docker.io/library/python:3.12 command: ["python", "agent.py"] volumes: ["scratch:/work"] x-nimbus: { backend: krun }
LIKE A POD · OCI IMAGE · NO DAEMON · CRUN · LIBKRUNAgents · 10 / Egress
Nimbus denies agent network access by default.
The egress proxy denies every sandbox request by default. An allow rule names one protocol, one host, one port, and its paths, with no wildcards.
compose.yamlservices: agent: x-nimbus: egress: allow: - { name: stripe-api, protocol: https, host: api.stripe.com, port: 443, methods: [POST], path_prefixes: [/v1/] }x-nimbus: egress: allow: - { name: stripe-api, protocol: https, host: api.stripe.com, port: 443, methods: [POST], path_prefixes: [/v1/] }
nimbus-egress DECIDES · nimbus-proxy ENFORCESWorkloads · 11 / Services
Code depends on a name, not a sandbox.
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" }); await 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 APIWorkloads · 12 / 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.
compose.yamlservices: web: image: ghcr.io/acme/web:1.4 ports: ["127.0.0.1:3000:8080"] agent: image: ghcr.io/acme/agent:1.4 command: ["python", "agent.py"] worker: image: ghcr.io/acme/worker:1.4 x-nimbus: { backend: crun } services: web: image: ghcr.io/acme/web:1.4 ports: ["127.0.0.1:3000:8080"] agent: image: ghcr.io/acme/agent:1.4 command: ["python", "agent.py"] worker: image: ghcr.io/acme/worker:1.4 x-nimbus: { backend: crun }
ONE SANDBOX PER SERVICE · REPLICAS PLANNEDWorkloads · 13 / Sessions
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.
app/agent.tsconst shell = await nimbus.sessions.open({ target: { service: { name: "agent" } }, channels: ["stdio"],});console.log(shell.spec.targetSnapshot.service);console.log(shell.spec.expiresAt); const shell = await nimbus.sessions.open({ target: { service: { name: "agent" } }, channels: ["stdio"],});console.log(shell.spec.expiresAt);
LIKE KUBECTL EXEC · TTL 15 MIN · NO BYTES YETRun · 14 / Binary
You run one process, not a platform.
Network, compute, storage, and agents ship in one Rust binary with one trust model. There is no sidecar and no queue to run.
shell$ lsnimbus $ ./nimbus start $ lsnimbus $ ./nimbus start
ONE BINARY · NO DOCKER · NO KUBERNETES · NO QUEUERun · 15 / Tenants
Tenants cannot see each other.
Each tenant has its own database, file store, key, and budget. Nimbus admits a request once. No API can express a cross-tenant read.
shell$ curl -s -X POST http://localhost:8080/api/tenants \ -H "Authorization: Bearer $NIMBUS_TOKEN" \ -d '{"id": "acme"}'{"id": "acme"} $ nimbus start --runtime-max-active-per-tenant 8$ curl -s -X POST \ http://localhost:8080/api/tenants \ -H "Authorization: Bearer $NIMBUS_TOKEN" \ -d '{"id": "acme"}'{"id": "acme"} $ nimbus start \ --runtime-max-active-per-tenant 8
OWN DATABASE · KEY · BUDGET · 429 OVER BUDGETRun · 16 / Deployment
Deploy to any Linux host you own.
Install on any Linux host and run it as a service. Deploy from a laptop with a dry run first.
shell$ curl -fsSL https://github.com/nimbus/nimbus/…/install.sh | sh$ sudo systemctl enable --now nimbus $ nimbus deploy https://nimbus.example.com --dry-run$ curl -fsSL \ https://github.com/nimbus/…/install.sh | sh$ sudo systemctl enable --now nimbus $ nimbus deploy https://nimbus.example.com \ --dry-run
NO TELEMETRY · NO METERING · NO CLUSTERING YETRun · 17 / Cluster · planned
Scale out to more hosts later.
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$ nimbus cluster init $ nimbus cluster join-token create $ nimbus cluster join <token> $ nimbus cluster promote <id> $ nimbus cluster members $ nimbus cluster init$ nimbus cluster join-token create$ nimbus cluster join <token> $ nimbus cluster promote <id> $ nimbus cluster members
IROH + OPENRAFT · QUIC UDP/7842 · RELAY TCP/443Run · 18 / Operator · planned
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$ nimbus cluster members $ nimbus cluster status$ nimbus forward … $ nimbus cluster members $ nimbus cluster status$ nimbus forward …
OPERATOR KEY · ADMIN · OP-FORWARD · NEVER A VOTERRun · 19 / 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$ brew install nimbus/tap/nimbus$ nimbus init convex my-app$ cd my-app$ nimbus dev Local: http://localhost:3210$ brew install nimbus/tap/nimbus$ nimbus init convex my-app$ cd my-app$ nimbus dev Local: http://localhost:3210
BREW INSTALL · NIMBUS INIT · NIMBUS DEV · LOCALHOST:3210