# What Is AccountService and How Does It Manage User Authentication in Palmier Pro

> Discover AccountService in Palmier Pro, a Swift singleton managing the full user authentication lifecycle, integrating Clerk and Convex for OAuth and real-time data sync.

- Repository: [Palmier/palmier-pro](https://github.com/palmier-io/palmier-pro)
- Tags: internals
- Published: 2026-06-23

---

**AccountService is a Swift singleton that integrates the Clerk identity provider with the Convex backend to manage the complete authentication lifecycle, from OAuth sign-in flows to real-time account data synchronization.**

The `AccountService` class in the palmier-pro repository serves as the central authentication hub for the Palmier Pro macOS application. Located in [`Sources/PalmierPro/Account/AccountService.swift`](https://github.com/palmier-io/palmier-pro/blob/main/Sources/PalmierPro/Account/AccountService.swift), this singleton coordinates between Clerk's OAuth flows and Convex's reactive backend to maintain user sessions, provision user records, and stream real-time account data to the UI.

## Core Architecture and Responsibilities

### Service Configuration and Initialization

The `configure()` method (lines 36-76) initializes the authentication stack by loading the Clerk publishable key and Convex deployment URL from `BackendConfig`. It instantiates `ConvexClientWithAuth`, configures the Clerk SDK via `Clerk.configure(...)`, and spawns the subscription pipelines by calling `startPlansSubscription()` and `startAuthObservation()`.

### Authentication State Management

AccountService maintains the current `AuthState<String>` from Convex through a private `authState` variable and exposes an `@Observable` `isSignedIn` computed property (lines 101-115). This allows SwiftUI views to reactively update when authentication status changes between loading, authenticated, and unauthenticated states.

### Clerk-to-Convex Observation Pipeline

The `startAuthObservation()` method (lines 78-102) bridges the two identity systems. It spawns a `Task` that first waits for Clerk to finish loading any persisted session, then iterates over `convex.authState.values`. When Convex reports authentication changes, the method updates `self.authState` and dispatches to the appropriate handler for loading, authenticated, or unauthenticated states.

### User Provisioning and Record Synchronization

When authentication becomes `.authenticated`, `provisionAndSubscribe()` (lines 105-130) executes the `users:upsertFromAuth` Convex mutation. This method constructs a payload from `Clerk.shared.user` containing email, name, and profile image, then creates or updates the user document in Convex with up to three retry attempts before giving up.

### Real-Time Account and Plans Data

After successful provisioning, `startAccountSubscription()` establishes a reactive subscription to the `account:get` Convex query. This keeps the `account` property synchronized with the backend, while `availablePlans` and other observable properties automatically update the UI when subscription tier, credits, or billing status changes.

### Authentication Flows

**Sign-in** is handled by `signInWithGoogle()` (lines 99-114), which delegates OAuth to Clerk via `Clerk.shared.auth.signInWithOAuth(provider: .google)`. Errors are captured in `lastError` for UI feedback.

**Sign-out** is managed by `signOut()` (lines 115-129), which invokes `Clerk.shared.auth.signOut()` followed by `clearAccount()` to terminate the session and remove local data.

### UI Helper Properties

The service exposes user-friendly display strings through computed properties (lines 120-148) including `displayPrimaryText`, `displaySecondaryText`, and `displayInitial`, formatting Clerk user data for SwiftUI consumption.

## Step-by-Step Authentication Flow

1. **App Launch**: `AccountService.shared.configure()` initializes the client from the app bootstrap (usually from `@main` or `AppDelegate`).

2. **Observation Begins**: `startAuthObservation()` waits for Clerk to load, then monitors the `convex.authState` stream.

3. **Authentication Detected**: When Convex reports `.authenticated`, `authState` updates and `isSignedIn` reflects the change for UI components.

4. **User Provisioning**: `provisionAndSubscribe()` calls the `users:upsertFromAuth` mutation to create or update the user record with Clerk profile data.

5. **Data Streaming**: `startAccountSubscription()` begins streaming the `account:get` query, while `availablePlans` populates subscription options.

6. **OAuth Completion**: `signInWithGoogle()` triggers Clerk's OAuth flow; upon success, Clerk sets a session that Convex recognizes, closing the authentication loop.

7. **Session Termination**: `signOut()` clears the Convex subscription, resets `authState` to unauthenticated, and removes local data via `clearAccount()`.

## Security and URL Validation

AccountService implements strict URL validation through `allowedBillingHosts`, whitelisting only `checkout.stripe.com` and `billing.stripe.com` for billing operations. The `openInBrowser(_:)` method validates HTTPS schemes and host domains before calling `NSWorkspace.shared.open`, preventing malicious URL redirection. All authentication errors are captured in `lastError` to provide safe user feedback without exposing sensitive system details.

## Code Implementation Examples

### Initializing the Service

Typically called from the app's entry point:

```swift
import PalmierPro

@main
struct PalmierProApp: App {
    init() {
        AccountService.shared.configure()
    }
    // …
}

```

### Triggering Google Sign-In

From a SwiftUI button:

```swift
Button("Sign in with Google") {
    Task {
        await AccountService.shared.signInWithGoogle()
    }
}
.disabled(AccountService.shared.isLoading)

```

### Reacting to Authentication State

Binding to observable properties:

```swift
@ObservableObject var account = AccountService.shared

var body: some View {
    if account.isSignedIn {
        Text("Welcome, \(account.displayPrimaryText)")
    } else {
        Text("You are not signed in")
    }
}

```

### Signing Out

```swift
Button("Sign Out") {
    Task {
        await AccountService.shared.signOut()
    }
}
.disabled(!account.isSignedIn)

```

### Accessing Account Tiers and Credits

```swift
let tier = AccountService.shared.tier          // .none, .pro, .max
let hasCredits = AccountService.shared.hasCredits
let remaining = AccountService.shared.remainingCredits

```

## Summary

- **AccountService** is a singleton that bridges Clerk OAuth with Convex backend in [`Sources/PalmierPro/Account/AccountService.swift`](https://github.com/palmier-io/palmier-pro/blob/main/Sources/PalmierPro/Account/AccountService.swift)
- **`configure()`** initializes the authentication stack and starts observation pipelines (lines 36-76)
- **`startAuthObservation()`** synchronizes Clerk sessions with Convex authentication state (lines 78-102)
- **`provisionAndSubscribe()`** creates user records via the `users:upsertFromAuth` mutation with retry logic (lines 105-130)
- Real-time account data flows through the `account:get` subscription to the observable `account` property
- **Google OAuth** is handled via `signInWithGoogle()` (lines 99-114), with `signOut()` (lines 115-129) clearing both Clerk and local state
- **UI helpers** like `displayPrimaryText` provide formatted user data for SwiftUI views (lines 120-148)

## Frequently Asked Questions

### What is AccountService in Palmier Pro?

AccountService is a Swift singleton class located in [`Sources/PalmierPro/Account/AccountService.swift`](https://github.com/palmier-io/palmier-pro/blob/main/Sources/PalmierPro/Account/AccountService.swift) that serves as the central authentication coordinator. It manages the entire user lifecycle by integrating Clerk for OAuth identity and Convex for backend data synchronization, exposing observable state for SwiftUI consumption.

### How does AccountService integrate Clerk with Convex?

The service uses `startAuthObservation()` to monitor Clerk's session state and pipe it to Convex's `ConvexClientWithAuth`. When Clerk authenticates a user, the service detects the state change in Convex's `authState` stream, triggers user provisioning via the `users:upsertFromAuth` mutation, and then subscribes to real-time account data through the `account:get` query.

### What happens when a user signs in with Google?

The `signInWithGoogle()` method delegates to `Clerk.shared.auth.signInWithOAuth(provider: .google)`. Upon successful OAuth completion, Clerk establishes a session that the `convex.authState` observer detects as `.authenticated`. This triggers `provisionAndSubscribe()` to create the user record and begins streaming account data to update `isSignedIn` and `account` properties.

### How does AccountService handle authentication errors?

Errors from Clerk operations and Convex mutations are captured in the `lastError` property, which UI components can observe to display feedback. The `provisionAndSubscribe()` method implements retry logic with up to three attempts before surfacing failures, ensuring transient network issues don't immediately break the user experience.