DEV Community

Cover image for I built a world you can rewind and fork — and forking it costs one database row
Kaviya Kumar
Kaviya Kumar

Posted on

I built a world you can rewind and fork — and forking it costs one database row

I wanted to ask a database a question it isn't supposed to answer:

What if this had happened instead — without re-running history from scratch?

Every system I build has the same blind spot. To test a different past you copy the data, replay everything, and pray it's deterministic. So I tried the opposite premise: don't store the world after computing it somewhere else — make the database itself BE the world.

The result is Loom: a deterministic simulation whose authoritative reality lives entirely inside Aurora DSQL. Three properties fall out of that one decision:

  • Each tick is one SQL transaction.
  • Time-travel is a query (just read a smaller tick).
  • Forking the universe is O(1) — one inserted row. Pre-fork history is shared, never copied.

🔗 Live demo: https://loom-ivory.vercel.app


The data model: copy-on-write, versioned state

Three tables. That's the whole engine.

  • branches — the multiverse tree. Forking = INSERT one row.
  • world_meta — the strongly-consistent "now" pointer per branch.
  • entity_state — copy-on-write, versioned world state. The table.

A branch only ever writes rows for ticks after its fork point. Everything before is read through its ancestors. So forking copies zero history.

CREATE TABLE entity_state (
  branch_id TEXT   NOT NULL,
  entity_id TEXT   NOT NULL DEFAULT gen_random_uuid()::text,
  tick      BIGINT NOT NULL,
  kind      TEXT   NOT NULL,        -- 'AGENT' | 'WORLD'
  state     JSONB  NOT NULL,
  PRIMARY KEY (branch_id, entity_id, tick)
);
Enter fullscreen mode Exit fullscreen mode

A tick reads the current frame, runs a pure nextState(prev) (no RNG, no wall-clock, no iteration-order surprises), and writes back only the entities that changed. At equilibrium a calm world writes ~1 row per tick instead of thousands — divergence becomes the only thing that costs storage.

The crown jewel: one query that is both a time machine and a multiverse

Reconstructing any frame of any timeline is a single recursive query. Vary the tick → you time-travel. Vary the branch → you cross into a parallel timeline.

WITH RECURSIVE lineage AS (
  SELECT branch_id, parent_branch_id, fork_tick, $2::bigint AS cap, 0 AS depth
    FROM branches WHERE branch_id = $1
  UNION ALL
  SELECT p.branch_id, p.parent_branch_id, p.fork_tick,
         LEAST(l.cap, l.fork_tick) AS cap, l.depth + 1
    FROM branches p JOIN lineage l ON p.branch_id = l.parent_branch_id
)
SELECT DISTINCT ON (es.entity_id) es.entity_id, es.kind, es.state
  FROM entity_state es
  JOIN lineage ln ON es.branch_id = ln.branch_id AND es.tick <= ln.cap
 ORDER BY es.entity_id, es.tick DESC, ln.depth ASC;
Enter fullscreen mode Exit fullscreen mode

LEAST(l.cap, l.fork_tick) caps each ancestor's contribution at the child's fork point, so a branch sees its ancestors' history only up to where it split off. DISTINCT ON … ORDER BY tick DESC, depth ASC takes the newest visible version of each entity, preferring the closest branch in the lineage. That one query is the engine.

The part I checked instead of claimed

A forked branch stores zero rows for everything before its fork point — I didn't want to assume it, so the proof endpoint measures it:

{ "branch": "drought", "isFork": true, "forkTick": 15,
  "rowsBeforeFork": 0, "sharedIdentical": true }
Enter fullscreen mode Exit fullscreen mode

rowsBeforeFork: 0, and readWorld('drought', t) === readWorld('root', t) for every t < 15. Forking reality in two cost one branch row + one meta row.

And it holds at scale: 10,000 agents, a stable world writing about one row per tick, any moment rebuilt in roughly 8 ms, and two full 10k-agent universes stored in about 91% less space than materializing each branch in full.

Two regions, one world

This isn't a laptop Postgres. It runs on a real multi-region Aurora DSQL cluster, active-active across Tokyo (ap-northeast-1) and Seoul (ap-northeast-2). Reading the same (branch, tick) from each regional endpoint returns byte-identical frames, and a fork written through one region shows up immediately in the other — strong global consistency, no replication lag. A region-proof script fingerprints both continents and asserts the hashes match.

DSQL is Postgres-wire-compatible but not Postgres, and a few things cost me time:

  • No DESC in index keys ("specifying sort order not supported") — use plain ascending and let the planner scan backward for the ORDER BY ... tick DESC.
  • Optimistic concurrency — a commit conflict comes back as SQLSTATE 40001, not a blocking lock. The per-branch ticker just retries.
  • ~10k changed-row transaction cap — which is exactly why copy-on-write (write only what changed) isn't just an optimization, it's load-bearing.

Hosting it: Vercel serverless → Aurora DSQL, with no static secrets

The frontend is Next.js on Vercel. The interesting part is how the deployed app talks to DSQL without me pasting an AWS key anywhere.

The Vercel AWS integration uses OIDC role assumption: each serverless function gets a short-lived OIDC token, assumes an IAM role, and those temporary credentials mint a DSQL auth token. The whole connection layer is a few lines:

import pg from 'pg';
import { DsqlSigner } from '@aws-sdk/dsql-signer';
import { awsCredentialsProvider } from '@vercel/functions/oidc';

const signer = new DsqlSigner({
  hostname: process.env.DDB_PGHOST,
  region: process.env.DDB_AWS_REGION,
  credentials: awsCredentialsProvider({ roleArn: process.env.DDB_AWS_ROLE_ARN }),
});

const pool = new pg.Pool({
  host: process.env.DDB_PGHOST, port: 5432, user: 'admin', database: 'postgres',
  ssl: { rejectUnauthorized: true },
  password: () => signer.getDbConnectAdminAuthToken(), // pg re-mints per connection
});
Enter fullscreen mode Exit fullscreen mode

DSQL IAM tokens are short-lived (~15 min), and pg calls the password function per new connection — so it transparently re-mints. The reconstruction query for the live timeline, the divergence chart, the "show me the SQL" receipts, the zero-rows proof — every panel on the hosted site is a real DSQL read, not an animation.

One gotcha worth knowing: the per-tick series chart did N sequential reconstructions, which is fine next to the cluster but brutal across an ocean from a function in another region. Running those reconstructions in parallel (Promise.all) collapses the wall-clock to roughly one round-trip.

Making it something you can feel

Numbers don't land in a demo, so the headline scenario is a concrete one: a small city of people who can all reach a hospital. I rewind, fork that exact moment, and inject one event — a flood that cuts the city in half. The south is stranded, resource access falls from 100% toward collapse… while the original timeline keeps flowing, untouched. Same seed, one changed event, two diverging futures — and history was never re-run.

That's the whole pitch: what-if stops being a thought experiment and becomes a row you insert. A substrate for digital twins, scenario modeling, and agent backtesting — run a thousand alternate histories of one decision on a single consistent engine and compare them.

The database isn't storing the world. It is the world — and you can rewind it, and fork it.

Top comments (0)