Chris Collins

Tools · infra

Postgres playground

Real PostgreSQL 18 in your browser (PGlite) with a psql-like console, twenty runnable labs for the distributed patterns Postgres does well — SKIP LOCKED queues, advisory-lock mutexes, leases with fencing tokens, leader election, LISTEN/NOTIFY, outbox, idempotency keys, rate limits — an animated model of concurrent sessions, and a real-server mode that races sessions on your own Postgres.

Added · updated 28 October 2026

Works with

How to use it

This is PostgreSQL itself — version 18, compiled to WebAssembly by PGlite — running in a Web Worker in this page. Nothing is installed and nothing leaves the browser. The same console can also talk to a real Postgres server through a small bridge you run, with several genuinely separate sessions, which is what you need to watch locks, SKIP LOCKED and LISTEN between sessions really happen.

The Patterns tab is the heart of it. People keep (re)discovering that a lot of coordination infrastructure — job queues, distributed locks, leader election, pub/sub, outboxes, idempotent APIs, rate limits, schedulers — can be a few tables and functions in the database you already run, with transactions making them correct. Each lab explains a pattern, why Postgres fits it, where it stops fitting, and lets you run it.

How to use it

Console. Type SQL and press Ctrl/⌘+Enter (runs the selection, or everything); Ctrl/⌘+Shift+Enter runs the statement at the cursor. Each statement gets its own block: a sortable results grid (click a header), Copy CSV / Markdown / JSON / Text and downloads, notices, notifications, errors with a caret under the position and the SQLSTATE explained, and the time taken. The prompt shows the transaction state like psql: postgres= idle, postgres* inside a transaction, postgres!# in a failed one.

  • Meta-commands work as in psql, implemented with catalog queries: \d name, \d, \dt[]`, `\di`, `\dv`, `\dm`, `\ds`, `\df`, `\dn`, `\dx` (`\dx lists every extension available), \du, \dT, \dp, \l, \sf, \x (expanded display), \timing, \conninfo, \echo, \h and \?. Patterns like \dt shop. and \df take work.

  • Explain runs EXPLAIN (ANALYZE, BUFFERS) on the statement at the cursor and draws the plan as a tree with a bar for each node’s own time; Text / tree switches to the raw plan, and Explain plan ↗ opens it in the Explain plan tool for the problems, advice and a before/after comparison.

  • Tab completes keywords, functions, table names (schema-qualified outside public) and the columns of tables you have mentioned; table. completes that table’s columns.

  • ↑ / ↓ at the start or end of the editor walk through the history. Format uses the SQL formatter's library.

  • + Session opens sessions B, C …; Side by side shows them in columns. On a real server each session is its own connection (its own backend PID), so you can BEGIN and lock a row in A and watch B block on it, or LISTEN in B and NOTIFY from A. Cancel sends a real CancelRequest.

Patterns. Pick a lab on the left. Run setup creates its tables and functions; then run the steps in order, or Open the whole lab in the console. Labs that are about concurrency have an animated model of sessions A, B, C on a timeline — what each runs, who holds which row or advisory lock, who waits, commits, deadlocks or gets a serialization failure — that you can play, step through event by event, or jump to the end. In your application shows the same pattern as worker code in Python (psycopg 3), Go (pgx) and Node (node-postgres). On a real server, Race it opens several connections and runs workers against each other for real.

Datasets loads small sample databases, each in its own schema: a shop (customers, products, 2,000 orders), an org chart, a road graph, documents for full-text search, 500 embeddings for pgvector, and 80,000 sensor readings.

Docs is a compact SQL reference — syntax, a summary, a runnable example and a link to the manual page for the version you are connected to. History keeps what you ran, per connection, in this browser; search it, run an entry again, export it. Statements that set passwords are never kept.

⛶ Full screen (or F outside the editor) gives the console the whole screen — the browser’s full screen where it is allowed, otherwise a full-window layer (iPhone); with Side by side the sessions sit next to each other at full height. The connection panel starts hidden; ◫ Panels shows it. Escape leaves.

The engine: PGlite

