postgres-branch-board
Comparison + transparently-dated benchmark of every commercial Postgres-branching service: Neon, Supabase Branching, Xata, Tembo, Ardent, Crunchy Bridge, Nile.
For the backend dev or CTO Googling "Neon vs Supabase branches" while wiring CI ephemeral DBs and trying to pick the one that won't cost $400/mo at 100 PRs.
The board shows one timed run of branch-creation latency (with the measurement date next to every cell), plus live GitHub stars / last-commit / HN traction / pricing-page heartbeat fetched by node-cron.
What's measured vs. what's live
| Source | Provenance | Refresh |
|--------|-----------|---------|
| Branch-creation latency, free-tier limits, cold-start, fork-from-prod, regions, price-at-scale | One-shot snapshot, 2026-05-14, every row carries source_url | Manual re-run; UI labels them as snapshot |
| Last funding round, amount, date | Snapshot, source URL per entry | Manual |
| GitHub repo: stars, forks, last commit | api.github.com/repos/{org}/{cli-repo} | hourly cron (cron expr 7 ) |
| HN news per provider | hn.algolia.com/api/v1/search?query=<provider>&tags=story | daily cron (17 5 ) |
| Pricing-page heartbeat (HTTP status + SHA-256 of body) | Each provider's public pricing URL | daily cron (37 6 ) |
| Docs-page heartbeat (status, keyword spot-checks) | Each provider's branching docs URL | weekly cron (47 7 1) |
There is no mock data. If a metric had no public source URL it was dropped — better one honest row than ten fake ones.
Endpoints (all under /postgres-branch-board)
| Method | Path | Purpose |
|--------|------|---------|
| GET | /health | {ok:true} — no auth |
| GET | / | SPA — board + filter chips + sortable table |
| GET | /p/:slug | Server-rendered scorecard shell (OG tags point at badge SVG) |
| GET | /api/providers | Full board (merged measured + live + badges) |
| GET | /api/providers/:slug | Single provider deep view (metrics, news, fetch state) |
| GET | /api/news?limit=50 | Newest HN items across all providers |
| GET | /api/fetch-log?limit=100 | Recent fetch attempts with status + duration |
| GET | /api/freshness | Latest fetch timestamp per source |
| POST | /api/refresh?source=github\|hn\|pricing\|docs\|all | Trigger ad-hoc fetch — no auth, rate-limited 1/min per source |
| GET | /api/badges/:slug.svg | Shareable 600×120 SVG badge (dark, vendor-friendly) |
Real data source URLs
GitHub (hourly)
https://api.github.com/repos/neondatabase/neonctlhttps://api.github.com/repos/supabase/clihttps://api.github.com/repos/xataio/client-tshttps://api.github.com/repos/tembo-io/tembohttps://api.github.com/repos/ardent-cloud/ardenthttps://api.github.com/repos/CrunchyData/cbhttps://api.github.com/repos/niledatabase/niledatabase
A GITHUB_TOKEN raises the rate limit from 60 to 5000 req/hour. Without it the hourly cron still works.
HN Algolia (daily)
https://hn.algolia.com/api/v1/search?query=Neon+Postgres&tags=storyhttps://hn.algolia.com/api/v1/search?query=Supabase+branching&tags=storyhttps://hn.algolia.com/api/v1/search?query=Xata+Postgres&tags=storyhttps://hn.algolia.com/api/v1/search?query=Tembo+Postgres&tags=storyhttps://hn.algolia.com/api/v1/search?query=Ardent+Postgres&tags=storyhttps://hn.algolia.com/api/v1/search?query=Crunchy+Bridge&tags=storyhttps://hn.algolia.com/api/v1/search?query=Nile+database+Postgres&tags=story
Pricing pages (daily heartbeat)
https://neon.tech/pricinghttps://supabase.com/pricinghttps://xata.io/pricinghttps://tembo.io/pricinghttps://www.tryardent.com/pricinghttps://www.crunchybridge.com/pricinghttps://www.thenile.dev/pricing
Docs pages (weekly heartbeat)
https://neon.tech/docs/manage/brancheshttps://supabase.com/docs/guides/deployment/branchinghttps://xata.io/docs/concepts/brancheshttps://tembo.io/docs/product/cloud/configuration-and-management/instance-managementhttps://www.tryardent.com/docs/branchinghttps://docs.crunchybridge.com/how-to/branchinghttps://www.thenile.dev/docs/platform/branching
If a docs/pricing URL 404s the heartbeat still records the status — that's the point. The UI shows it as an amber dot rather than pretending the page exists.
"Best for: X" rules (deterministic, no randomness)
Computed at request time from the merged board snapshot.
- Best for CI — lowest
branch_latency_samong providers withfree_branches >= 50 - Best for preview envs —
fork_from_prod = YesANDbranch_latency_s < 5 - Best for fork-from-prod —
fork_from_prod = Yes, ranked by largestfree_storage_gb - Cheapest at scale — lowest numeric prefix of
price_10gb_100branches - Most momentum — highest
stars / (days_since_last_commit + 1)
A provider can hold 0..N badges; the board shows the top inline + the rest in a tooltip.
Running locally
# Node 22+ required (better-sqlite3 prebuilts)
cp .env.example .env
npm install
npm start
# → http://localhost:4771/postgres-branch-board/
The server seeds providers + measured metrics on boot and kicks off one full live-signal fetch in the background so the board has data immediately. Subsequent live data is refreshed on the cron schedules above (or manually via POST /api/refresh).
To skip the initial bootstrap fetch (faster boots during dev):
SKIP_BOOTSTRAP_FETCH=1 npm start
Config knobs
All read from env, all optional except PORT if you want a non-default port.
| Var | Default | Purpose |
|-----|---------|---------|
| PORT | 4771 | Listen port |
| BASE_PATH | /postgres-branch-board | URL mount prefix |
| DB_PATH | ./data/board.db | better-sqlite3 file (WAL mode) |
| GITHUB_TOKEN | unset | Optional; raises GitHub rate limit |
| CRON_GITHUB / CRON_HN / CRON_PRICING / CRON_DOCS | see above | Override cron schedules |
| SKIP_BOOTSTRAP_FETCH | unset | Set to 1 to skip the boot-time fetch |
File layout
server.js # express bootstrap, helmet, compression
db.js # better-sqlite3 init + schema + read/write helpers
config.js # port, base path, cron schedules
scoring.js # "Best for: X" badge rules
scheduler.js # node-cron jobs + ad-hoc runOnce
scrapers/
http.js # abortable fetch with shared UA
github.js # repo + last-commit, ETag-aware
hn.js # Algolia search + dedupe by objectID
pricing.js # HEAD/GET pricing, sha256 the body, baseline diff
docs.js # GET docs, sha + keyword spot-check
routes/
api.js # board endpoints + /api/refresh
badges.js # /api/badges/:slug.svg renderer
scorecard.js # /p/:slug SSR shell with OG meta
data/
providers.json # 7 providers, config
measured_metrics.json # 7 × 7 metric snapshot, each row has source_url + measured_at
public/
index.html # SPA shell
app.js # board + scorecard + news + debug log
scorecard.js # standalone bundle for /p/:slug shell
style.css # dark theme, ≈200 LOC
Honesty notes
- The branch-creation latency numbers are one timed run per provider on 2026-05-14. The UI labels each cell with the date and a link to the source URL it came from. There is no claim of "continuous benchmarking."
- For providers whose CLI repo path was unknown the GitHub fetch returns 404; the UI shows the row with a stale dot and the board's "Most momentum" badge skips it. That's deliberate — better to surface the gap than fake a number.
- The pricing-page heartbeat verifies the URL is alive and hashes the body so we can flag "pricing page changed since 2026-05-14". It does not parse pricing fields — those come from the snapshot.
License
MIT.