Skip to Content
πŸ“š MyStoryFlow Docs β€” Your guide to preserving family stories

Session Management System

The MyStoryFlow Session Management System provides secure, seamless authentication across multiple applications in the monorepo ecosystem while maintaining strong security protocols and user experience.

πŸ—οΈ Architecture Overview

Core Components

  1. Session Bridge - Cross-domain session transfer mechanism using URL parameters
  2. Auth Context - React context for authentication state management
  3. Middleware - Server-side session refresh and route protection
  4. Cookie-based Storage - HTTP-only cookies managed by Supabase SSR
  5. Token Management - Automatic JWT token refresh via Supabase
  6. Cross-tab Sync - localStorage event-based synchronization

Technology Stack

  • Authentication: Supabase Auth with @supabase/ssr for SSR/SSG support
  • State Management: React Context API (client-side)
  • Storage: HTTP cookies (primary), localStorage (cross-domain transfer), sessionStorage (hash validation)
  • Security: JWT tokens with automatic refresh by Supabase client
  • Session Persistence: Managed by Supabase SSR package via Next.js middleware

πŸ” Session Bridge System

Cross-Domain Authentication

The Session Bridge enables seamless authentication across different domains and ports:

interface SessionBridge { // Session transfer methods createCrossDomainUrl(targetUrl: string, session: SessionData): string extractSessionFromUrl(): SessionData | null initializeSession(): Promise<boolean> // Session management getCurrentSession(): Promise<SessionData | null> storeSession(session: SessionData): Promise<void> clearSession(): Promise<void> // Real-time synchronization setupSessionSync(): void syncSessionAcrossTabs(): void // Security features validateSession(session: SessionData): boolean refreshToken(): Promise<SessionData | null> handleSessionExpiry(): Promise<void> } ``` --> ### Implementation ```tsx export class SessionBridge { private supabase: SupabaseClient private storageKey = 'mystoryflow_session' private syncChannel = 'session_sync' constructor(supabaseUrl: string, supabaseKey: string) { this.supabase = createClient(supabaseUrl, supabaseKey) } async getCurrentSession(): Promise<SessionData | null> { try { // Try Supabase session first const { data: { session }, } = await this.supabase.auth.getSession() if (session) { const sessionData = this.formatSession(session) await this.storeSession(sessionData) return sessionData } // Fallback to stored session return this.getStoredSession() } catch (error) { console.error('Failed to get current session:', error) return null } } createCrossDomainUrl(targetUrl: string, session: SessionData): string { const encodedSession = this.encodeSession(session) const url = new URL(targetUrl) url.searchParams.set('session_token', encodedSession) return url.toString() } extractSessionFromUrl(): SessionData | null { const urlParams = new URLSearchParams(window.location.search) const sessionToken = urlParams.get('session_token') if (!sessionToken) return null try { const session = this.decodeSession(sessionToken) // Validate session if (this.validateSession(session)) { // Clean URL window.history.replaceState({}, '', window.location.pathname) return session } } catch (error) { console.error('Failed to extract session from URL:', error) } return null } async initializeSession(): Promise<boolean> { try { // Check URL for session token const urlSession = this.extractSessionFromUrl() if (urlSession) { await this.restoreSession(urlSession) return true } // Check stored session const storedSession = this.getStoredSession() if (storedSession && this.validateSession(storedSession)) { await this.restoreSession(storedSession) return true } // Check Supabase session const currentSession = await this.getCurrentSession() return currentSession !== null } catch (error) { console.error('Session initialization failed:', error) return false } } private async restoreSession(session: SessionData): Promise<void> { try { // Set Supabase session await this.supabase.auth.setSession({ access_token: session.access_token, refresh_token: session.refresh_token, }) // Store locally await this.storeSession(session) // Sync across tabs this.broadcastSessionUpdate(session) } catch (error) { console.error('Failed to restore session:', error) throw error } } setupSessionSync(): void { // Listen for storage changes (cross-tab sync) window.addEventListener('storage', (event) => { if (event.key === this.storageKey && event.newValue) { const session = JSON.parse(event.newValue) this.handleSessionUpdate(session) } }) // Listen for Supabase auth changes this.supabase.auth.onAuthStateChange(async (event, session) => { if (event === 'SIGNED_IN' && session) { const sessionData = this.formatSession(session) await this.storeSession(sessionData) this.broadcastSessionUpdate(sessionData) } else if (event === 'SIGNED_OUT') { await this.clearSession() this.broadcastSessionUpdate(null) } else if (event === 'TOKEN_REFRESHED' && session) { const sessionData = this.formatSession(session) await this.storeSession(sessionData) this.broadcastSessionUpdate(sessionData) } }) } private validateSession(session: SessionData): boolean { if (!session || !session.access_token || !session.expires_at) { return false } // Check if token is expired const now = Date.now() / 1000 if (session.expires_at <= now) { return false } return true } private encodeSession(session: SessionData): string { const payload = { access_token: session.access_token, refresh_token: session.refresh_token, expires_at: session.expires_at, user_id: session.user.id, } return btoa(JSON.stringify(payload)) } private decodeSession(token: string): SessionData { const payload = JSON.parse(atob(token)) return { access_token: payload.access_token, refresh_token: payload.refresh_token, expires_at: payload.expires_at, user: { id: payload.user_id }, } } } ``` --> ## πŸ”’ Authentication Context ### Auth Provider Implementation ```tsx interface AuthContextType { user: User | null profile: Profile | null session: Session | null loading: boolean signIn: (email: string, password: string) => Promise<AuthResponse> signUp: ( email: string, password: string, metadata?: any ) => Promise<AuthResponse> signOut: () => Promise<void> resetPassword: (email: string) => Promise<void> updateProfile: (updates: Partial<Profile>) => Promise<void> refreshSession: () => Promise<void> } export function AuthProvider({ children }: { children: React.ReactNode }) { const [user, setUser] = useState<User | null>(null) const [profile, setProfile] = useState<Profile | null>(null) const [session, setSession] = useState<Session | null>(null) const [loading, setLoading] = useState(true) const sessionBridge = useMemo( () => new SessionBridge( process.env.NEXT_PUBLIC_SUPABASE_URL!, process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY! ), [] ) useEffect(() => { let mounted = true const initializeAuth = async () => { try { // Setup session synchronization sessionBridge.setupSessionSync() // Initialize session const sessionInitialized = await sessionBridge.initializeSession() if (sessionInitialized) { const currentSession = await sessionBridge.getCurrentSession() if (currentSession && mounted) { await loadUserProfile(currentSession.user.id) } } } catch (error) { console.error('Auth initialization failed:', error) } finally { if (mounted) { setLoading(false) } } } initializeAuth() // Cleanup return () => { mounted = false } }, [sessionBridge]) const loadUserProfile = async (userId: string) => { try { const { data: profile } = await supabase .from('profiles') .select('*') .eq('id', userId) .single() if (profile) { setProfile(profile) } } catch (error) { console.error('Failed to load user profile:', error) } } const signIn = async ( email: string, password: string ): Promise<AuthResponse> => { try { const { data, error } = await supabase.auth.signInWithPassword({ email, password, }) if (error) throw error if (data.user && data.session) { setUser(data.user) setSession(data.session) await loadUserProfile(data.user.id) // Store session for cross-app access const sessionData = sessionBridge.formatSession(data.session) await sessionBridge.storeSession(sessionData) } return { data, error: null } } catch (error) { console.error('Sign in failed:', error) return { data: null, error } } } const signOut = async (): Promise<void> => { try { await supabase.auth.signOut() await sessionBridge.clearSession() setUser(null) setProfile(null) setSession(null) } catch (error) { console.error('Sign out failed:', error) } } const refreshSession = async (): Promise<void> => { try { const { data: { session }, } = await supabase.auth.refreshSession() if (session) { setSession(session) setUser(session.user) const sessionData = sessionBridge.formatSession(session) await sessionBridge.storeSession(sessionData) } } catch (error) { console.error('Session refresh failed:', error) } } const value: AuthContextType = { user, profile, session, loading, signIn, signUp, signOut, resetPassword, updateProfile, refreshSession, } return <AuthContext.Provider value={value}>{children}</AuthContext.Provider> } ``` --> ## πŸ›‘οΈ Protected Routes ### Actual Route Protection Implementation **Two-Layer Protection System**: 1. **Server-Side Middleware** (Primary) - Route-level protection before page load 2. **Client-Side Protected Route Component** (Secondary) - Component-level protection ### Middleware Implementation (Server-Side) ```tsx // apps/web-app/middleware.ts import { updateSession } from '@/lib/supabase/middleware' export async function middleware(request: NextRequest) { return await updateSession(request) } // apps/web-app/lib/supabase/middleware.ts export async function updateSession(request: NextRequest) { let response = NextResponse.next({ request: { headers: request.headers } }) const supabase = createServerClient(url, key, { cookies: { get(name: string) { return request.cookies.get(name)?.value }, set(name: string, value: string, options: CookieOptions) { // Update both request and response cookies request.cookies.set({ name, value, ...options }) response.cookies.set({ name, value, ...options }) }, remove(name: string, options: CookieOptions) { // Clear from both request and response request.cookies.set({ name, value: '', ...options }) response.cookies.set({ name, value: '', ...options }) }, }, }) // CRITICAL: This call refreshes the session on every request await supabase.auth.getUser() // Custom route protection logic const pathname = request.nextUrl.pathname const isDashboard = pathname.startsWith('/dashboard') const isCampaignCreate = pathname.startsWith('/campaigns/create') if (isDashboard || isCampaignCreate) { const { data: { user } } = await supabase.auth.getUser() if (!user) { // Not authenticated - allow page to handle redirect return response } // Fetch setup status for additional checks const { data: profile } = await supabase .from('profiles') .select('setup_completed') .eq('id', user.id) .single() const setupCompleted = !!profile?.setup_completed // Dashboard requires setup completion if (isDashboard && !setupCompleted && !pathname.includes('purchase-gift')) { // Redirect to campaign creation return NextResponse.redirect(new URL('/campaigns/create?step=1', request.url)) } // Campaign creation redirects if already setup if (isCampaignCreate && setupCompleted) { return NextResponse.redirect(new URL('/dashboard', request.url)) } } return response }

Client-Side Protected Route Component

// packages/auth/src/protected-route.tsx - ACTUAL IMPLEMENTATION export function ProtectedRoute({ children, redirectTo = '/login', }: ProtectedRouteProps) { const { user, loading } = useAuth() const router = useRouter() useEffect(() => { if (!loading && !user) { // Capture current path for post-login redirect const currentPath = window.location.pathname + window.location.search const redirectUrl = `${redirectTo}?redirectTo=${encodeURIComponent(currentPath)}` router.replace(redirectUrl) } }, [user, loading, router, redirectTo]) // Loading state if (loading) { return <div className="min-h-screen flex items-center justify-center"> <div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary-600" /> </div> } // Not authenticated if (!user) return null // Authenticated - render protected content return <>{children}</> }

Route Protection Patterns

Pattern 1: Middleware-Only Protection

