Volter World

Keep app data in a managed local database

Use a Supabase-issued local connection with the unchanged node-postgres client. Create a table, write and read an app record, then stop and resume the same database. This release supplies a World-managed PostgreSQL connection. Supabase's HTTP data, Auth, Storage and Realtime APIs are outside this example and are not established by a successful SQL query.

Use Node 22.6 or newer, npm and registry access during installation. Start in an empty directory. No Supabase project, real credential or platform login is needed.

Select the database workflow

package.json
{
  "name": "local-managed-database",
  "private": true,
  "type": "module",
  "dependencies": { "pg": "8.16.3" },
  "devDependencies": {
    "@volter/world": "3.0.158",
    "@volter/world-core": "3.0.148",
    "@volter/world-runtime": "3.0.156",
    "@volter/world-console": "3.0.149",
    "@volter/twin-supabase": "1.0.3"
  }
}
.env.example
SUPABASE_DB_URL=
npm install --no-audit --no-fund
./node_modules/.bin/volter world init --name local-managed-database --twins supabase --source supabase=@volter/twin-supabase

Review Supabase and its required database backing in .volter/world.json. The World starts that backing and issues a throwaway connection URL. Keep the config and lockfile with your app; do not start or stop its database separately.

Init may also list the generic pg dependency with an unresolved endpoint. That static detection does not replace the declared backing or the issued connection. The app's returned row and resumed readback below show what this connection did.

Store and read an app record

records.mjs
import pg from 'pg';
if (!process.env.VOLTER_WORLD || !process.env.SUPABASE_DB_URL) {
  throw new Error('Run this app through its World to receive the database connection');
}
const client = new pg.Client({ connectionString: process.env.SUPABASE_DB_URL });
await client.connect();
try {
  if (process.argv[2] !== 'read') {
    await client.query('CREATE TABLE IF NOT EXISTS signup (id text PRIMARY KEY, email text NOT NULL)');
    await client.query('INSERT INTO signup (id,email) VALUES ($1,$2) ON CONFLICT (id) DO NOTHING',
      ['new-user', 'new-user@example.test']);
  }
  const { rows } = await client.query('SELECT id,email FROM signup WHERE id=$1', ['new-user']);
  console.log(JSON.stringify(rows, null, 2));
} finally {
  await client.end();
}
./node_modules/.bin/volter world up
./node_modules/.bin/volter world run -- node records.mjs
./node_modules/.bin/volter world down
./node_modules/.bin/volter world up
./node_modules/.bin/volter world run -- node records.mjs read
./node_modules/.bin/volter world status
./node_modules/.bin/volter world down
[
  {
    "id": "new-user",
    "email": "new-user@example.test"
  }
]

The resumed command should print that same row. It only reads and cannot recreate a missing record; an empty result would print []. down retains state, while reset deliberately reloads starting data. The selected database backing owns its persistence; SQL queries are not a promise that every native backing effect appears as a vendor-log entry.

This workflow establishes connection issuance, table creation, a parameterized write/read and retained readback with these exact dependencies. Compare the release's supported scope in the catalog before using another client or API. For an app with its own database requirements, follow full-stack setup.

Try a change in a branch

The executed local backing for this example is PGlite. It copies the stopped baseline's current database data into a branch. Only one of these branches runs at a time; checkout stops the current branch before resuming the selected one. Native SQL data does not have vendor-log positions for historical --at branches, push or deploy. A different database backing needs its own supported scope; this example does not establish container-backed copying.

Starting from the stopped baseline above, create preview.mjs:

preview.mjs
import pg from 'pg';
if (!process.env.VOLTER_WORLD || !process.env.SUPABASE_DB_URL) {
  throw new Error('Run this app through its World to receive the database connection');
}
const client = new pg.Client({ connectionString: process.env.SUPABASE_DB_URL });
await client.connect();
try {
  await client.query('UPDATE signup SET email=$1 WHERE id=$2',
    ['preview-user@example.test', 'new-user']);
  const { rows } = await client.query('SELECT id,email FROM signup WHERE id=$1', ['new-user']);
  console.log(JSON.stringify(rows, null, 2));
} finally {
  await client.end();
}
./node_modules/.bin/volter world branch signup-preview
./node_modules/.bin/volter world run -- node records.mjs read
./node_modules/.bin/volter world run -- node preview.mjs
./node_modules/.bin/volter world checkout local-managed-database
./node_modules/.bin/volter world run -- node records.mjs read
./node_modules/.bin/volter world checkout signup-preview
./node_modules/.bin/volter world run -- node records.mjs read
./node_modules/.bin/volter world down

The first read prints new-user@example.test. The preview mutation prints preview-user@example.test; returning to local-managed-database prints the original email. Returning to signup-preview prints the preview email again, without another write. Both branches retain their own data after stopping. Keep the baseline while its child references it; ordinary down stops compute without discarding either branch.

View Markdown source

On this page