PGlite 0.5.10 is PostgreSQL 18.6 compiled to WebAssembly (about 4 MB compressed, cached after the first visit), loaded from jsDelivr at a pinned version. It runs in a module Web Worker so a slow query never freezes the page; the page speaks the PostgreSQL wire protocol to it, exactly as it does to a real server, so results look the same either way.

  • Keep the database in this browser stores it in IndexedDB. Export saves the whole data directory as a .tar.gz; Import loads one back; Reset starts empty.

  • Extensions: loaded at start (small): pgvector 0.8, pg_trgm, btree_gist, btree_gin, ltree, hstore, fuzzystrmatch, uuid-ossp, citext, tablefunc, intarray, unaccent, cube, earthdistance, bloom, seg, isn, tcn, tsm_system_rows, pageinspect, pg_visibility, pg_buffercache, pg_freespacemap, amcheck. On request: pgcrypto (1.1 MB) and PostGIS (19 MB) — the engine restarts with your data. Then CREATE EXTENSION.

  • One connection. PGlite is a single-user Postgres: one backend, one connection. Sessions A, B, C in PGlite mode are separate editors on that one backend — one transaction, one set of locks. Concurrency behaviour therefore comes from the model (below) or from a real server.

The concurrency model

Locks between sessions are the point of most of these patterns, and a single backend cannot show them. The visualiser is a small, deterministic model written from the PostgreSQL manual’s descriptions of explicit locking, transaction isolation, the locking clause and NOTIFY: row locks with wait, SKIP LOCKED and NOWAIT; re-checking a row that changed while you waited (READ COMMITTED); session and transaction advisory locks; snapshots and first-updater-wins for REPEATABLE READ; a simplified serializable check that catches write skew; deadlock detection after deadlock_timeout (1 s) by the waiting backend; notifications delivered at commit, folded within a transaction, dropped on rollback; sequences that never roll back; leases and fencing tokens; crashes. It is labelled as a model on the page and it is not a recording of a server — the real-server races are.

Connecting to a real server

Browsers cannot open TCP connections, so a bridge you run copies bytes between a WebSocket and Postgres’s port. The page then speaks the wire protocol itself — startup, SCRAM-SHA-256 or md5 password authentication (the server’s SCRAM signature is verified), simple and extended queries, notices, notifications, COPY TO STDOUT and cancel requests.

With the gitwebsite app running on your computer, choose ⚡ gitwebsite app, type the server’s host:port (127.0.0.1:5432 is on the app’s TCP allow-list by default; add others in its Companion settings) and Connect: the app’s TCP relay is the bridge, one connection per session. In the app’s preview it is picked automatically; from this site press ⚡ Use the gitwebsite app and approve the code in the app.

Not needed if the gitwebsite app is running — otherwise: Download postgres-relay.py (Python standard library only) and run:

python3 postgres-relay.py                          # ws://127.0.0.1:6432  ->  localhost:5432
python3 postgres-relay.py --target db.lan:5432 --port 6433

It listens on 127.0.0.1 only, forwards to the one --target you name (it is not an open proxy), and refuses WebSocket connections from web pages other than this site and local previews (--allow-origin adds one). websockify works too: websockify 127.0.0.1:6432 localhost:5432. Or, with Node (npm install ws):

// pg-ws-bridge.js — node pg-ws-bridge.js [wsPort] [pgHost] [pgPort]
const net = require('net');
const { WebSocketServer } = require('ws');
const [port = 6432, host = '127.0.0.1', pgPort = 5432] = process.argv.slice(2);
const ok = o => o === 'https://geekychris.github.io' || /^http:\/\/(localhost|127\.0\.0\.1)(:\d+)?$/.test(o || '');
const wss = new WebSocketServer({ host: '127.0.0.1', port: +port, verifyClient: i => ok(i.origin) });
wss.on('connection', ws => {
  const pg = net.connect(+pgPort, host);
  pg.on('data', d => ws.send(d));
  ws.on('message', d => pg.write(d));
  pg.on('close', () => ws.close()); pg.on('error', () => ws.close());
  ws.on('close', () => pg.destroy());
});
console.log(`ws://127.0.0.1:${port} -> postgres ${host}:${pgPort}`);

A throwaway server to practise on:

docker run -d --name pg-playground -e POSTGRES_PASSWORD=playground -p 127.0.0.1:5432:5432 postgres:17
python3 postgres-relay.py
# then: Real server, ws://127.0.0.1:6432, user postgres, password playground
docker rm -f pg-playground          # when done

