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.

import { ConvexHttpClient } from "convex/browser";import { api } from "./convex/_generated/api"; // http://localhost:3210/convex/democonst client = new ConvexHttpClient(process.env.NEXT_PUBLIC_CONVEX_URL); await client.action(api.messages.send, { body: "hello" });
CONVEX · FIRESTORE · CLOUD FUNCTIONS · MONGODB · DYNAMODB

Network · 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.

# 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
:3210 HTTP + WS · :27017 MONGODB · :8000 DYNAMODB

Network · 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.

export const send = action({  args: { body: v.string() },  handler: async (ctx, { body }) => {    await ctx.runMutation(internal.messages.write, { body });   // chapter 05    await nimbus.sessions.open({                                // chapter 13      target: { service: { name: "agent" } }, channels: ["stdio"],    });  },});
nimbus-server · nimbus-adapters · nimbus-engine

Compute · 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.

import { 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,});
V8 IN-PROCESS · ONLY ACTIONS REACH THE NETWORK

Compute · 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.

"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
NODE 22 · 24 · 26 · ACTIONS ONLY · NO NATIVE ADDONS

Storage · 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.

export const write = internalMutation({  args: { body: v.string() },  handler: async (ctx, args) => {    await ctx.db.insert("messages", args);  },});
DOCUMENT + INDEX + COMMIT LOG = ONE TRANSACTION

Storage · 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.

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

Storage · 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.

$ 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
SQLITE DEFAULT · POSTGRES · MYSQL · LIBSQL · REDB

Storage · 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.

services:  agent:    image: python:3.12    volumes:      - scratch:/work   # a named tenant volumevolumes:  scratch: {}           # no host bind mounts · S3 API on :9000
BLAKE3 · ENCRYPTED · S3 API :9000

Agents · 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.

services:  agent:    image: docker.io/library/python:3.12   # pulled and unpacked, no daemon    command: ["python", "agent.py"]    volumes: ["scratch:/work"]              # the volume from chapter 08    deploy: { resources: { limits: { cpus: "1", memory: 512M } } }    x-nimbus: { backend: krun, egress: { allow: [] } }   # microVM · chapter 10
LIKE A POD · OCI IMAGE · NO DAEMON · CRUN · LIBKRUN

Agents · 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.

services:  agent:    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 ENFORCES

Workloads · 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.

const agent = await nimbus.services.get({ name: "agent" }); // every write fences on the generation it readawait nimbus.services.restart({  name: "agent", sourceGeneration: agent.metadata.generation,});
LIKE A K8S SERVICE · SAME VERBS IN COMPOSE AND API

Workloads · 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.

services:  web:    image: ghcr.io/acme/web:1.4    ports: ["127.0.0.1:3000:8080"]   # host listener → guest port  agent:    image: ghcr.io/acme/agent:1.4    command: ["python", "agent.py"]  # a microVM by default  worker:    image: ghcr.io/acme/worker:1.4    x-nimbus: { backend: crun }      # a container instead
ONE SANDBOX PER SERVICE · REPLICAS PLANNED

Workloads · 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.

const shell = await nimbus.sessions.open({  target: { service: { name: "agent" } }, channels: ["stdio"],});console.log(shell.spec.targetSnapshot.service);// { name: "agent", generation: 3, backend: "sandbox" }console.log(shell.spec.expiresAt);   // 15 min out
LIKE KUBECTL EXEC · TTL 15 MIN · NO BYTES YET

Run · 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.

$ lsnimbus            # one file $ ./nimbus start  # :8080 · network, compute, storage
ONE BINARY · NO DOCKER · NO KUBERNETES · NO QUEUE

Run · 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.

$ curl -s -X POST http://localhost:8080/api/tenants \    -H "Authorization: Bearer $NIMBUS_TOKEN" \    -d '{"id": "acme"}'{"id": "acme"}   # own SQLite file · own blob store + key$ nimbus start --runtime-max-active-per-tenant 8
OWN DATABASE · KEY · BUDGET · 429 OVER BUDGET

Run · 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.

$ curl -fsSL https://github.com/nimbus/nimbus/…/install.sh | sh# write /etc/systemd/system/nimbus.service, then$ sudo systemctl enable --now nimbus $ nimbus deploy https://nimbus.example.com --dry-run
NO TELEMETRY · NO METERING · NO CLUSTERING YET

Run · 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.

# 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
IROH + OPENRAFT · QUIC UDP/7842 · RELAY TCP/443

Run · 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.

# 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
OPERATOR KEY · ADMIN · OP-FORWARD · NEVER A VOTER

Run · 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.

$ 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
00 / 19CODECLIENT CALL · api.messages.send
request000%