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
{
"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"
}
}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-supabaseReview 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
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:
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 downThe 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.