  • Used for: Dashboard, campaign creation, family workspaces
  • Behavior: Redirect before page loads
  • Performance: Best (no client-side flash)

Pattern 2: Client-Side Protection

  • Used for: Component-level protection
  • Behavior: Redirect after mount
  • Use case: When middleware can’t handle complex logic

Pattern 3: Hybrid Protection (Recommended)

  • Middleware: Basic auth check and session refresh
  • Client component: Role-based or complex business logic
  • Best of both worlds

Role-Based Routing

export function RoleBasedRedirect() { const { user, profile, loading } = useAuth() const router = useRouter() useEffect(() => { if (!loading && user && profile) { // Redirect based on user role switch (profile.role) { case 'admin': case 'support': router.push('/admin/dashboard') break default: router.push('/dashboard') } } }, [user, profile, loading, router]) if (loading) { return <LoadingSpinner /> } return null } ``` --> ## πŸ’Ύ Session Storage Strategy ### Actual Implementation The session management uses the following storage mechanisms: ```tsx interface SessionStorage { // Primary storage - HTTP-only cookies (managed by Supabase SSR) httpCookies: { // Cookies are automatically managed by @supabase/ssr // Names follow Supabase convention: sb-<project-ref>-auth-token persistence: 'browser-cookie' scope: 'same-origin' security: 'httpOnly, secure, sameSite' } // Cross-domain transfer - localStorage localStorage: { key: 'mystoryflow-shared-session' data: SessionData // { access_token, refresh_token, expires_at, user } persistence: 'permanent' scope: 'same-origin' purpose: 'cross-domain session transfer and backup' } // Validation - sessionStorage sessionStorage: { key: 'mystoryflow-shared-session-hash' data: string // base64 encoded session persistence: 'tab-session' scope: 'same-origin' purpose: 'session validation and temporary storage' } // Cross-domain URL transfer urlParameters: { key: 'session' data: string // base64 encoded SessionData persistence: 'temporary' scope: 'cross-origin' purpose: 'transfer session between different domains/ports' } }

Storage Priority

  1. HTTP Cookies (Primary) - Managed by Supabase SSR, automatically set/refreshed by middleware
  2. localStorage (Backup) - Used for cross-domain transfers and as fallback
  3. URL Parameters (Transfer) - Only used during cross-domain navigation
  4. sessionStorage (Validation) - Hash validation for localStorage data

Storage Implementation

class SessionStorageManager { private readonly STORAGE_KEY = 'mystoryflow_session' private readonly HASH_KEY = 'mystoryflow_session_hash' async storeSession(session: SessionData): Promise<void> { try { // Store in localStorage for persistence localStorage.setItem(this.STORAGE_KEY, JSON.stringify(session)) // Store hash in sessionStorage for validation const sessionHash = await this.generateSessionHash(session) sessionStorage.setItem(this.HASH_KEY, sessionHash) // Update memory cache this.updateMemoryCache(session) } catch (error) { console.error('Failed to store session:', error) } } getStoredSession(): SessionData | null { try { const stored = localStorage.getItem(this.STORAGE_KEY) if (!stored) return null const session = JSON.parse(stored) // Validate with session hash if (this.validateSessionHash(session)) { return session } // Clear invalid session this.clearSession() return null } catch (error) { console.error('Failed to retrieve session:', error) return null } } async clearSession(): Promise<void> { try { localStorage.removeItem(this.STORAGE_KEY) sessionStorage.removeItem(this.HASH_KEY) this.clearMemoryCache() } catch (error) { console.error('Failed to clear session:', error) } } private async generateSessionHash(session: SessionData): Promise<string> { const data = JSON.stringify({ user_id: session.user.id, expires_at: session.expires_at, }) const encoder = new TextEncoder() const hashBuffer = await crypto.subtle.digest( 'SHA-256', encoder.encode(data) ) const hashArray = Array.from(new Uint8Array(hashBuffer)) return hashArray.map((b) => b.toString(16).padStart(2, '0')).join('') } private async validateSessionHash(session: SessionData): Promise<boolean> { try { const storedHash = sessionStorage.getItem(this.HASH_KEY) if (!storedHash) return false const computedHash = await this.generateSessionHash(session) return storedHash === computedHash } catch (error) { console.error('Session hash validation failed:', error) return false } } } ``` --> ## πŸ”„ Real-time Synchronization ### Cross-Tab Session Sync The current implementation uses storage events for cross-tab synchronization: ```tsx class SessionSynchronizer { private readonly STORAGE_KEY = 'mystoryflow_session' constructor() { this.setupEventListeners() } private setupEventListeners(): void { // Listen for storage changes (cross-tab sync) window.addEventListener('storage', (event) => { if (event.key === this.STORAGE_KEY) { const sessionData = event.newValue ? JSON.parse(event.newValue) : null this.handleSessionUpdate({ type: 'session_update', session: sessionData, }) } }) // Listen for page visibility changes document.addEventListener('visibilitychange', () => { if (document.visibilityState === 'visible') { this.syncSessionOnFocus() } }) } broadcastSessionUpdate(session: SessionData | null): void { // Storage events automatically fire when localStorage is modified // from another tab, so simply storing triggers cross-tab sync if (session) { localStorage.setItem(this.STORAGE_KEY, JSON.stringify(session)) } else { localStorage.removeItem(this.STORAGE_KEY) } } private handleSessionUpdate(message: any): void { if (message.type === 'session_update') { // Update local session state this.updateLocalSession(message.session) // Trigger re-authentication if needed if (!message.session) { this.handleSessionExpiry() } } } private async syncSessionOnFocus(): Promise<void> { try { // Check if session is still valid const currentSession = await this.getCurrentSession() if (currentSession && !this.validateSession(currentSession)) { // Session expired, try to refresh await this.refreshSession() } } catch (error) { console.error('Session sync on focus failed:', error) } } } ``` --> ## πŸ” Security Features ### Token Management **ACTUAL IMPLEMENTATION**: Token refresh is handled automatically by Supabase client library, not by custom code. ```tsx // Token refresh happens automatically through: // 1. Supabase client's built-in refresh logic // 2. Next.js middleware calling auth.getUser() // 3. Auth state change listeners // Middleware automatically refreshes on every request export async function updateSession(request: NextRequest) { const supabase = createServerClient(url, key, { cookies: {...} }) // This call automatically refreshes expired tokens await supabase.auth.getUser() return response } // Client-side automatic refresh via auth state listener supabase.auth.onAuthStateChange(async (event, session) => { if (event === 'TOKEN_REFRESHED' && session) { // Session automatically updated in cookies // Optionally store in localStorage for cross-domain await sessionBridge.storeSession(session) } })

Session Timeout Configuration

Default Supabase Settings (configured in Supabase dashboard):

  • Access Token Expiry: 1 hour (3600 seconds)
  • Refresh Token Expiry: 30 days (configurable)
  • Automatic Refresh: Handled by Supabase client before expiry
  • Refresh Window: Tokens refreshed automatically ~5 minutes before expiry

Cookie Configuration (set by @supabase/ssr):

// Cookies are automatically configured with: { httpOnly: true, // Prevents XSS attacks secure: true, // HTTPS only sameSite: 'lax', // CSRF protection path: '/', // Available to all routes maxAge: 3600 // Matches access token expiry }

Session Validation

interface SessionValidator { validateToken(token: string): boolean validateExpiry(expiresAt: number): boolean validateUser(user: User): boolean validatePermissions(user: User, requiredRole?: string): boolean } class SessionValidationService implements SessionValidator { validateToken(token: string): boolean { if (!token || typeof token !== 'string') { return false } try { // Basic JWT structure validation const parts = token.split('.') if (parts.length !== 3) { return false } // Decode header and payload const header = JSON.parse(atob(parts[0])) const payload = JSON.parse(atob(parts[1])) // Check required fields return !!(header.alg && payload.sub && payload.exp) } catch (error) { return false } } validateExpiry(expiresAt: number): boolean { const now = Math.floor(Date.now() / 1000) return expiresAt > now } validateUser(user: User): boolean { return !!(user && user.id && user.email) } validatePermissions(user: User, requiredRole?: string): boolean { if (!requiredRole) return true // Get user role from metadata or profile const userRole = user.user_metadata?.role || user.app_metadata?.role const roleHierarchy = { user: 1, support: 2, admin: 3, } const userLevel = roleHierarchy[userRole] || 0 const requiredLevel = roleHierarchy[requiredRole] || 0 return userLevel >= requiredLevel } } ``` --> ## πŸ”§ Cross-App Navigation ### Actual Cross-App Implementation **Current State**: Cross-app navigation works differently based on deployment: 1. **Same Domain** (Production): Sessions automatically shared via cookies 2. **Different Ports** (Development): Session transfer via URL parameters ```tsx // packages/auth/src/session-bridge.ts - ACTUAL IMPLEMENTATION export class SessionBridge { private storageKey = 'mystoryflow-shared-session' /** * Create URL with embedded session for cross-domain transfer */ createCrossDomainUrl(targetUrl: string, currentSession?: any): string { if (!currentSession) return targetUrl try { const sessionData: SessionData = { access_token: currentSession.access_token, refresh_token: currentSession.refresh_token, expires_at: currentSession.expires_at, user: currentSession.user, } // Base64 encode session data const sessionParam = btoa(JSON.stringify(sessionData)) const url = new URL(targetUrl) url.searchParams.set('session', sessionParam) // Note: key is 'session', not 'session_token' return url.toString() } catch (error) { console.error('Error creating cross-domain URL:', error) return targetUrl } } /** * Initialize session from URL params or stored session */ async initializeSession(): Promise<boolean> { // 1. Check if valid session already exists const currentSession = await this.getCurrentSession() if (currentSession && currentSession.expires_at * 1000 > Date.now()) { await this.storeSession(currentSession) return true } // 2. Try to restore from localStorage const storedSession = await this.getStoredSession() if (storedSession) { const success = await this.setSupabaseSession(storedSession) if (success) return true } // 3. Check URL for session parameter const urlParams = new URLSearchParams(window.location.search) const sessionParam = urlParams.get('session') if (sessionParam) { const sessionData = JSON.parse(atob(sessionParam)) if (sessionData.expires_at * 1000 > Date.now()) { await this.storeSession(sessionData) this.cleanUpUrl() // Remove session from URL return true } } return false } }

Cross-App Behavior

Between Apps on Different Ports (Development):

  • Session transferred via URL parameter ?session=<base64_encoded_data>
  • Receiving app extracts session, calls supabase.auth.setSession()
  • URL cleaned up after session restoration
  • Session stored in both cookies and localStorage

Between Apps on Same Domain (Production):

  • Cookies automatically shared (same-origin)
  • No URL parameter needed
  • Direct navigation without session transfer

Limitations:

  • URL-based transfer only works for same Supabase project
  • Session data temporarily visible in URL (cleaned up immediately)
  • Requires JavaScript enabled for session restoration
export function AdminPortalLink({ children, className, }: { children: React.ReactNode className?: string }) { const navigator = useAppNavigator() const { user } = useAuth() const handleClick = async () => { try { if (!user) { toast.error('Please sign in to access admin portal') return } await navigator.openAdminPortal() } catch (error) { toast.error('Failed to open admin portal') } } return ( <button onClick={handleClick} className={className}> {children} </button> ) } export function CrossAppMenu() { const navigator = useAppNavigator() const { user, profile } = useAuth() if (!user) return null return ( <DropdownMenu> <DropdownMenuTrigger asChild> <Button variant="outline" size="sm"> <Grid3X3 className="h-4 w-4 mr-2" /> Apps </Button> </DropdownMenuTrigger> <DropdownMenuContent> <DropdownMenuItem onClick={() => navigator.openWebApp()}> <Home className="h-4 w-4 mr-2" /> Main App </DropdownMenuItem> {(profile?.role === 'admin' || profile?.role === 'support') && ( <DropdownMenuItem onClick={() => navigator.openAdminPortal()}> <Settings className="h-4 w-4 mr-2" /> Admin Portal </DropdownMenuItem> )} <DropdownMenuItem onClick={() => navigator.openToolsApp()}> <Wrench className="h-4 w-4 mr-2" /> Tools </DropdownMenuItem> </DropdownMenuContent> </DropdownMenu> ) } ``` --> ## 🚨 Error Handling ### Session Error Recovery ```tsx class SessionErrorHandler { private maxRetries = 3 private retryDelay = 1000 async handleSessionError(error: SessionError): Promise<void> { switch (error.type) { case 'TOKEN_EXPIRED': await this.handleTokenExpiry() break case 'INVALID_SESSION': await this.handleInvalidSession() break case 'NETWORK_ERROR': await this.handleNetworkError(error) break case 'STORAGE_ERROR': await this.handleStorageError(error) break default: await this.handleGenericError(error) } } private async handleTokenExpiry(): Promise<void> { try { // Attempt token refresh const newSession = await this.refreshToken() if (newSession) { // Session refreshed successfully return } } catch (error) { console.error('Token refresh failed:', error) } // Refresh failed, redirect to login this.redirectToLogin('Session expired') } private async handleInvalidSession(): Promise<void> { // Clear invalid session data await this.clearSession() // Redirect to login this.redirectToLogin('Invalid session') } private async handleNetworkError(error: SessionError): Promise<void> { // Implement retry logic with exponential backoff for (let attempt = 1; attempt <= this.maxRetries; attempt++) { try { await this.delay(this.retryDelay * Math.pow(2, attempt - 1)) // Retry the failed operation await this.retryOperation(error.operation) return } catch (retryError) { console.error(`Retry attempt ${attempt} failed:`, retryError) } } // All retries failed this.showErrorMessage('Network error. Please check your connection.') } private redirectToLogin(reason: string): void { const loginUrl = new URL('/login', window.location.origin) loginUrl.searchParams.set('reason', reason) window.location.href = loginUrl.toString() } } ``` --> ## πŸ“Š Session Analytics ### Usage Tracking ```tsx interface SessionAnalytics { trackSessionStart(userId: string): void trackSessionEnd(userId: string, duration: number): void trackCrossAppNavigation(from: string, to: string): void trackSessionRefresh(userId: string): void trackSessionError(error: SessionError): void } class SessionAnalyticsService implements SessionAnalytics { trackSessionStart(userId: string): void { this.sendEvent('session_start', { user_id: userId, timestamp: Date.now(), user_agent: navigator.userAgent, referrer: document.referrer, }) } trackSessionEnd(userId: string, duration: number): void { this.sendEvent('session_end', { user_id: userId, duration, timestamp: Date.now(), }) } trackCrossAppNavigation(from: string, to: string): void { this.sendEvent('cross_app_navigation', { from_app: from, to_app: to, timestamp: Date.now(), }) } private sendEvent(eventType: string, data: any): void { // Send to analytics service if (typeof window !== 'undefined' && window.gtag) { window.gtag('event', eventType, data) } } } ``` --> ## πŸ”§ Development Tools ### Session Debug Tools ```tsx // Development-only session debugging export function SessionDebugger() { const { session, user } = useAuth() const [debugInfo, setDebugInfo] = useState<any>(null) const collectDebugInfo = async () => { const sessionBridge = new SessionBridge( process.env.NEXT_PUBLIC_SUPABASE_URL!, process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY! ) const info = { currentSession: session, storedSession: sessionBridge.getStoredSession(), localStorageKeys: Object.keys(localStorage).filter( (key) => key.includes('supabase') || key.includes('mystoryflow') ), sessionStorageKeys: Object.keys(sessionStorage).filter( (key) => key.includes('supabase') || key.includes('mystoryflow') ), userAgent: navigator.userAgent, timestamp: new Date().toISOString(), } setDebugInfo(info) } if (process.env.NODE_ENV !== 'development') { return null } return ( <div className="fixed bottom-4 right-4 p-4 bg-black text-white rounded"> <button onClick={collectDebugInfo}>Debug Session</button> {debugInfo && ( <pre className="mt-2 text-xs max-w-md overflow-auto"> {JSON.stringify(debugInfo, null, 2)} </pre> )} </div> ) } ``` --> ## πŸ“‹ Session Management Summary ### What's Actually Implemented **Session Storage**: - βœ… HTTP-only cookies (primary, managed by @supabase/ssr) - βœ… localStorage (cross-domain transfer, key: 'mystoryflow-shared-session') - βœ… sessionStorage (hash validation) - βœ… URL parameters (cross-domain transfer via '?session=' param) **Token Management**: - βœ… Automatic refresh via Supabase client (not custom code) - βœ… Middleware refreshes on every request via `auth.getUser()` - βœ… Default access token expiry: 1 hour - βœ… Default refresh token expiry: 30 days **Route Protection**: - βœ… Server-side middleware protection (primary) - βœ… Client-side ProtectedRoute component (secondary) - βœ… Custom business logic (setup status, campaign checks) - βœ… Redirect with return-to URL support **Cross-App Features**: - βœ… Session transfer via URL parameters (development with different ports) - βœ… Cookie sharing (production on same domain) - βœ… SessionBridge class for cross-domain navigation - βœ… Auth state synchronization via localStorage events **Auth Context**: - βœ… React Context API with AuthProvider - βœ… User and profile state management - βœ… Setup status tracking (setup_completed, first_campaign_created) - βœ… Auth state change listeners ### What's NOT Implemented **NOT in actual code**: - ❌ Custom token refresh timers (Supabase handles this) - ❌ Manual refresh threshold logic (built into Supabase) - ❌ Role-based routing (no role hierarchy system) - ❌ Session version management - ❌ Session analytics tracking - ❌ SessionDebugger component - ❌ SessionErrorHandler with retry logic - ❌ Custom session migration strategies ### Key Configuration Settings **Supabase Settings** (Dashboard Configuration):

Access Token Expiry: 3600 seconds (1 hour) Refresh Token Expiry: 2592000 seconds (30 days) JWT Algorithm: HS256 Auto Refresh: Enabled (built-in)

**Cookie Settings** (Auto-configured by @supabase/ssr):

httpOnly: true secure: true (production) sameSite: β€˜lax’ path: ’/’ maxAge: 3600 (1 hour)

**Environment Variables Required**: ```bash NEXT_PUBLIC_SUPABASE_URL=https://qrlygafaejovxxlnkpxa.supabase.co NEXT_PUBLIC_SUPABASE_ANON_KEY=<anon_key> SUPABASE_SERVICE_ROLE_KEY=<service_role_key> # For admin operations

πŸš€ Best Practices

Implementation Guidelines

  1. Session Management

    • βœ… Let Supabase handle token refresh automatically
    • βœ… Use middleware for server-side session refresh
    • βœ… Don’t implement custom refresh timers
    • βœ… Trust @supabase/ssr for cookie management
  2. Cross-App Navigation

    • βœ… Use SessionBridge for development (different ports)
    • βœ… Rely on cookie sharing for production (same domain)
    • ⚠️ Session data temporarily visible in URL
    • βœ… URL is cleaned immediately after session restoration
  3. Route Protection

    • βœ… Use middleware for basic auth checks
    • βœ… Use ProtectedRoute for component-level protection
    • βœ… Combine both for complex business logic
    • βœ… Always call auth.getUser() in middleware
  4. Performance

    • βœ… Middleware runs on every request (automatic refresh)
    • βœ… Client components debounce user/profile fetches
    • βœ… Use loading states to prevent layout shifts
    • βœ… Cache profile data in AuthContext

Configuration Checklist

  • Environment variables configured
  • Supabase authentication enabled
  • HTTP-only cookies working via @supabase/ssr
  • Middleware refreshing sessions on every request
  • Cross-app navigation working (URL params for dev)
  • Protected routes working (middleware + client)
  • Auth state synchronization across tabs
  • Role-based access control (not implemented)
  • Session analytics (not implemented)

πŸ” Troubleshooting Common Issues

Session Not Persisting Across Page Loads

Cause: Middleware not updating session cookies properly

Solution:

// Ensure middleware is called on all routes export const config = { matcher: [ '/((?!_next/static|_next/image|favicon.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)', ], } // Make sure auth.getUser() is called await supabase.auth.getUser() // This triggers token refresh

Cross-App Navigation Not Working

Cause: Session parameter not being extracted or URL not properly formed

Solution:

// Check SessionBridge initialization const sessionBridge = createSessionBridge(url, key) await sessionBridge.initializeSession() // Must be called on app mount // Verify session parameter name url.searchParams.set('session', encodedData) // NOT 'session_token'

Token Expired Errors

Cause: Middleware not running or refresh token expired

Solution:

  • Verify middleware matcher includes the route
  • Check refresh token hasn’t expired (30 days default)
  • Ensure user re-authenticates if refresh token is invalid
  • Middleware automatically handles refresh if token is valid

Auth State Not Syncing Across Tabs

Cause: localStorage events not firing or SessionBridge not initialized

Solution:

// Ensure SessionBridge.setupSessionSync() is called sessionBridge.setupSessionSync() // Verify localStorage key matches localStorage.getItem('mystoryflow-shared-session')

πŸ“ Documentation vs Implementation Notes

Important: This documentation has been updated to reflect the actual implementation in the codebase. Many code examples from the original documentation were aspirational or conceptual rather than implemented.

Key Differences from Original Docs:

  1. Token refresh is automatic via Supabase, not custom timers
  2. Session storage primarily uses cookies, not multi-layer custom storage
  3. Cross-app navigation uses simple URL params, not complex transfer mechanisms
  4. Role-based routing is not implemented
  5. Session analytics and debugging tools are not implemented
  6. Error recovery is basic, not the sophisticated retry system documented

Where to Find Actual Code:

  • Session Bridge: /packages/auth/src/session-bridge.ts
  • Auth Context: /packages/auth/src/auth-context.tsx
  • Middleware: /apps/web-app/lib/supabase/middleware.ts
  • Protected Route: /packages/auth/src/protected-route.tsx
  • Client Creation: /packages/auth/src/client.ts