Jun 06, 8-9 AM (3)
Jun 06, 9-10 AM (0)
Jun 06, 10-11 AM (3)
Jun 06, 11-12 PM (6)
Jun 06, 12-1 PM (2)
Jun 06, 1-2 PM (2)
Jun 06, 2-3 PM (2)
Jun 06, 3-4 PM (18)
Jun 06, 4-5 PM (1)
Jun 06, 5-6 PM (6)
Jun 06, 6-7 PM (0)
Jun 06, 7-8 PM (6)
Jun 06, 8-9 PM (0)
Jun 06, 9-10 PM (1)
Jun 06, 10-11 PM (27)
Jun 06, 11-12 AM (9)
Jun 07, 12-1 AM (14)
Jun 07, 1-2 AM (2)
Jun 07, 2-3 AM (0)
Jun 07, 3-4 AM (0)
Jun 07, 4-5 AM (1)
Jun 07, 5-6 AM (1)
Jun 07, 6-7 AM (3)
Jun 07, 7-8 AM (0)
Jun 07, 8-9 AM (0)
Jun 07, 9-10 AM (1)
Jun 07, 10-11 AM (2)
Jun 07, 11-12 PM (2)
Jun 07, 12-1 PM (5)
Jun 07, 1-2 PM (35)
Jun 07, 2-3 PM (2)
Jun 07, 3-4 PM (4)
Jun 07, 4-5 PM (2)
Jun 07, 5-6 PM (4)
Jun 07, 6-7 PM (0)
Jun 07, 7-8 PM (0)
Jun 07, 8-9 PM (17)
Jun 07, 9-10 PM (1)
Jun 07, 10-11 PM (21)
Jun 07, 11-12 AM (9)
Jun 08, 12-1 AM (9)
Jun 08, 1-2 AM (5)
Jun 08, 2-3 AM (3)
Jun 08, 3-4 AM (4)
Jun 08, 4-5 AM (2)
Jun 08, 5-6 AM (9)
Jun 08, 6-7 AM (5)
Jun 08, 7-8 AM (25)
Jun 08, 8-9 AM (36)
Jun 08, 9-10 AM (41)
Jun 08, 10-11 AM (24)
Jun 08, 11-12 PM (22)
Jun 08, 12-1 PM (40)
Jun 08, 1-2 PM (48)
Jun 08, 2-3 PM (33)
Jun 08, 3-4 PM (27)
Jun 08, 4-5 PM (12)
Jun 08, 5-6 PM (23)
Jun 08, 6-7 PM (14)
Jun 08, 7-8 PM (3)
Jun 08, 8-9 PM (6)
Jun 08, 9-10 PM (19)
Jun 08, 10-11 PM (29)
Jun 08, 11-12 AM (8)
Jun 09, 12-1 AM (5)
Jun 09, 1-2 AM (3)
Jun 09, 2-3 AM (1)
Jun 09, 3-4 AM (3)
Jun 09, 4-5 AM (26)
Jun 09, 5-6 AM (5)
Jun 09, 6-7 AM (23)
Jun 09, 7-8 AM (51)
Jun 09, 8-9 AM (35)
Jun 09, 9-10 AM (45)
Jun 09, 10-11 AM (51)
Jun 09, 11-12 PM (46)
Jun 09, 12-1 PM (86)
Jun 09, 1-2 PM (84)
Jun 09, 2-3 PM (36)
Jun 09, 3-4 PM (38)
Jun 09, 4-5 PM (16)
Jun 09, 5-6 PM (18)
Jun 09, 6-7 PM (18)
Jun 09, 7-8 PM (19)
Jun 09, 8-9 PM (16)
Jun 09, 9-10 PM (16)
Jun 09, 10-11 PM (28)
Jun 09, 11-12 AM (10)
Jun 10, 12-1 AM (11)
Jun 10, 1-2 AM (16)
Jun 10, 2-3 AM (11)
Jun 10, 3-4 AM (19)
Jun 10, 4-5 AM (5)
Jun 10, 5-6 AM (2)
Jun 10, 6-7 AM (46)
Jun 10, 7-8 AM (82)
Jun 10, 8-9 AM (18)
Jun 10, 9-10 AM (59)
Jun 10, 10-11 AM (46)
Jun 10, 11-12 PM (134)
Jun 10, 12-1 PM (49)
Jun 10, 1-2 PM (33)
Jun 10, 2-3 PM (32)
Jun 10, 3-4 PM (28)
Jun 10, 4-5 PM (36)
Jun 10, 5-6 PM (12)
Jun 10, 6-7 PM (12)
Jun 10, 7-8 PM (38)
Jun 10, 8-9 PM (11)
Jun 10, 9-10 PM (9)
Jun 10, 10-11 PM (20)
Jun 10, 11-12 AM (7)
Jun 11, 12-1 AM (10)
Jun 11, 1-2 AM (2)
Jun 11, 2-3 AM (0)
Jun 11, 3-4 AM (2)
Jun 11, 4-5 AM (8)
Jun 11, 5-6 AM (12)
Jun 11, 6-7 AM (34)
Jun 11, 7-8 AM (106)
Jun 11, 8-9 AM (36)
Jun 11, 9-10 AM (20)
Jun 11, 10-11 AM (105)
Jun 11, 11-12 PM (25)
Jun 11, 12-1 PM (38)
Jun 11, 1-2 PM (37)
Jun 11, 2-3 PM (14)
Jun 11, 3-4 PM (20)
Jun 11, 4-5 PM (5)
Jun 11, 5-6 PM (7)
Jun 11, 6-7 PM (26)
Jun 11, 7-8 PM (90)
Jun 11, 8-9 PM (11)
Jun 11, 9-10 PM (5)
Jun 11, 10-11 PM (25)
Jun 11, 11-12 AM (5)
Jun 12, 12-1 AM (8)
Jun 12, 1-2 AM (2)
Jun 12, 2-3 AM (2)
Jun 12, 3-4 AM (4)
Jun 12, 4-5 AM (7)
Jun 12, 5-6 AM (15)
Jun 12, 6-7 AM (46)
Jun 12, 7-8 AM (19)
Jun 12, 8-9 AM (28)
Jun 12, 9-10 AM (22)
Jun 12, 10-11 AM (29)
Jun 12, 11-12 PM (42)
Jun 12, 12-1 PM (24)
Jun 12, 1-2 PM (26)
Jun 12, 2-3 PM (22)
Jun 12, 3-4 PM (38)
Jun 12, 4-5 PM (23)
Jun 12, 5-6 PM (19)
Jun 12, 6-7 PM (26)
Jun 12, 7-8 PM (12)
Jun 12, 8-9 PM (17)
Jun 12, 9-10 PM (5)
Jun 12, 10-11 PM (30)
Jun 12, 11-12 AM (6)
Jun 13, 12-1 AM (6)
Jun 13, 1-2 AM (2)
Jun 13, 2-3 AM (0)
Jun 13, 3-4 AM (3)
Jun 13, 4-5 AM (0)
Jun 13, 5-6 AM (3)
Jun 13, 6-7 AM (7)
Jun 13, 7-8 AM (5)
Jun 13, 8-9 AM (0)
3,214 commits this week Jun 06, 2026 - Jun 13, 2026
fix(db): cap pg pool + stop retrying pool saturation (Supabase EMAXCONNSESSION)
Production was 500ing every query with "(EMAXCONNSESSION) max clients
reached in session mode - max clients are limited to pool_size: 15".

