Volter World

Run a full stack

App, database and twins together, for browser and end-to-end work.

For a complete browser application with login, sessions and signed callbacks, start with test an app in the browser and its example source. The HTTP script below explains the service mechanics; it does not drive a browser. The previous recording is historical and is not evidence for these current instructions.

A world can own more than twins. Your app itself, a real local Postgres, a Redis: anything the app needs running is a service in world.json, brought up and torn down together, with each service's connection details in the env the next service sees.

The app

A server whose /signup creates a Stripe customer the way production code would, and an end-to-end script that drives the server, not the twin.

package.json
{ "name": "acme-web", "private": true, "dependencies": { "stripe": "17.7.0" }, "devDependencies": { "@volter/world": "3.0.63", "@volter/twin-stripe": "3.0.1" } }
.env.example
STRIPE_SECRET_KEY=
server.mjs
import { createServer } from 'node:http';
import Stripe from 'stripe';

const stripe = new Stripe(process.env.STRIPE_SECRET_KEY);

createServer(async (req, res) => {
  if (req.url === '/health') { res.end('ok'); return; }
  if (req.url === '/signup') {
    const customer = await stripe.customers.create({ email: '[email protected]', name: 'Ada' });
    res.setHeader('content-type', 'application/json');
    res.end(JSON.stringify({ customerId: customer.id }));
    return;
  }
  res.statusCode = 404; res.end('not found');
}).listen(Number(process.env.PORT), '127.0.0.1');
e2e.mjs
const res = await fetch(`${process.env.APP_URL}/signup`);
const body = await res.json();
if (!/^cus_/.test(body.customerId)) { console.error(body); process.exit(1); }
console.log(`signed up ${body.customerId}`);
npm install
npx volter world init --name acme-web --twins stripe

The app as a service

init wrote the stripe twin into .volter/world.json. Add the app as a process service: what to run, the env name its URL is exported under, and how the world knows it is ready.

.volter/world.json
{
  "id": "acme-web",
  "env": { "STRIPE_SECRET_KEY": "$issue:stripe" },
  "services": [
    { "id": "stripe", "type": "twin", "package": "@volter/twin-stripe", "version": "3.0.1", "port": "auto", "injectEnv": "STRIPE_TWIN_URL" },
    { "id": "app", "type": "process", "command": "node", "args": ["server.mjs"], "port": "auto",
      "injectEnv": "APP_URL", "portArg": false, "rootArg": false, "ready": { "httpUrl": "${url}/health" } }
  ]
}

Services start in order, and each one receives the env the earlier ones produced, so the app starts after its twin is ready and inherits STRIPE_TWIN_URL. A twin declared after co-located twins starts as soon as their addresses are known, since a twin calls nothing while it starts. With a readiness probe, up waits until the app answers, not merely until it binds.

npx volter world up
acme-web  1 twin up, story loaded
  stripe: http://127.0.0.1:56314
  app: http://127.0.0.1:56320

Drive the app end to end

npx volter world run -- node e2e.mjs
signed up cus_twin_1

The script talked only to the app. The app talked to Stripe, and the write is in the log:

npx volter world log
stripe customer.create customer:cus_twin_1
npx volter world down

A real database

A real local tool is an external service: the world runs its up, status and down, waits for it to be ready, and reads its connection details into the env:

{
  "id": "db", "type": "external",
  "external": {
    "up":     ["./dev/db", "up"],
    "status": ["./dev/db", "status", "--json"],
    "down":   ["./dev/db", "down"],
    "readyWhen": { "command": "./dev/db", "args": ["ready"] },
    "discover": [{ "as": "DATABASE_URL", "jsonPath": "url" }]
  }
}

