python3 postgres-relay.py # ws://127.0.0.1:6432 -> localhost:5432
python3 postgres-relay.py --target db.lan:5432 --port 6433
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
- Explain plan Databases and streamingExplain plan ↗ under any plan opens it there with its query, and queries typed there run here
- Schema diff Databases and streamingopens here with schema A and the migration to B ready to run
- Distributed patterns on Redis Databases and streamingthe same coordination patterns on Redis — locks, leases, queues, rate limits — as races to step through, with their Lua
- Redis client Databases and streaminga redis-cli in the browser with its own built-in server, for the Redis side of these patterns
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` (`\dxlists every extension available),\du,\dT,\dp,\l,\sf,\x(expanded display),\timing,\conninfo,\echo,\hand\?. Patterns like\dt shop.and\df takework. -
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
BEGINand lock a row in A and watch B block on it, orLISTENin B andNOTIFYfrom 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:
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:
-
Work queue with SKIP LOCKED — enqueue, claim a batch, ack, retry with exponential backoff, leases (visibility timeouts), a dead-letter table, priorities and scheduled jobs. The design of pgmq, River, Oban, GoodJob, Solid Queue, Graphile Worker, pg-boss and Que; Craig Ringer’s SKIP LOCKED explainer; Brandur Leach on queues and MVCC bloat.
-
Distributed mutex with advisory locks — session and transaction scope, try-locks, keys from
hashtextextended. Advisory locks; the PgBouncer feature map. -
Leases with fencing tokens and leader election — Martin Kleppmann, How to do distributed locking; River’s leader election.
-
Pub/sub with LISTEN/NOTIFY — NOTIFY; Recall.ai, LISTEN/NOTIFY does not scale; DBOS, LISTEN/NOTIFY actually scales; the PostgreSQL 19 channel-map change.
-
Transactional outbox and an event log with offsets — microservices.io, with the commit-order trap and the
pg_snapshot_xminfix. -
Idempotency keys — Brandur Leach, Stripe-like idempotency keys in Postgres.
-
Rate limiting (token bucket, fixed window), optimistic / pessimistic / serializable concurrency (with SSI write-skew examples), counters without hot rows and gapless numbers (why sequences have gaps), event sourcing and history tables, cron-like scheduling.
-
Data models and search: exclusion constraints for bookings, recursive CTEs and ltree, JSONB documents with GIN and JSON_TABLE, full-text and trigram search, pgvector with HNSW and hybrid ranking, PostGIS (or earthdistance), time series with partitions, BRIN and windows, and row-level security for multi-tenancy.
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_statementsdata, replication or background workers, andCOPY FROM STDINis not supported in the console (useINSERT, 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 |
|---|---|---|
|
Open a pattern lab and run its setup in PGlite |
|
|
Which model scenario the lab shows first |
|
|
SQL for the console: run in PGlite, filled in (not run) for a real server |
|
|
Load a sample dataset in PGlite |
|
|
Open a page of the SQL reference |
|
|
A search in the Docs tab |
|
|
|
|
|
Bridge URL, user and database for the real-server form |
|
Presets for example=:
example= |
Opens |
|---|---|
|
Work queue with SKIP LOCKED (open; the model without SKIP LOCKED: open) |
|
Distributed mutex with advisory locks (open) |
|
Leases with fencing tokens (open) |
|
Leader election (open) |
|
Pub/sub with LISTEN/NOTIFY and its limits (open) |
|
Transactional outbox and an event log with offsets (open) |
|
Idempotency keys (open) |
|
Rate limiting in SQL (open) |
|
Optimistic, pessimistic, serializable — deadlocks and write skew in the model (open) |
|
Sharded counters and gapless numbers (open) |
|
Event sourcing and history tables (open) |
|
Cron-like scheduling without pg_cron (open) |
|
No double-booking with exclusion constraints (open) |
|
Hierarchies and graphs: recursive CTEs, ltree (open) |
|
A document store with JSONB (open) |
|
Full-text and fuzzy search (open) |
|
Vector similarity with pgvector (open) |
|
Time series: partitions, BRIN, windows (open) |
|
Row-level security for multi-tenancy (open) |
|
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.
Feedback is a gift. Something to improve, a feature to add, or a bug? Tell me.
Runs entirely in your browser — network requests go only to the services named on this page.