Production learnings (running log)

Durable notes from production incidents and fixes. Each entry captures symptoms, root cause, what we changed, and how to avoid repeating the mistake. Operational how-to stays in BACKEND_OPERATIONS.md; high-level architecture will live in ARCHITECTURE.md when added.

How to add an entry: append a dated section, link the PR, keep secrets out of this file.

---

2026-10-19 — Vercel + Supabase: session pool exhaustion, migration stampede, static 500s

PR: #42 (merged 2026-10-19)

Symptoms

Root cause (three layers)

1. Wrong pool mode for serverless runtime The app rewrote Supabase transaction pool URLs (:6543) to session pool (:5432). Each warm Vercel instance holds one postgres.js client (max: 1). Session mode caps concurrent backends (~15 on our tier). Traffic and cold starts → session pool exhausted → API 500s (including PUT /api/boards/..., uploads, explore).

2. Migration stampede Every cold start ran migrations inside a transaction with pg_advisory_xact_lock. Many parallel instances queued → statement timeout before any request could succeed.

3. Static traffic coupled to DB Catch-all routing sent all URLs through Node; session lookup ran before serving public/ files, so a sick DB could break JS loads.

What we changed

AreaFix
Runtime URL (Vercel)Use transaction pooler `:6543` for all API/query traffic (resolveDatabaseUrl).
MigrationsSeparate short-lived session URL (:5432 on pooler host, or POSTGRES_URL_NON_POOLING) only in PostgresStore.migrate(), then close.
postgres.jsOne module-level store per instance: max: 1, prepare: false, shorter idle/max_lifetime.
Migrations concurrencyFast path if schema_migrations complete; pg_try_advisory_lock + poll instead of blocking the whole fleet.
HTTPServe allowlisted public/ (and Studio) before accounts.current(); vercel.json adds { "handle": "filesystem" } for CDN static.

Vercel env (what we learned)

Verification

Do not repeat

Related docs

---

2026-10-25 — Stale immutable wall.js (catalog sprites + Browse backgrounds)

PRs: #59 (core bust) · #60 (media cache + catalog contrast)

Symptoms

Root cause

servePublicFile set Cache-Control: public, max-age=31536000, immutable on all non-HTML public files, including unhashed /wall.js and /styles.css. Browsers kept a year-old JS build while HTML stayed no-store.

What we changed

AreaFix
JS/CSSpublic, max-age=0, must-revalidate (+ vercel.json headers)
HTMLRewrite local script/link tags with ?v=<assetVersion> (release.assetVersion)
Other staticpublic, max-age=3600, must-revalidate (no immutable on redrawn /assets sprites)
Catalog UILight label text on dark catalog cards

Do not repeat

---

2026-10-20 — Studio board images 404; fragile sprite URLs

PR: (this fix branch)

Symptoms

Root cause

What we changed

AreaFix
Vercel bundleAdd docs/**, marketing/**, screenshots/** to serverless includeFiles so /studio/file/... works without CDN.
Sprite URLsAbsolute /assets/objects/... in wall.js and validation.js.

Do not repeat

---

<!-- Next entry: append above this line, newest first or oldest-first — we use newest-first after the header. -->