Fill in the bridge URL, user, password and database and press Connect. Chrome and Firefox let this https page open ws://localhost; other hosts need wss:// (a bridge behind TLS). The bridge carries the protocol unencrypted on your machine and anyone who can reach it can try to log in to your database: keep it on 127.0.0.1, use a database you can afford to lose, and stop it when you are done. Patterns, datasets and races ask before creating tables on a server, and DROP, TRUNCATE, DELETE/UPDATE without WHERE and ALTER SYSTEM ask before they run. The password stays in memory unless you tick Remember (then it is in this browser’s storage, readable by anyone using it); it never goes into links or history.

The patterns, and where they come from

Grouped as on the Patterns tab. Every lab lists its sources; the main ones:

The honest summary: Postgres is an excellent coordinator when the coordination is about data in that database, when you need it transactional with that data, and at the scale of one primary — typically thousands to tens of thousands of operations a second. It is the wrong tool for fan-out streams with replay to many consumers, very high-rate ephemeral counters, durable pub/sub, or coordination that must survive the database itself being down. Each lab’s Limits section is specific: vacuum and bloat on high-churn queue tables, NOTIFY’s commit-time global lock and 8000-byte payloads, session state (LISTEN, session advisory locks) through transaction poolers, sequence gaps, serialization failures that must be retried.

Limits

  • PGlite is single-connection: concurrent sessions in this page share one backend. Use a real server for true concurrency; the visualiser is a model.

  • PGlite has no pg_cron, pg_stat_statements data, replication or background workers, and COPY FROM STDIN is not supported in the console (use INSERT, or psql).

  • Results keep the first 100,000 rows of a statement and show the first 1,000 in the grid.

  • Real-server mode: no TLS between the page and the bridge (the bridge must be local or behind wss://), no channel binding (SCRAM-SHA-256-PLUS), no Kerberos/GSSAPI or certificate authentication. Values arrive as text, exactly as psql shows them.

  • Meta-commands cover the common psql ones, not all of them; output is close to psql’s but not identical.

Linking to an example

Links can be written by hand. Add the tab after #: console, patterns, datasets, docs or history. A link never connects to a server by itself and never carries a password.

Parameter Meaning Example

example

Open a pattern lab and run its setup in PGlite

example=queue#patterns

sim

Which model scenario the lab shows first

example=concurrency&sim=deadlock#patterns

sql

SQL for the console: run in PGlite, filled in (not run) for a real server

sql=SELECT%20version()

ds

Load a sample dataset in PGlite

ds=shop#datasets

doc

Open a page of the SQL reference

doc=MERGE#docs

q

A search in the Docs tab

q=lock#docs

conn

pglite or ws

conn=ws

ws, user, db

Bridge URL, user and database for the real-server form

conn=ws&ws=ws://127.0.0.1:6432&user=postgres

Presets for example=:

example= Opens

queue

Work queue with SKIP LOCKED (open; the model without SKIP LOCKED: open)

mutex

Distributed mutex with advisory locks (open)

lease

Leases with fencing tokens (open)

leader

Leader election (open)

pubsub

Pub/sub with LISTEN/NOTIFY and its limits (open)

outbox

Transactional outbox and an event log with offsets (open)

idempotency

Idempotency keys (open)

rate-limit

Rate limiting in SQL (open)

concurrency

Optimistic, pessimistic, serializable — deadlocks and write skew in the model (open)

counters

Sharded counters and gapless numbers (open)

event-sourcing

Event sourcing and history tables (open)

scheduling

Cron-like scheduling without pg_cron (open)

reservations

No double-booking with exclusion constraints (open)

graphs

Hierarchies and graphs: recursive CTEs, ltree (open)

documents

A document store with JSONB (open)

search

Full-text and fuzzy search (open)

vectors

Vector similarity with pgvector (open)

timeseries

Time series: partitions, BRIN, windows (open)

rls

Row-level security for multi-tenancy (open)

geo

Geospatial with PostGIS — loads 19 MB on request (open)

More: ?sql=SELECT * FROM pg_available_extensions ORDER BY 1 lists the extensions; ?doc=MERGE#docs opens one reference page.

Runs entirely in your browser — network requests go only to the services named on this page.