> ## Documentation Index
> Fetch the complete documentation index at: https://docs.swiftaiboilerplate.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Authentication Guide

> Unified authentication with Apple, Google, and Email via Supabase

This boilerplate provides a complete authentication solution using Supabase Auth with support for:

* **Apple Sign In** - Native iOS authentication
* **Google Sign In** - OAuth via Google
* **Email/Password** - Traditional email authentication

All authentication methods are unified through Supabase, providing a consistent user experience and automatic session management.

<Note>
  You can skip this guide while exploring the app in mock mode. Complete it when you are ready to connect real Supabase authentication providers.
</Note>

## Features

### Unified Sign In Experience

* **Single Sign In Screen**: All authentication options on one screen
* **Social OAuth**: Apple and Google sign in via Supabase Auth
* **Email Authentication**: Sign up and sign in with email/password
* **Password Reset**: Forgot password flow via email

### Session Persistence

✅ **Users stay logged in for days, not hours** - Production-ready session management that just works

The app implements battle-tested session persistence:

1. **Keychain Storage**: Auth tokens are securely stored in iOS Keychain
2. **Automatic Session Loading**: When the app launches, sessions are restored from Keychain
3. **Expired Session Recovery**: Even if access token has expired, the app automatically refreshes using the refresh token
4. **Proactive Refresh**: Tokens are refreshed 60 seconds before expiry
5. **Retry Logic**: Failed refresh attempts retry up to 3 times with exponential backoff

### Session Behavior

| Scenario                  | Behavior                                       |
| ------------------------- | ---------------------------------------------- |
| App opened after 1 hour   | ✅ Silent refresh, user stays logged in         |
| App opened after 3 days   | ✅ Silent refresh, user stays logged in         |
| App opened after 14+ days | Refresh token expired, user must sign in again |

<Note>
  Refresh token lifetime is configurable in your Supabase dashboard. Default is 7+ days.
</Note>

### Session Lifecycle

```text theme={null}
App Launch
    ↓
Load Session from Keychain
    ↓
Access Token Valid? ──No──→ Attempt Refresh with Refresh Token
    ↓ Yes                         ↓
    ↓                    Refresh Successful? ──No──→ Show Sign In Screen
    ↓                             ↓ Yes
Navigate to Main Screen  ←────────┘
    ↓
Schedule Proactive Refresh (60s before expiry)
    ↓
Silent background refresh
    ↓
Continue using app seamlessly
```

## Setup

### 1. Configure Supabase

#### Enable Social Providers

