Volter World

HTTP API

What a twin and a served world answer beside the vendor's API. Everything a vendor's SDK does goes through the vendor's own paths; these endpoints are the world's own.

A twin

Every running twin answers these on its own URL, whatever the vendor.

endpoint answers
GET /twin the twin's manifest: its vendor, version, protocol, what it models, the matchers its handlers accept, and whether it has a root
GET /twin/scenario the handlers in force, how many times each matched, and the requests none matched
GET /twin/store/<name> a named, deterministic projection of the twin's state. The only way a UI or a tool reads twin state; the names are per twin

There are no write endpoints beside the vendor's API. State changes through the vendor's paths; behavior changes by editing the handlers file and restarting.

A served world

volter world serve puts a world at http://<host>:<port>/<org>/<world>/. Each twin's vendor API is under its vendor id, so an app pointed at …/acme/team/github is an app pointed at that world's GitHub twin, and, when the twin has a root, at GitHub with the keys hidden. The world's own endpoints are under /-/<org>/<world>/.

GET /<org>/<world>/.well-known/volter-world answers a token holder with the world's attach manifest: { name, vendors: { <vendor>: <url> }, ca, proxy, env, streams? }. env holds VOLTER_TWINS_KEY and, for a twin whose client is pointed by an endpoint variable (TUNNEL_SERVER_URL, QSTASH_URL), that variable at the twin's URL. An app started with VOLTER_WORLD=<the world's URL> and VOLTER_WORLD_TOKEN=<token> under @volter/world-core/attach, or by volter-world attach <the world's URL> --token <token> -- <command>, reads it, and its unmodified SDKs reach the world's twins. A request the app addresses to the world's own paths carries the token as x-twins-key.

