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
- Session Bridge - Cross-domain session transfer mechanism using URL parameters
- Auth Context - React context for authentication state management
- Middleware - Server-side session refresh and route protection
- Cookie-based Storage - HTTP-only cookies managed by Supabase SSR
- Token Management - Automatic JWT token refresh via Supabase
- 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
- HTTP Cookies (Primary) - Managed by Supabase SSR, automatically set/refreshed by middleware
- localStorage (Backup) - Used for cross-domain transfers and as fallback
- URL Parameters (Transfer) - Only used during cross-domain navigation
- 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
Navigation Components
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
-
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
-
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
-
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
-
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 refreshCross-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:
- Token refresh is automatic via Supabase, not custom timers
- Session storage primarily uses cookies, not multi-layer custom storage
- Cross-app navigation uses simple URL params, not complex transfer mechanisms
- Role-based routing is not implemented
- Session analytics and debugging tools are not implemented
- 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