npx volter world init emits this shape for a Postgres, MySQL, Redis or MongoDB it detects in your env names, with a definition the world manages. The world serves a Redis without a container: the redis twin speaks Redis's own protocol on the declared port, so ioredis, node-redis and BullMQ connect unmodified, and its keys are the world's state, branched and reset with it. A Postgres runs in a container, or, where there is no container runtime, as the machine's own PostgreSQL (a postgres server on PATH, or VOLTER_WORLD_POSTGRES_BIN), else through PGlite. MongoDB runs its own mongod server from PATH, loopback only, with its data kept under the world's state directory. The Supabase twin hands the application that managed Postgres connection. Your migrations and native Postgres client use the same database. Supabase's Management API, PostgREST, Storage and Auth are unsupported; installing the twin does not make Supabase SDK calls work. Apply your application's schema through its migrations; the twin does not install Supabase's Auth or Storage schemas.

With no container runtime and no PostgreSQL installed, the world serves that Postgres with real Postgres compiled to WASM (PGlite), behind a wire-protocol listener on the same port, with the same env. MySQL needs a container runtime and is refused without one.

MongoDB needs its own server binary, not a container or an extra twin package. The MongoDB example gives a pinned installation and an executed world lifecycle. The mongodb driver and mongoose use the injected connection URL unchanged. The server supplies its own queries, indexes and stored data. It starts as one standalone node; add command: ["mongod", "--replSet", "world"] to the definition's MongoDB service when the application needs transactions or change streams. The world initializes that one member and waits for its primary. Authentication, TLS, multiple members, failover, sharding and Atlas services are not configured. Database time follows the machine. A branch copies a stopped world's current data; reset starts fresh data. Database writes have no world log, checkpoint, push or deploy.

What the PGlite Postgres does and does not do:

  • Extensions. Every contrib extension PGlite ships and pgvector are available, so migrations' CREATE EXTENSION IF NOT EXISTS pgcrypto | citext | "uuid-ossp" | unaccent | pg_trgm | btree_gist | hstore | ltree | fuzzystrmatch | vector | … work. Others (postgis, pg_cron, timescaledb) fail with Postgres's "is not available" error.
  • One declared database and login. Use the database name, username and throwaway password configured in the World's database definition. An undeclared database is refused with 3D000, and incorrect credentials with 28P01. current_database() names the declared database. CREATE DATABASE always fails: for the database your connection named it answers 42P04 ("already exists", which is true, so rails db:create proceeds), and for any other name 0A000 (not supported). Nothing can make a second, separate database, so a Prisma shadow database (prisma migrate dev) or a test runner's test_<name> database needs a container runtime; prisma migrate deploy does not.
  • No bulk load over COPY. COPY … FROM STDIN (psql \copy, pg_restore data, copy streams) is refused with 0A000; load rows with INSERT. COPY … TO STDOUT works.
  • One serialized session. Connections take turns, and an open transaction blocks the others until it ends. They share one session: SET, temp tables and SQL PREPAREd statements leak between connections, session advisory locks do not exclude each other, and LISTEN/NOTIFY does not reach across connections. A statement a driver prepares over the protocol stays its connection's own, even when two connections pick the same name (Prisma's s0), and its own SQL EXECUTE or DEALLOCATE of that name reaches it.

Browser tests

The browser is not a Node process, so the injector does not reach it. Two ways in:

  • Server-side calls. Most apps call vendors from the server. The server runs inside the world and is redirected; the browser talks only to your app, as the end-to-end script above did.
  • Browser-side calls. Put the browser proxy shipped with the kernel in front of the app, so the browser's SDK calls share the same twins. Configure the browser with the World's proxy (VOLTER_WORLD_PROXY) and trust its session CA (VOLTER_WORLD_CA); a Node injector alone does not redirect requests made inside a browser. Load a named browser shows how to use the World's saved browser state and proxy. This needs no platform source checkout.

Then run the browser suite inside the world: npx volter world run -- npx playwright test.

What is real here

The Clerk twin issues RS256 tokens against a real JWKS, and the S3 twin verifies SigV4 signatures. Native SQL queries run in the World's Postgres. These mechanisms do not establish support for every vendor authentication or permission flow; check the selected release's limitations. Generative twins return labeled deterministic stubs; assert on the calls and state changes, not the prose.

Runnable examples

The cookbook holds complete stacks you can copy: an on-call agent across GitHub, Jira, Slack and OpenAI, and open-source applications (Postiz, Twenty) served whole in a World.

View Markdown source

On this page