Identity System Deep Dive (historical)

Lifecycle: HISTORICAL SUPPORTING
Recovered from legacy wiki path content/archive/architecture/IDENTITY-SYSTEM.md (attic copy retained; not deleted).
Prefer current SSOT pages for production decisions. Last promoted: 2026-07-20.

Funday Guest Identity System

Overview

Funday implements a guest-first identity system where every visitor automatically receives a stable, persistent Nakama-backed account without requiring signup. This enables seamless access to social features (friends, chat, leaderboards, matchmaking) while maintaining a frictionless user experience.

Note: As of 2025-11-21, all identity data is stored in a single consolidated funday-identity cookie to work around a SvelteKit adapter-node bug that drops multiple Set-Cookie headers. Historical COOKIE-FIX-2025-11-21 note was not recovered into L0 — verify against current Identity Proof links (ensure-session) and frontend/src/lib/server/cookieHelper.ts.

Architecture

Core Principle

One Device → One Identity → One Nakama Account

The identity system follows a strict hierarchy:

  1. Device ID (persistent, 1-year cookie) = Browser/device identifier
  2. Nakama Account (created via device auth) = Full player account in Nakama
  3. Username (immutable handle) = TwoWord name (e.g., “MemeBlastoise”, “ElectricTitan”)
  4. DisplayName (mutable persona) = User-changeable expression

Two-Tier Identity Model

Username (Handle)

  • Purpose: Stable unique identifier for @mentions, friend requests, URLs
  • Format: TwoWord combination (Adjective + Noun, e.g., “ShadowDragon”)
  • Mutability: Immutable for guests; claimable after email registration
  • Uniqueness: Enforced via Nakama unique constraint
  • Visibility: Profile URLs (/profile/@username), @mentions, friend search
  • Generation: Server-side via usernameGenerator.ts before Nakama auth

DisplayName (Persona)

  • Purpose: Expressive identity that can change with mood/events
  • Examples: ”🎮 Champion”, “Midnight Racer”, “SpeedDemon”
  • Mutability: User can change anytime via /settings
  • Uniqueness: No constraint (multiple users can share)
  • Visibility: Primary UI display (nav, chat, leaderboards, profile header)
  • Default: Initially set to match username

Implementation

Central Authority: hooks.server.ts

All guest identity creation happens in hooks.server.ts (the SvelteKit server hook):

export const handle: Handle = async ({ event, resolve }) => {
  // 1. Unified security context
  const forwardedProto = event.request.headers.get("x-forwarded-proto");
  const isSecure = forwardedProto === "https" || event.url.protocol === "https:";
  const cookieSecure = IS_DEV ? isSecure : true; // Force secure in prod
 
  // 2. Hydrate session from existing cookies
  // ... (read funday-session and funday-user cookies into event.locals)
 
  // 3. Create guest session if none exists
  if (!event.locals.session) {
    // A. Get or create device ID
    let deviceId = event.cookies.get("funday-device-id");
    if (!deviceId) {
      deviceId = `device-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
      event.cookies.set("funday-device-id", deviceId, {
        secure: cookieSecure,
        httpOnly: false, // JS access for game plugins
        sameSite: "lax",
        maxAge: 60 * 60 * 24 * 365, // 1 year
      });
    }
 
    // B. Generate nice username
    const { generateUsername } = await import("$lib/utils/usernameGenerator");
    const baseUsername = generateUsername();
 
    // C. Authenticate with Nakama (with retry on 409 conflict)
    const nakamaClient = NakamaAPI.createForRequest();
    const { session, user } = await nakamaClient.authenticateDevice(
      deviceId,
      true,
      baseUsername
    );
 
    // D. Initialize displayName and avatar for new users
    if (isNewUser && needsProfile) {
      await nakamaClient.updateAccount(session, {
        display_name: baseUsername,
        avatar_url: `https://api.dicebear.com/9.x/avataaars/svg?seed=${user.id}`
      });
    }
 
    // E. Persist in cookies and locals
    event.cookies.set("funday-session", JSON.stringify(session), getCookieOptions(isSecure));
    event.cookies.set("funday-user", JSON.stringify(user), getPublicCookieOptions(isSecure));
    event.locals.session = session;
    event.locals.user = user;
 
    // F. Metrics
    guestSessionCreated.labels("nakama").inc();
  }
 
  // 4. Continue with request
  return resolve(event);
};

Downstream Consumers

+layout.server.ts

Simplified to a thin projection of event.locals:

export const load: LayoutServerLoad = async ({ locals }) => {
  return {
    session: locals.session ?? null,
    user: locals.user ?? null,
    isAuthenticated: !!locals.user && !isGuestUser(locals.user),
  };
};

No more:

  • Device ID creation
  • Nakama authentication
  • Cookie writes
  • Session cache
  • Local fallbacks

All identity logic is centralized in hooks.server.ts.

API Endpoints

Endpoints trust event.locals.session and event.locals.user populated by hooks:

export const POST: RequestHandler = async ({ locals, request }) => {
  if (!locals.session) {
    return json({ error: "Not authenticated" }, { status: 401 });
  }
  
  // Use locals.session and locals.user directly
  const userId = locals.user!.id;
  // ...
};
CookieSecureHttpOnlySameSiteMaxAgePurpose
funday-device-id✅ (prod)Lax1 yearPersistent device identifier; JS-readable for game plugins
funday-sessionLax1 dayNakama session token; httpOnly for security
funday-userLax1 yearUser profile data; JS-readable for UI