Verified root cause (live + multi-agent audit):
- src/server/db.ts created the @prisma/adapter-pg pool with no `max`, so
  node-postgres defaulted to 10 connections per warm Vercel instance. A
  couple of instances overrun Supabase's session-mode pool (15 client
  slots) -> EMAXCONNSESSION on every query, including user.createUser.
- The retry wrapper amplified it: it called $connect() against the dead
  pool between retries and, once connectionTimeoutMillis is finite, would
  treat the "timeout exceeded when trying to connect" acquire error as a
  retryable connection error.

Changes:
- Cap the pool: max: 2, idleTimeoutMillis 10s, connectionTimeoutMillis 10s
  (finite timeout fails fast instead of pg's default infinite wait).
- isConnectionError(): never retry pool-saturation errors (max clients
  reached / pool_size / EMAXCONNSESSION / connect-timeout).
- Drop the $connect() reconnect between retries (the driver-adapter pool
  reconnects lazily; forcing connect just adds load).

The Prisma globalThis singleton was verified correct and left unchanged.
The 5 interactive $transaction blocks are pure-DB (no external I/O held),
so no leak fix is required for this to hold.

NOTE (maintainer action, env — cannot be done in code): point production
DATABASE_URL at the Supabase TRANSACTION pooler (port 6543, ?pgbouncer=true)
and keep DIRECT_URL on the direct connection (5432) for migrations. The
session pooler (5432) is the wrong mode for serverless; this code change is
the necessary client-side cap and works as an interim mitigation too.

Co-Authored-By: Claude Fable 5 <[email protected]>
fix(auth): normalize all userAddress sources in layout to bech32 (stuck "Loading…")
THE root-cause fix for the stuck "Loading…": layout fed userAddress / the
session check / createUser from hex-encoded addresses (react-2.0 useAddress()
and raw CIP-30 getUsedAddresses/getUnusedAddresses), but user records and
sessions are keyed by bech32 — so getUserByAddress(hex) returned null and the
app hung. Confirmed live on preprod: getUserByAddress(addr1qyvgdy2…) finds the
user; getUserByAddress(0118869144…) → null (same wallet).

- Normalize `address` once at the useAddress() source so every consumer
  (store sync, walletAddressForSession/Check, createUser) uses bech32.
- Normalize the getUsedAddresses()/getUnusedAddresses() setUserAddress sites.

(The equivalent change was authored alongside #281 but merged after that PR
was already squashed, so it never reached preprod — this carries it, plus the
session/createUser hardening, with the required import.)

Co-Authored-By: Claude Fable 5 <[email protected]>
fix(auth): normalize userAddress to bech32 in layout so getUserByAddress finds the user
Root cause of the stuck "Loading…": layout synced userAddress to the store
from useAddress() and raw activeWallet.getUsedAddresses()/getUnusedAddresses()
WITHOUT normalizing. react-2.0's useAddress and raw CIP-30 wallets return
hex-encoded address bytes, but user records are keyed by bech32. So the store
held the hex form, getUserByAddress(hex) returned null, `user` stayed null,
and the connect button spun "Loading…" forever.

Confirmed live on preprod: getUserByAddress(addr1qyvgdy2…) → user found;
getUserByAddress(0118869144…) → null (same wallet).

connect-wallet.tsx already normalized at its setUserAddress; layout's three
call sites were missed. Normalize all of them via normalizeAddressToBech32.

Co-Authored-By: Claude Fable 5 <[email protected]>
fix(auth): recover from stuck "Loading…" when wallet connects but never gets a session
When a wallet connects but authorization doesn't complete (auto-authorize
failed/cancelled — common when multiple Cardano extensions fight over
window.cardano), layout sets hasCheckedSession=true and never reopens the
auth modal. createUser stays 403 (no session), `user` stays null, and the
Connect button spins "Loading…" forever with no recovery path.

- Relabel that state: connected + user-query-resolved-empty now shows an
  actionable "Authorize" instead of an infinite "Loading…" spinner.
- Add an "Authorize wallet" item to the connect dropdown (shown when
  connected but unauthorized) that bumps a new reauthNonce signal.
- layout watches reauthNonce, clears the hasCheckedSession latch, and
  refetches the session so the existing session-check effect reopens the
  (now signData-fixed) WalletAuthModal.

Additive and manual-trigger only — no change to connect/sign/session logic,
no auto-retry loop.

Co-Authored-By: Claude Fable 5 <[email protected]>