Rebuilding Verified Handles
In 2026 I rebuilt Verified Handles as one TypeScript/PostgreSQL platform, replacing MediaWiki, PHP and Python bots, and migrated all 591k historical revisions with zero data loss. It has been live since 09/2026.
- Role
- Founder & Lead Developer
- When
- 11/2020 – Present; rebuilt in 2026
- Stack
- TypeScript, PostgreSQL, Go
- 155,000+public figures and organisations
- 591khistorical revisions migrated
- 0revisions lost
What it is
A public directory of verified social handles for 155,000+ public figures and organisations, 153,000+ of them linked to Wikidata.

The problem
The old site was MediaWiki on one server. Each entry's data lived in wikitext templates and was read back by fragile string parsing, and a second copy of the index sat in a separate MySQL table that had to be kept in step with the first. Edits passed through Node, a Python subprocess and a PHP endpoint before reaching the database, new entries came from a Python bot, and search was a substring match on titles.
The data itself was regular: a title, a subtitle, handles and identifiers for each of 155,000 entries. It needed one database, not two stores and a pile of scripts.
What I designed
One PostgreSQL database beside a TypeScript application (Hono), which renders HTML on the server. Public pages carry almost no JavaScript: the search bar is about 13 kB and the site script about 18 kB, and the editor's 413 kB loads only when someone presses Edit.
Every field records where its value came from, so the Wikidata sync never overwrites a hand edit. History is kept in three tiers: a verbatim archive of the old wiki, field-level changes from launch on, and an append-only audit log. Search runs in PostgreSQL itself, with trigram and full-text indexes, rather than a separate search service.
The site runs behind Google Project Shield, nginx and Varnish, and pages are purged by surrogate keys when an entry changes. Monitoring is self-hosted: Prometheus, Grafana, Loki and GlitchTip. Measured in 10/2026, a cached entry page answers in a median 56 ms and an uncached one in 70 ms.
I also built JEMEDIA Accounts, the Go OAuth 2.0/OpenID Connect provider behind sign-in.

The migration
The old wiki held 1,602,200 revisions. 1,268,335 were bot counters on user pages, left out by design; the other 590,953, every revision of every entry, were carried over.
Every run of the migration ends in a gate: exact counts of entries and revisions, no revision cut off from its entry, and a re-parse of every page's latest version to check that each subtitle, image, description, handle and Wikidata ID reached the new database. A failure stops the run. Before switching the old server off, I checked that its final dumps matched the migration's source: all 117 tables had the same row counts.
The checks missed one case. Twelve pages were transclusions the migration skipped, so those entries went live empty, though their content was safe in the archive. I found them in 10/2026 and restored them the same day.
The new site went live in 09/2026 by pointing the proxy at the new server. The old one kept running for a week as the way back, then was switched off.
How I built it
I designed it, writing the design first in 06/2026, and built it with Claude Code. Since launch, development has run as a pipeline of parallel agents. Each task gets its own clone, database and ports. A worker must reproduce a bug and add a regression test that fails before fixing it. A script, not an agent, merges finished work into staging and re-runs the whole gate: typecheck, lint, build and about 10,000 tests on a fresh database. Nothing reaches production until I approve a release.
In the month after launch that pipeline merged 1,307 tasks and shipped 58 releases.
What went wrong, and what I changed
Parallel agents sharing one checkout overwrote each other's work: a git stash mixed up two runs, and a reset deleted a good merge. I moved every run into its own clone and made one script the only thing that writes to staging.
A shared development database hid about 317 integration tests that were silently skipped. Each gate now gets its own throwaway database and fails if it doesn't have one.
One schema migration held a lock for 48 seconds and 16 requests failed. Changes to big tables now follow a separate, expand-then-contract rule.
Moving production to a new server in 10/2026 took the site offline for about 34 minutes, longer than planned: the edge proxy reached the new server over HTTPS, not the HTTP the runbook assumed. The move script now sets up and checks HTTPS before any switch.
Since launch
In its first four weeks the new site had six times the Google Search clicks of the old site's last four weeks, from about the same number of impressions.
- TypeScript
- PostgreSQL
- Go