Skip to Content
📚 MyStoryFlow Docs — Your guide to preserving family stories

Cross-App Authentication System

The MyStoryFlow ecosystem uses a cross-app authentication system that allows users to move between different applications (web app, admin app, tools app) with authentication context. The implementation differs across apps based on their specific needs.

🏗️ Architecture Overview

Core Components

  1. AuthProvider Component (@mystoryflow/auth) - Shared authentication context for React applications
  2. SessionBridge Class (@mystoryflow/auth) - Available for cross-domain session transfer (currently not exported from package)
  3. Supabase SSR - Server-side session management with cookies
  4. Middleware - Route protection and session validation

Current Implementation Status

Note: While the SessionBridge class exists in the codebase (packages/auth/src/session-bridge.ts), it is not currently exported from the @mystoryflow/auth package and is not actively used in production apps. The primary authentication mechanism uses Supabase SSR with cookies.

Session Storage Strategy

The system currently uses these mechanisms:

  • HTTP-only Cookies - Primary storage via Supabase SSR (server-side)
  • Supabase Auth Client - Client-side session management
  • Browser Storage - localStorage/sessionStorage for client state (not for cross-domain transfer)
  • Session Validation - Middleware checks for protected routes

🚀 Implementation Guide

1. Package Setup

The auth package is available as a workspace package:

// package.json { "dependencies": { "@mystoryflow/auth": "workspace:*", "@supabase/ssr": "^0.6.1" } }

Current exports from @mystoryflow/auth:

  • AuthProvider / useAuth - Client-side authentication context
  • ProtectedRoute - Route protection component
  • AuthButton - Pre-built auth UI component
  • createClient / createClientFromEnv - Supabase client utilities
  • Role-based routing utilities

Not exported (but exists in codebase):

  • SessionBridge class and related functions

2. Setting Up AuthProvider

Web App Example (apps/web-app/app/components/ClientAuthProvider.tsx):

'use client' import { AuthProvider } from '@mystoryflow/auth' import { ReactNode } from 'react' export function ClientAuthProvider({ children }: { children: ReactNode }) { return ( <AuthProvider supabaseUrl={process.env.NEXT_PUBLIC_SUPABASE_URL!} supabaseAnonKey={process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!} > {children} </AuthProvider> ) }

Admin App Example (apps/admin-app/src/app/providers.tsx):

'use client' import { AuthProvider } from '@mystoryflow/auth' export function Providers({ children }: { children: React.ReactNode }) { const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL const supabaseAnonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY if (!supabaseUrl || !supabaseAnonKey) { return <div>Configuration Error: Missing environment variables</div> } return ( <AuthProvider supabaseUrl={supabaseUrl} supabaseAnonKey={supabaseAnonKey} > {children} </AuthProvider> ) }

3. Using Authentication in Components

import { useAuth } from '@mystoryflow/auth' export function UserProfile() { const { user, profile, loading, signOut } = useAuth() if (loading) return <div>Loading...</div> if (!user) return <div>Please sign in</div> return ( <div> <h1>Welcome, {profile?.full_name || user.email}</h1> <p>Role: {profile?.role}</p> <button onClick={signOut}>Sign Out</button> </div> ) }

Available Auth Context Values:

interface AuthContextType { user: User | null profile: Profile | null loading: boolean error: string | null setupCompleted: boolean firstCampaignCreated: boolean incompleteCampaign: IncompleteCampaign | null checkSetupStatus: () => Promise<SetupStatus> completeSetup: () => Promise<void> signIn: (email: string, password: string) => Promise<void> signUp: (email: string, password: string, metadata: Record<string, unknown>) => Promise<void> signOut: () => Promise<void> signInWithGoogle: () => Promise<void> signInWithApple: () => Promise<void> signInWithMagicLink: (email: string) => Promise<void> sendPasswordReset: (email: string) => Promise<void> updateProfile: (updates: Partial<Profile>) => Promise<void> }

4. Server-Side Middleware (Web App)