HTTPS Behind Reverse Proxy

The system correctly detects HTTPS via x-forwarded-proto header:

const forwardedProto = event.request.headers.get("x-forwarded-proto");
const isSecure = forwardedProto === "https" || event.url.protocol === "https:";

In production, cookieSecure = true is enforced regardless of event.url.protocol, ensuring cookies work correctly behind nginx.

Social Features

Because guest accounts are real Nakama accounts, they have full access to:

  • Friends - Add/remove friends, see online status
  • Chat - DM, group chat, global channels
  • Leaderboards - Submit scores, view rankings
  • Matchmaking - Join multiplayer matches
  • Storage - Save game progress, achievements
  • Analytics - Track gameplay stats
  • Tournaments - Participate in competitions

No additional setup required - all features work immediately upon landing on the site.

Username Generation

Algorithm

The usernameGenerator.ts produces memorable TwoWord combinations:

  1. Base generation: Random Adjective + Random Noun

    • Adjectives: Gaming, Pokemon, Minecraft, Memes, Fun themes
    • Nouns: Classes, Pokemon, Minecraft, Memes, Animals, Food
    • Examples: ElectricTitan, MemeBlastoise, ShadowPizza
  2. Conflict resolution: If base name is taken, append number suffix

    • Try: ElectricTitanElectricTitan2ElectricTitan3
    • Max attempts: 5 retries with exponential backoff
  3. Clean-first principle: Numbers only appear on collision

    • Most users get clean names (no suffix)
    • Collision rate < 1% due to large namespace (5000+ combinations)

Word Lists

  • Adjectives (50+): Electric, Void, Cosmic, Pika, Creeper, Doge, Chad, etc.
  • Nouns (140+): Titan, Dragon, Pikachu, Enderman, Llama, Impostor, Noob, Pizza, etc.

Guest → Registered User Flow

When a guest decides to register:

  1. Email/password link - Guest claims their account via email auth
  2. Username transfer - Their guest username becomes permanent
  3. DisplayName persistence - Any custom displayName is preserved
  4. Friend graph intact - All friends, chat history, scores remain
  5. Handle upgrade - Username becomes a Twitter-style @handle

Troubleshooting

Multiple Identities Per Refresh

Symptom: Each page refresh shows different username

Cause: Guest creation happening in +layout.server.ts instead of hooks.server.ts

Fix: Ensure all identity logic is in hooks.server.ts (already fixed in current architecture)

Cookies Not Persisting

Symptom: Cookies cleared on every visit

Cause: secure: false cookies rejected by browser on HTTPS

Fix: Always use isSecure detection via x-forwarded-proto header (already fixed)

Username Conflicts

Symptom: Nakama returns 409 Conflict on username

Fix: Retry with numeric suffix (ElectricTitan2, ElectricTitan3, etc.)

Testing

Manual Verification

  1. Clear browser cookies
  2. Visit https://funday.gg/profile
  3. Observe username in profile/nav
  4. Refresh page 5-10 times
  5. Expected: Same username every time
  6. Check DevTools → Application → Cookies:
    • funday-device-id (1 year expiry, secure)
    • funday-session (1 day expiry, secure, httpOnly)
    • funday-user (1 year expiry, secure, non-httpOnly)

Automated Tests

Run Playwright regression suite:

cd frontend
npx playwright test tests/e2e/identity-persistence.spec.ts

Tests verify:

  • ✅ Stable device ID across refreshes
  • ✅ Consistent username across all UI surfaces
  • ✅ No duplicate Nakama accounts
  • ✅ Correct cookie security attributes
  • ✅ Identity preserved across navigation

Metrics

The system emits Prometheus metrics:

  • funday_guest_session_created{source="nakama"} - Successful Nakama guest creation
  • funday_guest_session_created{source="local_fallback"} - Offline fallback creation
  • frontend/src/hooks.server.ts - Central identity engine
  • frontend/src/routes/+layout.server.ts - Thin locals projection
  • frontend/src/lib/utils/usernameGenerator.ts - TwoWord name generation
  • frontend/src/lib/server/nakama.ts - Nakama API client
  • frontend/src/lib/server/cookieHelper.ts - Cookie configuration
  • frontend/tests/e2e/identity-persistence.spec.ts - Regression tests

Benefits

User Experience

  • Zero friction - No signup required to play
  • Instant social - Friends, chat, leaderboards work immediately
  • Persistent identity - Same username across devices (when logged in)
  • Memorable names - Fun, readable usernames (not random IDs)

Technical

  • DRY architecture - Single source of truth (hooks.server.ts)
  • No duplicates - One device → one Nakama account
  • Correct security - Secure cookies on HTTPS behind nginx
  • Nakama-native - Full platform features without custom auth

Business

  • Higher engagement - No signup barrier
  • Viral potential - Users can share @handle for challenges
  • Conversion ready - Smooth upgrade path to registered accounts
  • Social graph - Build network effects before signup

Future Enhancements

  • Username customization - Allow registered users to change @handle once
  • DisplayName emojis - Support emoji in displayName for expression
  • Avatar customization - Let users pick/upload custom avatars
  • Handle marketplace - Premium @handles as IAP
  • Cross-device sync - Link multiple devices to one identity
  • Social auth - Google/Discord/Apple login with handle claim

References

Ask Docs

AI assistant to help answer questions about the documentation. Answers are read-only and cite docs/source.

Hi! How can I help you with the documentation today? Answers are read-only and cite docs/source.

Ctrl+Enter to send