Skip to main content
Full technical details in your project at /docs/modules/Auth.md

What You Get

  • 3 auth providers - Apple, Google, Email (all via Supabase)
  • Session persistence - Users stay logged in automatically
  • Proactive token refresh - 60s before expiry, with 3-attempt retry
  • Keychain storage - Secure, encrypted token persistence
  • AsyncStream state - Observable authentication state
  • MockAuthClient - DEBUG mode works without backend
Time saved: 20-32 hours of auth flows, token management, session handling, and comprehensive testing.

Key Components

AuthClient Protocol

protocol AuthClient {
    func signInWithApple() async throws -> AuthUser
    func signInWithEmail(email: String, password: String) async throws -> AuthUser
    func signUpWithEmail(email: String, password: String) async throws -> AuthUser
    func resetPassword(email: String) async throws
    func signOut() async throws
    func currentUser() async -> AuthUser?
    func authStates() -> AsyncStream<AuthState>
}

Production Setup (From Real Code)

How authentication is set up in CompositionRoot.swift:
// 1. Configure Supabase
let authConfig = AuthConfig(
    supabaseURL: URL(string: ProcessInfo.processInfo.environment["SUPABASE_URL"]!)!,
    supabaseAnonKey: ProcessInfo.processInfo.environment["SUPABASE_ANON_KEY"]!
)

// 2. Create HTTP client for Supabase API
let supabaseHTTPClient = Auth.SupabaseHTTPClient(
    baseURL: authConfig.supabaseURL,
    session: .shared
)

// 3. Create Apple Sign In coordinator
let appleSignInCoordinator = Auth.AppleSignInCoordinator()

// 4. Create SessionManager (production auth client)
let sessionManager = Auth.SessionManager(
    httpClient: supabaseHTTPClient,
    keychain: keychainStore,
    apple: appleSignInCoordinator,
    config: authConfig
)

// SessionManager automatically:
// - Loads saved session from Keychain on init
// - Schedules proactive token refresh (60s before expiry)
// - Emits auth state changes via AsyncStream
// - Handles all 3 providers (Apple, Google, Email)

What SessionManager Does

On App Launch:
  1. Loads session from Keychain
  2. Validates token expiry
  3. Auto-refreshes if expiring soon
  4. Emits .authenticated or .unauthenticated state
On Sign In:
  1. Exchanges provider token with Supabase
  2. Saves access + refresh tokens to Keychain
  3. Schedules proactive refresh
  4. Emits .authenticated(user) state
Token Refresh:
  • Scheduled 60s before token expiry
  • Retries up to 3 times with backoff
  • Handles Supabase token rotation
  • Clears session on refresh failure
Production Quality:
  • ✅ Race-safe refresh mutex
  • ✅ Cancellation-aware
  • ✅ Comprehensive error handling
  • ✅ Fully tested (85%+ coverage)

Token Management

Automatic Refresh

// SessionManager handles:
// - Token refresh before expiry
// - Retry on refresh failure
// - Keychain persistence
// - Logout on refresh failure

Secure Storage

All tokens stored in Keychain:
  • ✅ Access token
  • ✅ Refresh token
  • ✅ Never in UserDefaults
  • ✅ OS-level encryption

Auth State Observation

// Observe auth state changes
for await state in authClient.authStates() {
    switch state {
    case .signedIn(let user):
        // Navigate to authenticated content
    case .signedOut:
        // Show sign-in screen
    }
}

Customization Examples

Add Social Provider

// Add to AuthClient protocol
func signInWithGoogle() async throws -> AuthUser

// Implement in SupabaseAuthClient
func signInWithGoogle() async throws -> AuthUser {
    // Use Supabase's Google OAuth
    let session = try await supabase.auth.signIn(
        provider: .google
    )
    return AuthUser(from: session.user)
}

Add Custom Fields

// Extend AuthUser
struct AuthUser {
    let id: String
    let email: String
    var displayName: String?
    var photoURL: URL?
    var customField: String?  // Add custom fields
}

Mock Auth in DEBUG

Enabled by default! No setup needed.
// Feature flag controls it:
public static var shouldUseMock: Bool {
    #if DEBUG
    // AUTH_BYPASS env var can disable it
    return ProcessInfo.processInfo.environment["AUTH_BYPASS"] != "0"
    #else
    return false  // Never in RELEASE
    #endif
}
To use real auth in DEBUG:
  1. Edit Scheme → Run → Environment Variables
  2. Add AUTH_BYPASS = 0
  3. Configure Supabase in Config/Secrets.xcconfig

Key Files

ComponentLocation
ProtocolPackages/Auth/Sources/Auth/Protocols/AuthClient.swift
SupabasePackages/Auth/Sources/Auth/Supabase/
Apple Sign InPackages/Auth/Sources/Auth/Apple/
SessionPackages/Auth/Sources/Auth/Session/
MockPackages/Auth/Sources/Auth/Mock/

Dependencies

  • Core - Error handling, logging
  • Networking - HTTP client (for Supabase)

Used By

  • All features - Protected by authentication
  • CompositionRoot - Observes auth state
  • LaunchRouter - Auth-gated navigation

Best Practices

  • Always store in Keychain
  • Never log tokens
  • Refresh before expiry
  • Clear on sign out
  • Map to AppError
  • User-friendly messages
  • Retry transient failures
  • Log technical details
  • Use MockAuthClient
  • Test token refresh
  • Test error scenarios
  • Test state transitions

Learn More

Test Coverage

87%+ - Comprehensive auth testing Tests include:
  • Sign in/up flows
  • Token refresh
  • Session management
  • Apple Sign In coordination
  • Error scenarios
  • State transitions
I