Middleware Configuration (apps/web-app/middleware.ts):

import { type NextRequest } from 'next/server' import { updateSession } from '@/lib/supabase/middleware' export async function middleware(request: NextRequest) { return await updateSession(request) } export const config = { matcher: [ '/((?!_next/static|_next/image|favicon.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)', ], }

Session Update Function (apps/web-app/lib/supabase/middleware.ts):

import { createServerClient } from '@supabase/ssr' import { NextResponse, type NextRequest } from 'next/server' export async function updateSession(request: NextRequest) { let response = NextResponse.next({ request: { headers: request.headers } }) const supabase = createServerClient( process.env.NEXT_PUBLIC_SUPABASE_URL!, process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!, { cookies: { get(name: string) { return request.cookies.get(name)?.value }, set(name: string, value: string, options: CookieOptions) { request.cookies.set({ name, value, ...options }) response.cookies.set({ name, value, ...options }) }, remove(name: string, options: CookieOptions) { request.cookies.set({ name, value: '', ...options }) response.cookies.set({ name, value: '', ...options }) }, }, } ) // Always update session for SSR await supabase.auth.getUser() // Route protection logic const pathname = request.nextUrl.pathname const isDashboard = pathname.startsWith('/dashboard') if (isDashboard) { const { data: { user } } = await supabase.auth.getUser() if (!user) { // Redirect to login return NextResponse.redirect(new URL('/login', request.url)) } // Check setup completion const { data: profile } = await supabase .from('profiles') .select('setup_completed') .eq('id', user.id) .single() if (!profile?.setup_completed) { return NextResponse.redirect(new URL('/campaigns/create?step=1', request.url)) } } return response }

5. Cross-App Navigation (Current Implementation)

Simple Link-Based Navigation (apps/web-app/app/components/AdminPortalLink.tsx):

'use client' import { useEffect, useState } from 'react' import { createClientFromEnv } from '@mystoryflow/auth' import { Shield, ExternalLink } from 'lucide-react' export default function AdminPortalLink() { const [isAdmin, setIsAdmin] = useState(false) const supabase = createClientFromEnv() useEffect(() => { async function checkAdminStatus() { const { data: { user } } = await supabase.auth.getUser() if (user) { const { data: profile } = await supabase .from('profiles') .select('role') .eq('id', user.id) .single() const adminRoles = ['admin', 'moderator', 'super_admin', 'customer_support'] setIsAdmin(adminRoles.includes(profile?.role)) } } checkAdminStatus() }, [supabase]) if (!isAdmin) return null const handleAdminPortalClick = () => { const adminUrl = process.env.NEXT_PUBLIC_ADMIN_APP_URL || 'http://localhost:3003' // Simple navigation - session handled by cookies window.open(`${adminUrl}/dashboard`, '_blank') } return ( <button onClick={handleAdminPortalClick} className="..."> <Shield className="h-4 w-4" /> <span>Admin Portal</span> <ExternalLink className="h-3 w-3" /> </button> ) }

Current Behavior:

  • Sessions are managed via HTTP-only cookies set by Supabase SSR
  • When navigating between apps on the same domain, cookies are automatically shared
  • For different domains, users need to authenticate separately in each app
  • The SessionBridge class exists but is not currently used for cross-domain transfer

🔄 How Authentication Works Across Apps

Current Authentication Flow

  1. User logs in to any app (web, admin, or tools)
  2. Supabase sets HTTP-only cookies containing session tokens
  3. Session is validated on both client and server side
  4. AuthProvider syncs state across the app
  5. Middleware protects routes by checking session validity
  6. Profile data is fetched from the profiles table
  7. User can navigate within the same app seamlessly

Same-Domain Session Sharing

When apps are deployed on the same domain (e.g., mystoryflow.com):

  • Cookies are automatically shared across subdomains
  • Navigation between app.mystoryflow.com and admin.mystoryflow.com preserves sessions
  • No additional code needed for session transfer

Cross-Domain Scenarios (Development)

During local development, apps run on different ports (localhost:3000, localhost:3003):

  • Each app maintains separate sessions via Supabase auth
  • Users may need to authenticate separately in each app
  • The SessionBridge class was built to handle this but is not currently integrated

Tools App Session Management

The tools app uses a different approach:

// tools-app/src/lib/session.ts export class SessionManager { static async initializeSession(userId?: string): Promise<string> { const sessionId = this.getSessionId() // Create session record in tools_sessions table await supabaseTools.from('tools_sessions').insert({ session_id: sessionId, user_id: userId || null, ip_address: await this.getClientIP(), user_agent: navigator.userAgent, }) return sessionId } }

Tools App Specifics:

  • Maintains its own tools_sessions table for analytics
  • Supports both authenticated and anonymous usage
  • Tracks session activity and usage patterns

🔧 Configuration

Environment Variables

Each app requires these environment variables:

# Supabase Configuration (Required) NEXT_PUBLIC_SUPABASE_URL=https://your-project.supabase.co NEXT_PUBLIC_SUPABASE_ANON_KEY=your-anon-key # App URLs for cross-navigation (Optional) NEXT_PUBLIC_WEB_APP_URL=http://localhost:3000 NEXT_PUBLIC_ADMIN_APP_URL=http://localhost:3003 NEXT_PUBLIC_TOOLS_APP_URL=http://localhost:3001

TypeScript Types

From @mystoryflow/auth package:

// User type (from Supabase) export type User = SupabaseUser // Profile structure export type Profile = { id: string full_name?: string phone?: string avatar_url?: string role?: string setup_completed?: boolean first_campaign_created?: boolean [key: string]: unknown } // Setup status export interface SetupStatus { setupCompleted: boolean firstCampaignCreated: boolean incompleteCampaign?: IncompleteCampaign | null } export interface IncompleteCampaign { id: string currentStep: string progress: Record<string, unknown> | null }

SessionBridge types (exists but not exported):

// packages/auth/src/session-bridge.ts export interface SessionData { access_token: string refresh_token: string expires_at: number user: any }

🔍 Debugging

Console Logs

The AuthProvider provides auth state change logs:

🔄 Auth state changed: SIGNED_IN Auth error: [error details] Error fetching user and profile: [error details] Auth initialization timed out after 5 seconds

Tools App Session Logs:

✅ Valid session already exists 🔍 No valid session found, checking stored session... 📦 Found stored session, restoring... ❌ No valid session found

Common Issues

Authentication Not Working

Check Supabase Configuration:

// Verify environment variables are set console.log('Supabase URL:', process.env.NEXT_PUBLIC_SUPABASE_URL) console.log('Has Anon Key:', !!process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY)

Check User Session:

const { user, loading, error } = useAuth() if (loading) { console.log('Auth loading...') return <Loading /> } if (error) { console.error('Auth error:', error) return <div>Error: {error}</div> } if (!user) { console.log('No user found') return <LoginForm /> } console.log('User authenticated:', user.id) return <AuthenticatedApp />

Middleware Redirect Loops

// Check middleware logic for infinite loops // Ensure protected routes don't redirect to themselves if (isDashboard && !user) { // Make sure /login is not a protected route return NextResponse.redirect(new URL('/login', request.url)) }

Profile Not Loading

const { user, profile, loading } = useAuth() useEffect(() => { if (!loading && user && !profile) { console.error('Profile not found for user:', user.id) // Profile may not exist in database } }, [user, profile, loading])

🔒 Security Considerations

Session Data Protection

The current implementation uses secure practices:

  1. HTTP-only Cookies - Session tokens stored in HTTP-only cookies (not accessible to JavaScript)
  2. Secure Flag - Cookies use the Secure flag in production (HTTPS only)
  3. SameSite Protection - Cookies configured with SameSite attribute to prevent CSRF
  4. Token Expiration - Sessions automatically expire and refresh via Supabase
  5. Server-Side Validation - Middleware validates sessions on every request

Best Practices

Production Configuration:

// Supabase automatically handles secure cookie storage in production // No manual cookie configuration needed // For custom storage (if needed): const supabase = createBrowserClient(url, key, { auth: { storage: { // Custom storage implementation getItem: (key) => localStorage.getItem(key), setItem: (key, value) => localStorage.setItem(key, value), removeItem: (key) => localStorage.removeItem(key), }, // Supabase SSR handles secure cookies automatically detectSessionInUrl: true, flowType: 'pkce' // Use PKCE flow for better security }, })

Middleware Security:

// Always validate user on protected routes const { data: { user } } = await supabase.auth.getUser() if (!user) { return NextResponse.redirect(new URL('/login', request.url)) } // Verify user roles for admin routes const { data: profile } = await supabase .from('profiles') .select('role') .eq('id', user.id) .single() if (!['admin', 'super_admin'].includes(profile?.role)) { return NextResponse.redirect(new URL('/unauthorized', request.url)) }

📱 Multi-App Support

Current Applications

Production Apps (using @mystoryflow/auth):

  • Web App (localhost:3000) - Main user interface with full AuthProvider integration
  • Admin App (localhost:3003) - Administrative dashboard with role-based access
  • Tools App (localhost:3001) - Utility tools with custom session tracking
  • 🔄 Marketing Site (localhost:3002) - Public marketing (minimal auth needed)
  • 🔄 Docs App (localhost:3004) - Documentation (public access)

App-Specific Implementations

Web App:

  • Full AuthProvider with profile management
  • Setup completion tracking
  • Campaign progress tracking
  • Middleware route protection

Admin App:

  • Standard AuthProvider setup
  • Role-based access control
  • Admin-specific features

Tools App:

  • Simple auth with useAuth
  • Custom SessionManager for analytics
  • Supports anonymous and authenticated users
  • Tracks tool usage per session

Adding New Apps

  1. Install the auth package:
npm install @mystoryflow/auth --workspace=apps/your-app
  1. Add AuthProvider to app layout:
// apps/your-app/app/layout.tsx import { AuthProvider } from '@mystoryflow/auth' export default function RootLayout({ children }) { return ( <html> <body> <AuthProvider supabaseUrl={process.env.NEXT_PUBLIC_SUPABASE_URL!} supabaseAnonKey={process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!} > {children} </AuthProvider> </body> </html> ) }
  1. Configure environment variables:
NEXT_PUBLIC_SUPABASE_URL=https://your-project.supabase.co NEXT_PUBLIC_SUPABASE_ANON_KEY=your-anon-key
  1. Add middleware (if needed):
// apps/your-app/middleware.ts import { type NextRequest } from 'next/server' import { updateSession } from '@/lib/supabase/middleware' export async function middleware(request: NextRequest) { return await updateSession(request) }

🚀 Testing

Manual Testing

1. Start multiple apps:

# Terminal 1 - Web App cd apps/web-app && npm run dev # Terminal 2 - Admin App cd apps/admin-app && npm run dev # Terminal 3 - Tools App cd apps/tools-app && npm run dev

2. Test authentication flow:

  • Login to web app at localhost:3000
  • Verify user profile loads correctly
  • Check browser cookies (should see Supabase auth cookies)
  • Try accessing protected routes (e.g., /dashboard)

3. Test cross-app navigation:

  • From web app, click Admin Portal link (if you have admin role)
  • Opens admin app in new tab
  • Note: In local development, you’ll need to login again in admin app
  • In production (same domain), session will be shared via cookies

4. Test role-based access:

  • Login as regular user
  • Admin Portal link should not appear
  • Login as admin user
  • Admin Portal link should be visible

Automated Testing

Auth Context Tests (apps/web-app/__tests__/auth/auth-context.test.tsx):

import { renderHook, waitFor } from '@testing-library/react' import { AuthProvider, useAuth } from '@mystoryflow/auth' describe('AuthProvider', () => { it('should provide auth context', async () => { const wrapper = ({ children }) => ( <AuthProvider supabaseUrl={process.env.NEXT_PUBLIC_SUPABASE_URL!} supabaseAnonKey={process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!} > {children} </AuthProvider> ) const { result } = renderHook(() => useAuth(), { wrapper }) await waitFor(() => { expect(result.current.loading).toBe(false) }) expect(result.current.user).toBeDefined() }) })

Session Management Tests (apps/web-app/tests/e2e/critical/session-management.spec.ts):

import { test, expect } from '@playwright/test' test.describe('Session Management', () => { test('should maintain session across page reloads', async ({ page }) => { await page.goto('/login') // Login flow... await page.waitForURL('/dashboard') // Reload page await page.reload() // Should still be authenticated await expect(page).toHaveURL('/dashboard') }) })

📈 Performance

Current Optimizations

AuthProvider:

  • Debounced Updates - Auth state changes debounced to prevent excessive re-renders
  • Cached Profile Data - Profile data cached to reduce database queries
  • Loading Timeout - 5-second failsafe prevents infinite loading states
  • Lazy Initialization - Auth context only initializes client-side

Middleware:

  • Efficient Cookie Handling - Server-side session validation via cookies
  • Selective Database Queries - Only queries database for protected routes
  • Route-Specific Logic - Conditional checks based on pathname

Tools App:

  • Session Caching - localStorage-based session ID caching
  • Periodic Updates - Session activity updated every 30 seconds
  • Minimal Database Calls - Only creates session record on initialization

Performance Metrics

  • Auth initialization: ~100-300ms
  • Profile fetch: ~50-150ms
  • Middleware validation: ~10-50ms
  • Session persistence: Automatic via cookies

🔮 Future Enhancements

Potential Improvements

  1. Export SessionBridge - Make SessionBridge class available from package for cross-domain scenarios
  2. URL-Based Session Transfer - Implement the existing SessionBridge for development environments
  3. Multi-Tenant Support - Organization-based session isolation
  4. Session Analytics Dashboard - Track authentication metrics across apps
  5. Offline Support - Handle auth state when network is unavailable

SessionBridge Integration (Future)

The SessionBridge class exists and could be enabled:

// packages/auth/src/index.ts (add these exports) export { SessionBridge, createSessionBridge, useSessionBridge } from './session-bridge' // Then use in apps: import { createSessionBridge } from '@mystoryflow/auth' const sessionBridge = createSessionBridge(supabaseUrl, anonKey) await sessionBridge.initializeSession() sessionBridge.setupSessionSync() // Cross-domain navigation: const url = sessionBridge.createCrossDomainUrl(targetUrl, session) window.open(url, '_blank')

📞 Support

Troubleshooting Checklist

Authentication Issues:

  1. Check browser console for auth errors
  2. Verify environment variables are set (NEXT_PUBLIC_SUPABASE_URL, NEXT_PUBLIC_SUPABASE_ANON_KEY)
  3. Ensure Supabase project is active and accessible
  4. Check browser cookies for Supabase session tokens
  5. Test with a fresh browser session (clear cookies)

Cross-App Navigation:

  1. Verify apps are running on correct ports
  2. Check that user has appropriate role for target app
  3. In development, expect to re-authenticate in each app
  4. In production, ensure apps are on same domain for cookie sharing

Profile Loading:

  1. Verify user exists in profiles table
  2. Check database RLS policies allow profile access
  3. Ensure profile fetch query matches schema
  4. Check for any database connection issues

Middleware Redirects:

  1. Check middleware matcher patterns
  2. Verify redirect logic doesn’t create loops
  3. Ensure login/signup routes are not protected
  4. Test with different user roles

Support Resources

  • Supabase Logs: Check auth logs in Supabase dashboard
  • Browser DevTools: Network tab for failed requests
  • Server Logs: Check Next.js server logs for errors
  • Database: Verify user and profile data in Supabase tables