better/compute
Concepts

Architecture

The planes and components of Better Compute, the api, gateway and worker process modes, and how joins, reservations, bare metal and VPS flow through them.

@better-compute/server@better-compute/gateway@better-compute/cliPLAN.mdindex.tsoptions.tsREADME.mdREADME.mdserve.tsgateway.tsREADME.md

Better Compute separates who may ask for something from where the answer is kept and from the machines that carry it out. Requests arrive over HTTP and are decided once by Better IAM; the answer is written to PostgreSQL; daemons learn about it through long-lived sessions they opened themselves. This page describes those parts and how the four main flows move through them.

Planes

PlaneComponentsTrust
InterfaceThe typed client, the CLI, MCP tools, the console kit, webhook receiversUntrusted callers holding Better IAM credentials
IdentityBetter IAM, embedded in the api and worker processes or remote, plus Better Compute's IAM pluginTrusted
Controlapi: stateless HTTP, admission, JoinService, the secret broker. gateway: daemon sessions and tunnels. worker: reconcilers, placer, workflows, event sinks, usage, IAM jobs, the CommandSweeperTrusted; the gateway only relays
DataPostgreSQL (with PgBouncer in transaction mode), an object store, a time-series database, a KMS or VaultTrusted
Sitebcd site: proxy DHCP, TFTP, iPXE and HTTP boot, image cache, BMC access; one active boot server per siteHigh value; secrets are sealed per request
Node and ramdiskbcd node with its runtimes and CDI; bcd ramdisk during commissioningTrusted while the platform holds root; the ramdisk gets a restricted certificate of at most 2 hours

Components

  • PostgreSQL holds nodes, sessions, devices, bookings, reservations, commands, the work queue, the outbox, leases and the authorization projection. It is the only place state is authoritative.
  • Gateways hold daemon sessions in memory and write liveness to the database in batches. They sit behind an L4 load balancer with TLS passthrough, because mutual TLS terminates in the gateway.
  • Workers claim work from the database with SKIP LOCKED under shard and pool leases. Optional pokes (a notifier) wake gateways sooner, but every loop also converges without them.
  • Telemetry goes from the daemon or dcgm-exporter to a time-series database over OTLP or remote write, never through PostgreSQL or the session.

The control plane in your process

betterCompute(options) is synchronous and side-effect free: it validates options (INVALID_CONFIG otherwise) and registers system, the server's own groups, the built-in node controllers and the always-on inventory and capacity modules, then your modules and plugins. Nothing touches the database or Better IAM until you call it.

MemberWhat it is
apiEvery method as (credential, input), through the same envelope as HTTP. It also drives the typed client's inference.
handlerThe fetch transport: (request: Request) => Promise<Response>.
nodeHandlerThe Node.js transport, for createServer(compute.nodeHandler).
gatewayServicesJoinService's join and renew and the secret broker, consumed by createGateway.
initialize()Checks every method's IAM action against the merged catalog, runs migrations, anchors the clock and awaits each module's start(). Idempotent.
start({ roles }), stop()Run and stop the worker roles on timers, under their leases.
jobsOne deterministic pass per tick(), for tests and the simulator.
eventsIn-process subscribers of committed events.
context, registry, caches, authz, selfCheck(), endpointThe module context, the method registry, cache invalidation, the projection, read-only health checks, and where the handler is mounted.
server.ts
import { createServer } from 'node:http';
import { compute } from './compute';

await compute.initialize(); // registry checks, migrations, module start()
createServer(compute.nodeHandler).listen(3000); // POST /api/compute/{group}/{method}
await compute.start({ roles: ['controllers', 'sinks', 'iam'] });

The server package never imports the store, the IAM bridge, the gateway, or the injected provision, metal and vps modules; they arrive through options, and scripts/check-deps.mjs fails CI if that changes.

Process modes

One configuration runs in any process mode, so you scale each part separately against one database. With the CLI, better-compute serve --roles picks the mode:

ModeRunsScales byserve --roles
apiStateless authentication and authorization, drafts, the admission transaction with inline placement, the outbox writes, JoinServiceRequests, behind an L7 load balancerapi
gatewayDaemon sessions in memory, liveness, dispatch, result and event ingestion, Join/Renew/Secret hand-off, tunnelsStreams (at least three per region planned)gateway
workercontrollers (reconcilers, workflows, the CommandSweeper), sinks (outbox dispatch), iam (Better IAM's jobs under singleton leases), scheduler256 shards spread over the workerscontrollers,scheduler,sinks,iam
allThe API and every worker role in one process, plus the gateway when the configuration's host.gateway is setNothing; for development and small installationsall (the default)
better-compute migrate                                                    # once per deploy
better-compute serve --roles api --port 3000 --no-migrate
better-compute serve --roles controllers,scheduler,sinks,iam --no-migrate
better-compute serve --roles gateway --no-migrate

Leases and shard ownership keep every job single-writer, so you can run several processes of each mode. The scheduler role runs the reconcilers that declare role: 'scheduler' (placers), under their own rendezvous membership (worker rows scheduler/<id>, leases scheduler/shard:<n>), so a scheduler-only process never takes node shards from the controllers and each role spreads the 256 shards over its own processes. The pool-lease placer (capacity.placer, used with capacity: { placer: 'pool-lease' }) is such a reconciler, so with that placer at least one process must run the scheduler role. See deployment and scaling.

Nodes only dial out

A daemon opens no listening port. It dials gw.<region>:443 and keeps one HTTP/2 connection carrying the Session stream and, from v0.3, on-demand Tunnel streams. Mutual TLS terminates in the gateway, which checks the certificate chain, revocation and role, and that the SPIFFE ID matches the node id in Hello.

The gateway package holds no CA and no signing key. It hands Join, Renew and SecretRequest to gatewayServices, the control plane's JoinService and secret broker, through dependency inversion. The design puts those in the api process behind internal mutual TLS; the serve --roles gateway host of this release constructs them in the gateway's own process from the shared configuration. Either way, a compromised gateway can delay or drop frames but cannot forge what matters: renewals need proof of possession of the node key, join tokens are sealed to JoinService's key, secrets travel as JWE to a per-request key, and attested results carry the node's signature. See the gateway and security.

The four flows

A daemon joins (v0.1)

  1. An operator mints a join token with joinTokens.create (compute:join-tokens:create on the pool). The token, bcjt_<id>.<secret>, is shown once; only a hash is stored.
  2. The install script puts bcd, its pinned configuration and the JoinService encryption key on the server.
  3. bcd join sends Join with the token sealed to JoinService's key, a CSR and hardware claims.
  4. JoinService runs the attestor chain, computes the hardware key, enforces the token's expect and reclaim rules, inserts the node as adopting, decrements the token and signs a 24-hour SPIFFE certificate.
  5. The daemon opens its Session; the gateway bumps the session epoch and asks for inventory sections.
  6. The inventory controller derives devices and conditions; the lifecycle controller runs the pool's acceptance checks (diag.nvml). On pass the node is adopted and receives desired-state generation 1.

No step calls Better IAM. See joining.

A user or agent reserves GPUs (v0.1)

This is the admission sequence every request-as-resource create follows:

A caller outside the input's tenant, and not in the root tenant, gets ACCESS_DENIED before any database access. A repeated idempotency key from the same identity replays the stored result; a different request under the same key is CONFLICT. The placer then books atoms in the second transaction, and the reservation binds through the node's desired state. See reservations and placement.

Bare metal from PXE to ready (v0.2)

A site agent enrolled with a site token holds the site's boot lease and serves proxy DHCP and iPXE. metal.enroll stores the BMC credentials as a secret reference; the machine is inspected over Redfish, PXE-booted into bcd ramdisk, diagnosed and wiped (node-signed results), and reaches ready. metal.deploy streams an image to disk, writes cloud-init with a single-use token, and kexecs into the installed system, which rejoins as the same node. The control plane for this flow has landed. bcd site and bcd ramdisk are written but not released and covered only by Go unit tests, so the flow's tests run against simulated agents. See bare metal.

A VPS instance (v0.3)

instances.create follows the same admission sequence on compute-instance. On a hosted pool (class: 'vps-host') the placer claims vCPU, memory, disk and optionally a GPU, and the instance joins the node's desired state for the daemon to run. On a provider pool (class: 'provider', agent: 'none') a workflow calls the provider's API, and no platform agent ever runs inside a tenant's VM. Later operations change only the instance's desired state. See VPS.

Was this page helpful?

Better Compute is created by Sean Filimon

Last updated

On this page