1. Go to [Supabase Dashboard](https://app.supabase.com)
2. Select your project
3. Navigate to **Authentication** → **Providers**

### Apple Sign In Setup (Step-by-Step)

<Steps>
  <Step title="Step 1: Go to Supabase Authentication">
    In your Supabase dashboard, navigate to **Authentication** settings. This is where you'll enable Apple as a sign-in provider.
  </Step>

  <Step title="Step 2: Enable Apple Sign In Provider">
    Go to **Sign In Providers** and find Apple in the list. Click on it to configure.
  </Step>

  <Step title="Step 2.1: Enable Apple Sign In">
    Toggle **Enable Sign in with Apple** to ON. This activates Apple as an authentication provider in your Supabase project.
  </Step>

  <Step title="Step 3: Configure Apple Developer Identifiers">
    In [Apple Developer Portal](https://developer.apple.com), go to **Certificates, Identifiers & Profiles**:

    1. Select **Identifiers** → Your App ID
    2. Ensure **Sign in with Apple** capability is checked
    3. Click **Save**
  </Step>

  <Step title="Step 4: Complete Apple Developer Portal Setup">
    Complete the Apple Developer Portal configuration:

    * **Services ID**: Create one if needed for web callback
    * **Team ID**: Your Apple Developer Team ID
    * **Key ID**: Generate a new key with Sign in with Apple enabled
    * **Private Key**: Download and save securely

    Add these to your Supabase Apple provider settings.
  </Step>

  <Step title="Step 5: Add Xcode Capability">
    In Xcode, go to your target's **Signing & Capabilities**:

    1. Click **+ Capability**
    2. Search for "Sign in with Apple"
    3. Add it to your target

    This enables the native Apple Sign In button in your app.
  </Step>
</Steps>

<Note>
  Apple requires apps that offer third-party login (like Google) to also support Sign in with Apple. The boilerplate has this pre-built with Supabase integration.
</Note>

### Google Sign In Setup

Google Sign In uses two pieces of configuration:

* A **Web OAuth client** for Supabase, because Supabase owns the OAuth callback URL
* An **iOS OAuth client** for the native Google Sign-In SDK, because the app runs on iPhone and is identified by your Bundle ID

<Warning>
  If Google asks for a Bundle ID, use your app's iOS bundle identifier. The boilerplate derives every target's bundle ID from a single `APP_BUNDLE_ID` in `Config/App.xcconfig` (each target sets `PRODUCT_BUNDLE_IDENTIFIER = $(APP_BUNDLE_ID)`), so set `APP_BUNDLE_ID` there or read the resolved value from Xcode target settings. If Google asks for an App Store ID, you can usually leave it blank while developing locally. Add it later after you create the app record in App Store Connect.
</Warning>

<Steps>
  <Step title="Find your Supabase callback URL">
    In your Supabase project, copy your project reference from the project URL, then build this callback URL:

    ```text theme={null}
    https://YOUR_PROJECT_REF.supabase.co/auth/v1/callback
    ```

    Keep this URL open. You will paste it into Google Cloud in the next step.
  </Step>

  <Step title="Create the Web OAuth client in Google Cloud">
    Open [Google Cloud Console](https://console.cloud.google.com), select or create a project, then go to **APIs & Services** -> **Credentials**.

    1. Click **Create Credentials** -> **OAuth client ID**
    2. Choose **Web application**
    3. Name it something like `SwiftAI Supabase Web`
    4. Add the Supabase callback URL under **Authorized redirect URIs**
    5. Save the client

    Copy the generated **Client ID** and **Client Secret**. These values go into Supabase.
  </Step>

  <Step title="Enable Google in Supabase">
    In Supabase, go to **Authentication** -> **Sign In / Providers** -> **Google**.

    1. Toggle **Enable Sign in with Google** on
    2. Paste the **Web OAuth Client ID** from Google Cloud
    3. Paste the **Web OAuth Client Secret** from Google Cloud
    4. Save the provider
  </Step>

  <Step title="Create the iOS OAuth client in Google Cloud">
    Back in Google Cloud **Credentials**, create a second OAuth client:

    1. Click **Create Credentials** -> **OAuth client ID**
    2. Choose **iOS**
    3. Enter your **Bundle ID** exactly as it resolves from `APP_BUNDLE_ID` in `Config/App.xcconfig`, for example `com.yourcompany.yourapp`
    4. Leave **App Store ID** blank for local development if you do not have an App Store Connect app yet
    5. Save the client

    Copy the generated **iOS Client ID**. This value goes into your app's `Config/Secrets.xcconfig` as `GOOGLE_CLIENT_ID`.
  </Step>
</Steps>

### 2. Configure iOS App

**Update Secrets Configuration:**

```bash theme={null}
# Config/Secrets.xcconfig
SUPABASE_URL = https://YOUR_PROJECT_REF.supabase.co
SUPABASE_ANON_KEY = YOUR_PUBLIC_ANON_KEY

# For Google Sign In (optional)
GOOGLE_CLIENT_ID = YOUR_IOS_CLIENT_ID
```

<Warning>
  After editing `Config/Secrets.xcconfig`, run `bash scripts/update-config.sh` and rebuild the app. The generated Swift configuration is not updated automatically.
</Warning>

**Add Google Sign In SDK (Optional):**

Google Sign In is optional. `GoogleSignInCoordinator` is guarded by `#if canImport(GoogleSignIn)`, so the app builds and runs without the SDK — Google sign-in is simply unavailable until you add it.

To enable it, add the GoogleSignIn-iOS package to the Xcode project via **File → Add Package Dependencies**:

```text theme={null}
https://github.com/google/GoogleSignIn-iOS
```

Then add the `GoogleSignIn` product to the app target.

### Google Credential Reference

| Field                 | Where to Get It                                                     | Where to Put It                                                    |
| --------------------- | ------------------------------------------------------------------- | ------------------------------------------------------------------ |
| Supabase callback URL | Supabase project ref                                                | Google Cloud Web OAuth client -> Authorized redirect URIs          |
| Web Client ID         | Google Cloud Web OAuth client                                       | Supabase Google provider                                           |
| Web Client Secret     | Google Cloud Web OAuth client                                       | Supabase Google provider                                           |
| iOS Client ID         | Google Cloud iOS OAuth client                                       | `Config/Secrets.xcconfig` as `GOOGLE_CLIENT_ID`                    |
| Bundle ID             | `APP_BUNDLE_ID` in `Config/App.xcconfig` (or Xcode target settings) | Google Cloud iOS OAuth client                                      |
| App Store ID          | App Store Connect app record                                        | Optional for local development; add before production if available |

### 3. Setup CompositionRoot

The boilerplate comes with auth pre-configured, but here's how it works:

```swift theme={null}
// CompositionRoot.swift

// Create auth client with social providers
let authClient = SessionManager(
    httpClient: httpClient,
    keychain: keychain,
    apple: AppleSignInCoordinator(),
    google: GoogleSignInCoordinator(clientID: googleClientID), // Optional
    config: authConfig
)
```

## Usage

### Sign In Flow

The `SignInView` provides a premium, minimal authentication experience:

```swift theme={null}
SignInView(authClient: authClient)
```

**Design Philosophy:**

* Clean, Apple-like aesthetic with generous whitespace
* Native Apple button (HIG compliant) as primary CTA
* Visual hierarchy: Apple → Google → "Use email instead" link
* Premium feel with continuous corner radii and subtle borders
* Adapts to light/dark mode automatically

**Features:**

* **Apple Sign In**: Native button (HIG compliant) - primary option
* **Google Sign In**: Secondary button with subtle border
* **Email Sign In**: Shown via "Use email instead" link (opens sheet)
* **Create Account**: Accessible from email login sheet
* **Forgot Password**: Accessible from email login sheet

**Theme Integration:**
All auth screens (Sign In, Sign Up, Forgot Password) adapt to the active theme:

* Links and accents use `DSColors.accentPrimary` (warm pink in Aurora, neutral in others)
* Text colors use `DSColors.textPrimary` and `DSColors.textSecondary`
* Backgrounds and surfaces adapt to theme palette
* Icons inherit theme accent color
* No hardcoded system blue - everything theme-aware

**Customization:**
To adjust the visual design, edit `SwiftAIBoilerplatePro/AppShell/SignInView.swift`:

**Spacing & Rhythm:**

* Top space: `Spacer().frame(height: 100)` - Adjust for vertical positioning
* Between hero and buttons: `64pt` - Control breathing room
* Bottom space: `80pt` then `48pt` - Legal section padding

**Typography:**

* App title: `28pt semibold rounded` - Adjust size/weight
* Tagline: `15pt regular, 0.3 tracking, 70% opacity` - Refine elegance
* Legal text: `12pt, 55% opacity` - Keep whisper-quiet

**Colors & Effects:**

* Avatar blur: `blur(radius: 20)` on gradient circle
* Background: Subtle top-to-bottom gradient
* Google button: `surface.opacity(0.6)` with `0.5pt border`
* Link colors: Uses `accentPrimary.opacity(0.7)` for cohesion

**Animations:**

* Hero fade-in: `.easeOut(duration: 0.6)` on appear
* Buttons slide: `.delay(0.15)` for staggered entrance
* Error message: `.spring(response: 0.35)` for smooth entry

**Container Width:**

* Max width: `380pt` for button container (narrower = more refined)

### Check Authentication State

```swift theme={null}
// Check current user
let user = await authClient.currentUser()

if let user = user {
    print("Signed in as: \(user.email ?? "unknown")")
} else {
    print("Not signed in")
}

// Observe auth state changes
for await state in authClient.authStates() {
    switch state {
    case .authenticated(let user):
        print("User signed in: \(user.id)")
    case .unauthenticated:
        print("User signed out")
    case .refreshing:
        // Show loading UI while recovering expired session
        print("Refreshing session...")
    }
}
```

### Sign Out

```swift theme={null}
try await authClient.signOut()
```

This will:

1. Revoke the access token on the server
2. Clear tokens from Keychain
3. Clear in-memory session
4. Navigate to sign-in screen

## How It Works

### Apple Sign In Flow

1. User taps "Continue with Apple"
2. System shows Apple Sign In sheet
3. User authenticates with Face ID/Touch ID
4. App receives Apple ID token + nonce
5. Token is exchanged with Supabase for session
6. Session stored in Keychain
7. User navigated to main screen

```swift theme={null}
// Internal flow
let (idToken, nonce) = try await apple.requestIDToken(...)
let session = try await api.exchangeAppleIDToken(idToken: idToken, nonce: nonce)
try await persistSession(session)
```

### Google Sign In Flow

1. User taps "Continue with Google"
2. Google Sign In SDK presents OAuth flow
3. User selects Google account
4. App receives Google ID token
5. Token is exchanged with Supabase for session
6. Session stored in Keychain
7. User navigated to main screen

```swift theme={null}
// Internal flow
let idToken = try await google.requestIDToken()
let session = try await api.exchangeGoogleIDToken(idToken: idToken)
try await persistSession(session)
```

### Email Sign In Flow

1. User enters email and password
2. Credentials sent to Supabase
3. Supabase validates and returns session
4. Session stored in Keychain
5. User navigated to main screen

```swift theme={null}
// Internal flow
let session = try await api.signInWithEmail(email: email, password: password)
try await persistSession(session)
```

## Session Persistence Details

### How Sessions Are Stored

Sessions are stored in iOS Keychain with the following keys:

```text theme={null}
auth_access_token   - JWT access token
auth_refresh_token  - Long-lived refresh token
auth_expires_at     - Token expiration timestamp
auth_user_json      - User profile data
```

Security:

* Uses `kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly`
* Data is encrypted by iOS
* Survives app updates
* Deleted on app uninstall

### Automatic Session Loading

On app launch (`loadInitialSession()`):

1. **Load from Keychain**: Attempts to load stored session
2. **Validate Expiry**: Checks if access token is expired
3. **Attempt Refresh**: If expired, tries to refresh using the refresh token (up to 3 attempts)
4. **Recover or Clear**: Restores session if refresh succeeds, clears if refresh token is also expired
5. **Emit State**: Notifies observers of auth state (`.refreshing` during recovery)

```swift theme={null}
// SessionManager+Persistence.swift
func loadInitialSession() async {
    do {
        if let session = try loadSession() {
            if Date.now > session.expiresAt {
                // Access token expired - try the refresh token before
                // signing the user out (it usually outlives the access token)
                await attemptSessionRefresh(with: session)
                return
            }

            // Session valid, restore it
            currentSession = session
            emit(.authenticated(session.user))

            // Schedule proactive refresh (or refresh now if due soon)
            if session.needsRefresh {
                Task { try? await refreshIfNeeded() }
            } else {
                scheduleRefresh(for: session)
            }
        }
    } catch {
        try? clearSession()
    }
}
```

<Tip>
  The `.refreshing` state allows you to show a loading indicator while the session is being recovered, providing a smooth user experience.
</Tip>

### Token Refresh Strategy

The app uses **proactive refresh** and **reactive recovery** to ensure users stay logged in:

**Proactive Refresh (Before Expiry):**

1. **Schedule Refresh**: 60 seconds before token expiry
2. **Silent Background Execution**: User never sees a loading state
3. **Token Rotation**: Handles Supabase refresh token rotation

**Reactive Recovery (After Expiry):**

1. **Detect Expired Session**: On app launch, check if access token expired
2. **Attempt Refresh**: Use refresh token to get new access token
3. **Retry Logic**: Up to 3 attempts with exponential backoff
4. **Emit `.refreshing` State**: Show loading UI if needed

```swift theme={null}
// Proactive: Scheduled 60 seconds before expiry
let refreshTime = session.expiresAt.addingTimeInterval(-60)
let delay = max(0, refreshTime.timeIntervalSinceNow)
refreshTask = Task {
    try await sleeper.sleep(for: delay)
    if !Task.isCancelled {
        try await refreshIfNeeded()
    }
}

// Reactive: Recover an expired session on app launch
func attemptSessionRefresh(with expiredSession: AuthSession) async {
    currentSession = expiredSession
    emit(.refreshing)

    for attempt in 1...3 {
        do {
            let newSession = try await api.refresh(refreshToken: expiredSession.refreshToken)
            try await persistSession(newSession)
            currentSession = newSession
            emit(.authenticated(newSession.user))
            scheduleRefresh(for: newSession)
            return
        } catch {
            if attempt < 3 {
                try? await sleepBeforeRetry(attempt: attempt)
            }
        }
    }

    // Refresh token also invalid - clear and require sign-in
    try? clearSession()
    currentSession = nil
    emit(.unauthenticated)
}
```

<Info>
  **Result**: Users stay logged in for days (until refresh token expires), not just 1 hour (access token lifetime).
</Info>

## Navigation Based on Auth State

The `LaunchRouter` automatically handles navigation:

```swift theme={null}
// LaunchRouter observes auth state
for await state in authClient.authStates() {
    switch state {
    case .authenticated:
        destination = .main  // Show main app
    case .unauthenticated:
        destination = .signIn  // Show sign in
    case .refreshing:
        // Keep current screen, just refreshing token
    }
}
```

This means:

* ✅ Users automatically navigate to main screen when signed in
* ✅ Users automatically navigate to sign in when signed out
* ✅ No manual navigation needed
* ✅ Works for all auth methods (Apple, Google, Email)

## Best Practices

### Always Check Auth State

```swift theme={null}
// Don't store user state yourself
// ❌ Bad
@State private var isSignedIn = false

// ✅ Good - observe auth state
for await state in authClient.authStates() {
    // React to state changes
}
```

### Handle Sign Out Gracefully

```swift theme={null}
func signOut() {
    Task {
        do {
            try await authClient.signOut()
            // Navigation handled automatically by LaunchRouter
        } catch {
            // Show error to user
            errorMessage = "Failed to sign out: \(error.localizedDescription)"
        }
    }
}
```

### Use Session Tokens for API Calls

When the `HTTPClient` is built with an `AuthInterceptor`, the current access token is attached to every request automatically — call sites never touch the token:

```swift theme={null}
// AuthInterceptor adds `Authorization: Bearer <access_token>` if the
// header isn't already set, sourcing the token from a TokenProvider.
let request = HTTPRequest(path: "/rest/v1/profiles", method: .get)
let response = try await httpClient.send(request)
let profile = try JSONDecoder().decode(UserProfile.self, from: response.data)
```

## Troubleshooting

### Users Have to Sign In Every Time

This should be rare with the new session recovery. Check:

1. Keychain is accessible (not disabled in Xcode capabilities)
2. `loadInitialSession()` is being called
3. No errors in `SessionManager` initialization
4. Check logs for refresh attempts and failures
5. Verify refresh token hasn't expired (7+ days of inactivity)

<Note>
  With proper session recovery, users only need to sign in again if their refresh token expires (typically 7+ days of not opening the app).
</Note>

### Google Sign In Not Working

1. Verify `GOOGLE_CLIENT_ID` is set to the **iOS OAuth Client ID**, not the Web Client ID.
2. Verify the Supabase Google provider uses the **Web OAuth Client ID** and **Web OAuth Client Secret**.
3. Ensure the Google Cloud Web OAuth client has this exact authorized redirect URI: `https://YOUR_PROJECT_REF.supabase.co/auth/v1/callback`.
4. Ensure the Google Cloud iOS OAuth client Bundle ID exactly matches the bundle ID resolved from `APP_BUNDLE_ID` in `Config/App.xcconfig`.
5. If you changed `Secrets.xcconfig`, run `bash scripts/update-config.sh`, clean, and rebuild.
6. Verify GoogleSignIn SDK is installed.
7. For local development, do not block on App Store ID. Add it later after creating the app in App Store Connect.

### Apple Sign In Fails

1. **Supabase Configuration**: Verify Apple provider is enabled in Supabase Authentication → Providers
2. **Apple Developer Portal**: Check that Sign in with Apple capability is enabled on your App ID
3. **Xcode Capability**: Ensure "Sign in with Apple" capability is added in Signing & Capabilities
4. **Credentials**: Verify Team ID, Key ID, and Private Key are correct in Supabase
5. **Bundle ID**: Check bundle ID matches between Xcode, App Store Connect, and Supabase

### Token Refresh Fails

Check logs for:

* Network connectivity issues
* Supabase service status
* Invalid refresh token (forces re-authentication)
* Too many concurrent refresh attempts

## Security Considerations

1. **Tokens in Keychain**: Never store tokens in UserDefaults or files
2. **HTTPS Only**: All auth requests use HTTPS
3. **Token Expiry**: Access tokens expire (default: 1 hour), but sessions persist via refresh tokens
4. **Refresh Token Lifetime**: Configurable in Supabase dashboard (default: 7+ days)
5. **Refresh Rotation**: Supabase rotates refresh tokens for security
6. **Server-Side Validation**: Always validate tokens server-side
7. **Detailed Logging**: Auth events are logged for debugging (tokens are never logged)

## Next Steps

<CardGroup cols={2}>
  <Card title="Supabase Auth Docs" icon="database" href="https://supabase.com/docs/guides/auth">
    Official Supabase authentication documentation
  </Card>

  <Card title="Apple Sign In Guide" icon="apple" href="https://developer.apple.com/sign-in-with-apple/">
    Apple's official Sign in with Apple documentation
  </Card>

  <Card title="Google Sign In Setup" icon="google" href="https://developers.google.com/identity">
    Google Identity Platform documentation
  </Card>

  <Card title="Auth Module" icon="lock" href="/pages/modules/auth">
    Technical documentation for the Auth package
  </Card>
</CardGroup>
