Source:
ocean/docs/patterns/authentication.md| ✏️ Edit on GitHub
Authentication Patterns
Ocean uses Supabase Auth with passwordless OTP (One-Time Password) authentication, featuring sophisticated session management, multi-tab synchronization, and JWT monitoring.
Authentication Flow
Passwordless OTP Login
// 1. Request OTP
const { error } = await supabase.auth.signInWithOtp({
email: 'user@example.com',
options: {
emailRedirectTo: `${window.location.origin}/auth/callback`,
},
})
// 2. Verify OTP (10-minute expiry)
const { error } = await supabase.auth.verifyOtp({
email: 'user@example.com',
token: '123456',
type: 'email',
})
Using the Auth Context
import { useAuth } from '@/contexts/auth-context'
function MyComponent() {
const {
user, // Current user or null
profile, // User profile data
isLoading, // Initial load state
isAuthenticated,// Boolean auth state
signOut // Sign out function
} = useAuth()
if (isLoading) {
return <AuthLoadingSkeleton />
}
if (!isAuthenticated) {
return <Navigate to="/login" />
}
return <div>Welcome, {profile?.firstName}!</div>
}
Protected Routes
Route-Level Protection
// In your route file (e.g., dashboard.tsx)
import { Navigate } from '@tanstack/react-router'
import { useAuth } from '@/contexts/auth-context'
function DashboardLayout() {
const { user, isLoading } = useAuth()
if (isLoading) {
return <DashboardLoadingSkeleton />
}
if (!user) {
return <Navigate to="/login" />
}
return <DashboardContent />
}
Component-Level Protection
import { withAuth } from '@/components/with-auth'
// Automatically redirects to login if not authenticated
export default withAuth(MyProtectedComponent)
// With custom loading component
export default withAuth(MyProtectedComponent, {
loadingComponent: <CustomLoader />,
redirectTo: '/custom-login'
})
Multi-Tab Synchronization
Authentication state automatically syncs across browser tabs:
// No setup required! The auth context handles:
// - Broadcast channel communication
// - Storage event listeners
// - State reconciliation
// When user logs in/out in one tab:
// 1. Auth state updates locally
// 2. Broadcasts to other tabs
// 3. Other tabs update automatically
// 4. All tabs stay in sync
JWT Monitoring
Real-Time Token Monitoring
// The JWTMonitor component is included in the app root
// It provides:
// - Visual countdown to token expiry
// - Automatic refresh attempts
// - Health status indicators
// Access JWT info programmatically
import { useJWT } from '@/hooks/use-jwt'
function TokenStatus() {
const {
token,
expiresAt,
timeUntilExpiry,
isExpired,
isExpiringSoon // Within 5 minutes
} = useJWT()
if (isExpiringSoon) {
return <Alert>Session expiring soon</Alert>
}
return <div>Session expires in {timeUntilExpiry}</div>
}
Session Management
Automatic Session Restoration
// Sessions are automatically restored on page load
// The auth context handles:
// 1. Check localStorage for session
// 2. Validate session with Supabase
// 3. Fetch user profile
// 4. Set loading states appropriately
// Manual session refresh
const { data, error } = await supabase.auth.refreshSession()
Session Persistence
// Configure session persistence
const { data, error } = await supabase.auth.signInWithOtp({
email,
options: {
emailRedirectTo: redirectUrl,
shouldCreateUser: true,
data: {
// Custom user metadata
firstName: 'John',
lastName: 'Doe',
},
},
})
Race Condition Protection
The auth context includes sophisticated race condition handling:
// Multiple simultaneous auth operations are safe
// The context uses:
// - Initialization flags
// - Promise deduplication
// - State locks
// Example: These won't cause issues
await Promise.all([
supabase.auth.refreshSession(),
supabase.auth.getUser(),
supabase.auth.updateUser({ data: { name: 'New' } }),
])
Error Handling
Auth-Specific Error Boundary
import { AuthErrorBoundary } from '@/components/error-boundary'
// Catches auth-specific errors
// - Session expiry
// - Invalid tokens
// - Network issues
<AuthErrorBoundary>
<App />
</AuthErrorBoundary>
Auth Error Types
import { AuthError } from '@/lib/errors'
// Common auth errors
if (error.message.includes('expired')) {
throw new AuthError('Session expired', { code: 'SESSION_EXPIRED' })
}
if (error.message.includes('Invalid login')) {
throw new AuthError('Invalid credentials', { code: 'INVALID_CREDENTIALS' })
}
Organization Context
Works seamlessly with auth:
import { useOrganization } from '@/contexts/organization-context'
function OrgSelector() {
const { user } = useAuth()
const {
currentOrganization,
organizations,
switchOrganization,
isLoading
} = useOrganization()
// Organization context automatically loads when user is authenticated
if (!user) return null
return (
<Select
value={currentOrganization?.id}
onValueChange={switchOrganization}
>
{organizations.map(org => (
<SelectItem key={org.id} value={org.id}>
{org.name}
</SelectItem>
))}
</Select>
)
}
PostHog Integration
Authentication events are automatically tracked:
// Automatic events:
// - user_signed_up
// - user_signed_in
// - user_signed_out
// - session_refreshed
// The PostHogAuthTracker component handles this
// No manual tracking needed!
Security Best Practices
Secure Token Storage
// Tokens are stored in httpOnly cookies (when possible)
// and localStorage as fallback
// Never store tokens in:
// - Regular cookies
// - Session storage
// - Global variables
API Authentication
// All API calls automatically include auth headers
const apiClient = useApiClient() // Auth headers included
// Manual API calls
const response = await fetch('/api/endpoint', {
headers: {
Authorization: `Bearer ${session.access_token}`,
'Content-Type': 'application/json',
},
})
Testing Authentication
Mock Auth in Tests
import { mockAuth } from '@/test-utils/auth'
// In your test
const { user, session } = mockAuth({
email: 'test@example.com',
profile: {
firstName: 'Test',
lastName: 'User'
}
})
// Render with mocked auth
render(
<AuthProvider initialSession={session}>
<MyComponent />
</AuthProvider>
)
Common Patterns
Conditional Rendering
function NavBar() {
const { isAuthenticated, profile } = useAuth()
return (
<nav>
{isAuthenticated ? (
<>
<span>Welcome, {profile?.firstName}</span>
<Button onClick={signOut}>Sign Out</Button>
</>
) : (
<Button href="/login">Sign In</Button>
)}
</nav>
)
}
Auth-Dependent Queries
function UserData() {
const { user } = useAuth()
const { data } = useQuery({
queryKey: ['userData', user?.id],
queryFn: () => fetchUserData(user!.id),
enabled: !!user // Only run when authenticated
})
return <DataDisplay data={data} />
}
Redirect After Login
// Store intended destination
const redirectUrl = encodeURIComponent(window.location.pathname)
// In login component
const handleLogin = async (email: string) => {
await supabase.auth.signInWithOtp({
email,
options: {
emailRedirectTo: `${window.location.origin}/auth/callback?redirectUrl=${redirectUrl}`,
},
})
}
// In callback handler
const redirectUrl = searchParams.get('redirectUrl') || '/dashboard'
router.push(decodeURIComponent(redirectUrl))