Appearance
Auth migrated Better Auth → Privy (headless social + invite codes)
2026-06-27 — login moves from self-hosted Better Auth email/password to Privy as the auth provider, integrated headless behind our own brat-themed UI. This first cut is social login (Google/Apple) + invite-code-gated registration; embedded wallets + a fiat on-ramp are deferred to a post-Solana, Solana-native phase (building them pre-Solana = building them twice).
The migration is bounded by the auth-abstraction: the user_auth bridge isolates the provider, so users.id, every downstream FK, and the JWKS JWT contract are preserved — game-server / admin-server verification is unchanged.
What changed
- auth-server is a Privy-session → our-JWT bridge —
POST /api/auth/privyverifies a Privy access token (@privy-io/server-auth,iss: privy.io,aud: PRIVY_APP_ID), resolves the user byuser_auth.auth_provider_user_id = sub, and mints OUR existing JWKS-signed JWT via Better Auth's server-onlysignJWT(the same key game-server verifies against; no Better Auth session, and/api/auth/sign-jwtis not a public route, so no arbitrary-mint surface). - Invite-code-gated registration — the pre-existing
invite_codestable is reused as the registration gate (no migration). First sign-in consumes a code; the consume + user-create share one transaction, so a refused code creates no account. Operators mint viaPOST /admin/invite-codes(service-key). - Public email/password signup disabled —
POST /api/auth/sign-up/email→403. The Better Auth password-reset flow is retired alongside it. - Frontend (poker) —
PrivyProvider+ a brat headless login (our own Google/Apple buttons viauseLoginWithOAuth, never Privy's modal) + an invite-code field. The Privy → our-JWT bridge publishes the minted JWT through aGameTokenContextoverride, souseGameToken()and every WS / hook consumer is unchanged. Admin stays on Better Auth (a separate follow-up).
Verification
The bridge + invite gate are vitest-integration-verified live (T1–T7): no-code / reused / expired all refused with no account created; a returning user resolves to the same users.id; email/password signup disabled; and the load-bearing tripwire — a verified Privy token → a minted JWKS JWT that verifies exactly as game-server does (createRemoteJWKSet + jwtVerify, sub === users.id). The brat-UI visual + the real Google/Apple OAuth round-trip are manual smoke on staging.
See the auth-server guide.