A twin whose clients speak a TCP protocol is reached through a stream: streams names each one (smtp; planetscale-mysql, PlanetScale's MySQL wire over the same state as its HTTP API) with the WebSocket that carries it and the variables its client reads (SMTP_HOST/SMTP_PORT…, or PLANETSCALE_MYSQL_TWIN_URL). The attacher listens on loopback for each and sets those variables to the listener, so nodemailer, mysql2 or Python's smtplib connects to 127.0.0.1 unmodified (@volter/world-core/stream-bridge); an app that reads its MySQL URL under another name is given PLANETSCALE_MYSQL_TWIN_URL there. The WebSocket is /-/<org>/<world>/streams/<id>; its first message is the world's token (the read token opens no stream), and after it each message is bytes of the connection, both ways.

Credentials are tokens in the x-volter-token header. A served world has two: the token, which opens everything, and a read token, which opens only GET on the vendor API and the endpoints marked below. serve prints both and writes them to .volter/token. The vendor API also takes the token where an app presents it: in x-twins-key, which the injector sends from VOLTER_TWINS_KEY so the app's own Authorization stays the vendor's, or as the app's API key (Bearer <token>, token <token>, or the password half of Basic).

Every answer on the vendor API of a twin that has a root carries x-volter-observed-at: the instant of the last completed refresh. It is absent until the first refresh completes, so an app reading the copy can always tell how old the copy is.

The history

endpoint read token answers
GET …/log/<twin>?after=<position> yes the world's log for that twin from a position: what volter world fetch reads
GET …/checkpoint/<twin>[?at=<position>] yes the twin's state at a position, the latest by default: what clone starts from
GET …/changesets and GET …/changesets/<name> yes the changesets that landed, with their receipts, verification and approvals, its stored contentHash, and currentHash, the body's hash as it stands (an approval or verification made on another hash is for an earlier version; a stored hash that differs from it is a changeset whose body changed outside the World, which approval refuses)
GET …/requests?hours=24 yes the world's request report over the period: per twin, the requests by status class, the response time's p50 and p95, and the top routes

Pushing and deploying

endpoint answers
POST …/push with a changeset as the body append the changeset's changes to the world's log if its base is current, else 409 with the drift. Under deploy: auto the receipts are in the answer; otherwise each change is landed
POST …/changesets/<name>/verify run the world's checks over the changeset and record the result
POST …/changesets/<name>/approve { as, note? } sign the changeset's current hash; a browser session signs as the person it was opened for (a platform's pass; this machine on a local World) and its as is not read
POST …/deploy[/<name>] perform landed changes against each twin's root, by its policy; receipts in the answer. A browser's session (no token presented) deploys only what the body names: { "confirm": "<name>" } (the changeset, or the world's name for all), else 400

A twin's root

endpoint answers
GET …/twins every twin of the world, each as its status endpoint answers it (read token)
GET …/twins/<vendor>/status the twin as a consumer sees it: protocol, root, whether a credential is sealed, the last refresh, the position, the last performed entry's receipt
GET/PUT …/twins/<vendor>/root { url, deploy, refresh } as in the config; PUT with null clears it
PUT …/twins/<vendor>/credential seal the vendor's real credential. Never readable back; GET answers only that one is sealed, and when. A signingSecret field enables webhook ingest
GET/PUT/DELETE …/twins/<vendor>/scenario a generative twin's scenario: the handlers document it answers its generative surface from, read again on every request, so a PUT is live on the next call. A hosted world keeps it in its own state; a local world writes the file its service declares as colocate.scenarioPath. 404 for a twin that takes none
POST …/twins/<vendor>/refresh[?force=1] observe the root now: the twin observes, the kernel folds what changed; throttled to the root's refresh.atMost (else the twin's) unless force=1
POST …/session, DELETE …/session a browser session for this world in the presented token's scope: an HttpOnly cookie holding an opaque session id (never the token), which the vendor API and the World's endpoints accept like the token. Sessions last 30 days and end when the tokens rotate; DELETE ends this one
GET …/origin, PUT …/origin { url, token } the world this one branches from: PUT names it (https://<host>/<org>/<world>, the token that opens it) and brings its history in, as volter world clone does
POST …/origin/pull fetch what the origin has and move this branch onto it, naming conflicts by record and field, as volter world pull does
POST …/changesets { message, name? } cut the unpushed changes into a changeset, as volter world changeset does
POST …/origin/push push every unpushed changeset to the origin, oldest first; each answers with its receipts, as volter world push does
GET …/links, PUT …/links/<name> { origin }, DELETE …/links/<name> a link: /<org>/<world>/<name>/… forwards to the vendor at origin (https, a hostname) with the credential sealed as …/twins/<name>/credential, whose headers replace any of the same name the caller sent. Stateless: no log, no tree. The caller presents the world's token
GET …/checks, PUT …/checks/<name>, DELETE …/checks/<name> the world's checks: JavaScript exporting check (or default) as { name, run(entry, tree) }, run over every entry before it is performed. PUT refuses a file that exports no check. A hosted world runs each check in an isolate of its own, with no network and nothing of the world's
POST …/twins/<vendor>/ingest a vendor webhook, verified with the sealed signing secret, folded into the log. Keyless; the vendor's signature is the credential

Every served world is capped: request bodies to 4 MB, checkpoints to 24 MB, and a request budget answered with Retry-After.

Looking into a world

What a person steps into a world through: each vendor's own UI, what the world holds, what happened in it, its clock and its branches. Every host answers these alike (volter world serve, volter world view, world-host, a hosted world); the console reads nothing else. The model is Viewing a World.

endpoint read token answers
GET /<org>/<world>/<vendor>/… opened as a page yes the vendor's own screens (the twin's mirror) at that very address, when the twin does not serve that page itself (an OAuth screen): a browser navigation or frame (Sec-Fetch-Dest: document or iframe) that the twin answers with an error or non-HTML gets the mirror's shell, so a mirror's routes are real, reloadable URLs
GET /<org>/<world>/<vendor>/mirror/ keyless the mirror's shell (opened as a page, it moves to the vendor's address above); mirror/assets/<file> its client. GET only; 404 for a twin without one
POST …/session, DELETE …/session yes a browser session in the token's scope, { world, scope: "read" | "write", origin? }. Where the host gives the world an origin of its own (<world>--<org>.localhost:<port> locally, <world>--<org><WORLD_ORIGIN_SUFFIX> hosted) the session lives only there, in a host-only cookie, and asked for elsewhere answers 409 { origin }. A request the session carries that is not a read must say Sec-Fetch-Site: same-origin. A World served on this machine's loopback with an origin of its own (volter world view, volter world serve) opens a write session with no token for its own page: POST …/session with nothing presented, a loopback Host, an Origin equal to that origin and Sec-Fetch-Site: same-origin, answered { world, scope: "write", local: true, origin }. A read session browses every mirror: its reads pass, and a twin that enforces the read scope refuses its writes (at any other, its non-GET requests are refused). A named key opens no session, except one that may only read: that is how a shared read-only link opens the World's pages, and its session reads only, lasts no longer than the key and ends when the key is revoked.
POST …/session with x-volter-pass — a person a trusted platform signed a pass for (--trust, TRUSTED_ISSUERS), from the world's own page (Sec-Fetch-Site: same-origin) and only where the world has an origin of its own: a session of the pass's scope for 12 hours, { world, scope, who, issuer, origin }. A pass is spent once; one for another world or origin, expired or from an untrusted platform answers 401 with the reason
GET …/keys, POST …/keys, DELETE …/keys/<id>, DELETE …/keys?person=<subject> no the world's named keys, each for what holds it (an app, a CI job), managed with write access by the world's token or a person's browser session, never by a key: POST { name, scope?: "write" | "read" } answers the key (tok_k_…) once with its id; the world keeps only its hash and who made it, and a key opens the vendor API and the World's endpoints as the token of its scope does, never handing back the world's token. With the world's token, POST also takes for (the person it is for), expiresAt and replace: true (the key of that name for that person goes). GET lists { keys: [{ id, name, scope, createdAt, lastUsedAt, expiresAt, for, createdBy }] }; DELETE revokes one alone; DELETE ?person= (the world's token) revokes every key made by or for a person. New tokens (rotate) end every key
GET …/map yes every twin and what it holds: { twins: [{ twin, position, mirror, root, resources: [{ type, count }] }] }
GET …/timeline?limit=&before=&twin=&trace= yes the twins' logs merged, newest first, by each entry's occurredAt, then twin, then position: { entries: [{ twin, position, entry }], next }. next is the before cursor of the page after; twin narrows to one twin, trace to one W3C trace id. Entries at one instant are not ordered finer than the clock
GET …/diff yes this world's own changes since its base, what a changeset would cut: { base: { id, at }, changes: [{ twin, entry }] }
GET …/clock yes { at, frozen }: the instant every twin stamps from, or the wall clock when none is set
PUT …/clock { at }, POST …/clock/advance { by: "<N>(s|m|h|d)" } set or advance it. 409 when it would move back: before its current instant, or, unset, before the newest entry. Advancing needs a set clock. Twins catch up on their next request
GET …/history?at=<instant> yes each twin's history cut at the instant, { views: { <twin>: { view, position } } }: what a branch as of that instant clones
GET …/branches yes the host's branches of this world: [{ name, from, at, createdAt, expiresAt }]. 404 where the host makes none (volter world serve)
POST …/branches { at?: { instant }, ttl?, live?, label? } a branch as of the instant (now, without one): another world, cloned from this one's history cut there, its clock frozen at the instant (live: true, for a branch as of now only: its clock keeps this world's time, as a pull request's preview does; label names the branch <world>-<label>-<4 hex>), removed after ttl seconds (60 to 2592000) when given. 201 { name, token, readToken, expiresAt }
DELETE …/branches/<org>/<world> remove one of this world's branches

On the vendor wire, the read token's GET and HEAD pass as before, and anything else is refused, except at a twin whose manifest names requestScopes: ["read"]: its other requests reach it with x-volter-read-only: 1, and the twin refuses the ones that write (an append, stored bytes, a git ref) while the vendor's own moves (a renewal falling due) still happen. A twin records the incoming W3C traceparent on the entries a request causes and carries it, continued, on the webhooks those entries cause; the timeline's trace follows it.

The hosting product

@volter/world-host serves many worlds under one URL by <org>/<world>, each with its own tokens. It mounts every bare world under a directory (<dir>/<org>/<world>/, made by volter world init --bare <org>/<world>) and answers the names of its worlds at GET /-/ping, unauthenticated.

Its own endpoints open to the admin token, minted once and kept at <dir>/.volter-host/admin, printed by volter-host serve. The admin token opens nothing under a world, and a world's token opens nothing here.

endpoint answers
GET /-/worlds every world: { org, world, name, base, owner?, token, readToken, twins: [{ vendor, protocol, root }] }
GET /-/console/ the console, when @volter/world-console is installed beside the host: one page for every world above, reading these endpoints and a world's with the token you type
POST /-/worlds { org, world, vendors, owner? } a new bare world, mounted at once, with its two tokens; owner records the org that owns it, as its platform names the org
PUT /-/worlds/<org>/<world>/owner { owner } record the owning org of a world made without one (a claim); 409 when another org is recorded
GET /-/vendors the twins this host can make a world with: { vendors: [vendor, …] }
POST /-/worlds/<org>/<world>/rotate new tokens; the old ones die with the response
DELETE /-/worlds/<org>/<world> remove the world and its tree

A world served by the host answers exactly what a world served by volter world serve answers.

Hosted on Cloudflare

apps/cloud is the same hosting product as a Cloudflare Worker: one Durable Object per world, each world in an isolate of its own, its state in the object's SQLite and its blobs in R2. A world answers exactly what a served world answers, and the admin endpoints answer as a world-host's do (the same inventory row, the admin token as x-volter-token or a bearer), so a platform provisions onto either. The admin token is the Worker's ADMIN_TOKEN secret.

endpoint answers
GET /-/worlds every world as an inventory row: { org, world, name, base, owner?, token, readToken, twins: [{ vendor, protocol, root }] }
POST /-/worlds { org, world, vendors, from? } with from: "<org>/<world>", a branch of that world: its origin is the parent, at the parent's current position. A world with branches is not removed
POST /-/worlds { org, world, vendors, token?, readToken?, owner? } a new world, answered as its inventory row (owner: the org that owns it); 409 when the name is taken. token/readToken adopt a credential the world's callers already hold (24-256 URL-safe characters) instead of minting one
POST /-/worlds/<org>/<world>/rotate new tokens, { name, token, readToken }; the old ones are refused from the next request
PUT /-/worlds/<org>/<world>/owner { owner } record the owning org of a world made without one (a claim); 409 when another org is recorded
GET /-/vendors the twins this host can make a world with: { vendors: [vendor, …] }
DELETE /-/worlds/<org>/<world> remove the world: its state, its blobs, its refresh schedule
PUT /-/worlds/<org>/<world>/import { twin, files } place a twin's state files, { ".volter/world/<state>/<file>": content }: moving a v1 twins-cloud namespace into a world
GET /<org>/<world>/<vendor>/mirror/, …/mirror/assets/<file> the twin's mirror page and its bundle, keyless. The page's base is the twin's place in the world, so its reads go to the world's vendor API with the browser's session for that world (POST /-/<org>/<world>/session)

A rooted twin refreshes on the world's own schedule (root.refresh.every, else the twin's), and its sealed credential is wrapped by the Worker's SEALING_KEY secret. A Worker accepts no inbound TCP connection, so the TCP twins (smtp, PlanetScale's MySQL wire) are reached through their streams; a twin that answers a WebSocket upgrade on its vendor API (tunnel's control socket and its visitors' sockets) answers it here too. A world names the code it ran on in x-volter-world-code on the answers of its vendor API and endpoints (not on an upgrade). Not hosted: the real engines a twin can run locally beside its twinned control plane (fly's Docker execution plane, livekit's server and egress), which a hosted world serves virtually.