An account without Google
Passkeys arrive alongside Google sign-in as an independent credential type, with an in-page modal chosen over a dedicated route and throwaway prototypes shaping the flow.
When player accounts first shipped to sync daily challenges and duels across devices, Google sign-in was the only identity provider available. That setup satisfied the immediate goal of syncing scores without maintaining custom password infrastructure, but it left the entire account model strictly dependent on a Google identity. That limitation ended today. WebAuthn passkeys now arrive as a parallel, independent credential type on the same underlying player account, allowing players to register, authenticate, and manage cross-device profiles using device biometrics or platform credentials without needing a Google account.
Parallel credentials, not a replacement#
Rather than treating passkeys as a secondary feature layered on top of Google authentication, the architecture treats both credentials as equals. A player can register an account using only a passkey, attach additional passkeys over time, or optionally link a Google account later. Storing credentials required an additive schema change: a dedicated credentials table tracking credential identifiers, device labels derived from user agent headers, signature counters for clone detection, and public key bytes. In a schema where every previous binary blob had been serialized as text, storing the raw public key as a binary column marks a deliberate first exception following WebAuthn storage conventions. The database continues to require at least one active credential per account, enforced atomically inside a row-locking transaction so concurrent credential removals cannot leave an orphan account behind.
Three throwaway prototypes for one modal#
Integrating passkeys immediately forced a rethink of the user interface. The initial implementation simply dropped new buttons into the settings page, creating a confusing four-button stack alongside existing game preferences. Clicking a sign-in prompt from the main menu or a post-game recap transported the player to the bottom of a busy settings screen with zero framing. To resolve this, a prototype branch explored three distinct directions: reshaped settings with progressive disclosure, a dedicated sign-in page on its own route, and a lightweight in-page modal dialog. Testing the variations on a live development server confirmed that the in-page modal felt the most focused, keeping the player directly in their current context rather than navigating them across pages.
flowchart TD
accTitle: Account modal view transitions
accDescr: Flowchart illustrating that the modal switches internally between signed-out tabs and signed-in credential management without closing itself.
SO["Signed-out view<br/>(Sign in / Create tabs)"] -->|Passkey ceremony or OAuth callback| SI["Signed-in view<br/>(Stats and credentials)"]
SI -->|Sign out or delete| SO
SI -->|Add passkey or link Google| SI
class SI accentSeparate tabs for separate ceremonies#
The resulting modal component handles both initial onboarding and ongoing credential management while adhering to a deliberate rule: it never dismisses itself on actions that alter authentication state. Creating a passkey-only account swaps the modal view directly to signed-in credential management, while signing out immediately displays the login options again. Furthermore, WebAuthn ceremonies cannot infer player intent from a single click, since registration and authentication require different browser calls. To prevent empty password manager prompts when an unregistered visitor clicks to sign in, the signed-out interface splits into separate “Sign in” and “Create account” tabs. Google sign-in appears on both tabs under identical wording because OAuth handles both outcomes in one flow, whereas passkey actions are separated by intention.
Surviving the OAuth redirect#
Handling external redirects required bridging the modal with OAuth’s departure from the page. While WebAuthn ceremonies resolve entirely client-side without page reloads, Google authentication leaves the application for a third-party consent screen, destroying in-memory dialog state. The OAuth callback now redirects directly to the main menu with a validated query parameter that reopens the modal automatically, presenting the resulting status without attempting to reconstruct complex originating history. Review passes across the implementation also caught critical edge cases: driver error handling was updated to unwrap underlying database errors when verifying uniqueness constraints, and dismissed authenticator prompts are handled gracefully without locking interactive buttons.
// src/lib/server/db/pgErrors.ts
export function isUniqueViolation(err: unknown): boolean {
const codeOf = (e: unknown) => (e as { code?: unknown } | null | undefined)?.code;
return codeOf(err) === '23505' || codeOf((err as { cause?: unknown } | null | undefined)?.cause) === '23505';
}
With the dual-credential foundation and consolidated modal in place, the account system no longer presumes a single identity provider. The next phase will focus on observing real-world authenticator behavior across different operating systems and browsers, verifying how credential backup flags and multi-device passkey synchronization behave in everyday play.