Identity System Deep Dive (historical)
Lifecycle: HISTORICAL SUPPORTING
Recovered from legacy wiki pathcontent/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-identitycookie to work around a SvelteKit adapter-node bug that drops multiple Set-Cookie headers. HistoricalCOOKIE-FIX-2025-11-21note was not recovered into L0 — verify against current Identity Proof links (ensure-session) andfrontend/src/lib/server/cookieHelper.ts.
Architecture
Core Principle
One Device → One Identity → One Nakama Account
The identity system follows a strict hierarchy:
- Device ID (persistent, 1-year cookie) = Browser/device identifier
- Nakama Account (created via device auth) = Full player account in Nakama
- Username (immutable handle) = TwoWord name (e.g., “MemeBlastoise”, “ElectricTitan”)
- 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.tsbefore 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;
// ...
};Cookie Security
Cookie Attributes
| Cookie | Secure | HttpOnly | SameSite | MaxAge | Purpose |
|---|---|---|---|---|---|
funday-device-id | ✅ (prod) | ❌ | Lax | 1 year | Persistent device identifier; JS-readable for game plugins |
funday-session | ✅ | ✅ | Lax | 1 day | Nakama session token; httpOnly for security |
funday-user | ✅ | ❌ | Lax | 1 year | User 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:
-
Base generation: Random Adjective + Random Noun
- Adjectives: Gaming, Pokemon, Minecraft, Memes, Fun themes
- Nouns: Classes, Pokemon, Minecraft, Memes, Animals, Food
- Examples:
ElectricTitan,MemeBlastoise,ShadowPizza
-
Conflict resolution: If base name is taken, append number suffix
- Try:
ElectricTitan→ElectricTitan2→ElectricTitan3… - Max attempts: 5 retries with exponential backoff
- Try:
-
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:
- Email/password link - Guest claims their account via email auth
- Username transfer - Their guest username becomes permanent
- DisplayName persistence - Any custom displayName is preserved
- Friend graph intact - All friends, chat history, scores remain
- 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
- Clear browser cookies
- Visit
https://funday.gg/profile - Observe username in profile/nav
- Refresh page 5-10 times
- Expected: Same username every time
- 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.tsTests 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 creationfunday_guest_session_created{source="local_fallback"}- Offline fallback creation
Related Files
frontend/src/hooks.server.ts- Central identity enginefrontend/src/routes/+layout.server.ts- Thin locals projectionfrontend/src/lib/utils/usernameGenerator.ts- TwoWord name generationfrontend/src/lib/server/nakama.ts- Nakama API clientfrontend/src/lib/server/cookieHelper.ts- Cookie configurationfrontend/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
- Nakama Device Authentication Docs
- SvelteKit Hooks Documentation
- Current Identity SSOT
- Repo rules (not published in Quartz):
.agents/rules/funday_guest-first.md/.windsurf/rules/funday_guest-first.md(two-tier rule file absent; see current Identity page)