Skip to main content

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))