# How to Manage Users in Palmier Pro: Authentication, Subscriptions, and Credits

> Learn to manage users in Palmier Pro using AccountService for authentication, subscriptions, and credits. Centralize control with Clerk, Convex, and Stripe integration.

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

---

**Palmier Pro centralizes user management through the `AccountService` singleton, which orchestrates Clerk authentication, Convex backend synchronization, and Stripe-powered subscription and credit purchases.**

The **palmier-io/palmier-pro** repository implements a comprehensive user management system that handles everything from OAuth sign-ins to in-app credit top-ups. The architecture relies on a single shared service instance that mediates between the Clerk identity platform, Convex database, and the SwiftUI presentation layer.

## Understanding the AccountService Architecture

The `AccountService` class acts as the single source of truth for user identity, subscription status, and credit balances across the application.

### Singleton Pattern and Configuration

The service exposes a shared singleton at `AccountService.shared` that you configure immediately on app launch. In [`Sources/PalmierPro/App/main.swift`](https://github.com/palmier-io/palmier-pro/blob/main/Sources/PalmierPro/App/main.swift), the entry point calls `configure()` to initialize the Clerk SDK with your publishable key and establish the Convex client connection:

```swift
import SwiftUI

@main
struct PalmierProApp: App {
    init() {
        AccountService.shared.configure()
    }
    
    var body: some Scene {
        WindowGroup {
            ContentView()
        }
    }
}

```

The `configure()` method sets up Clerk using `BackendConfig.clerkPublishableKey` and prepares the authentication observation pipeline.

### Authentication State Management

After configuration, the service launches `startAuthObservation()` as an asynchronous task to monitor `authState` changes. This coroutine tracks whether the user is signed in, misconfigured, or authenticated, updating the `@Published` properties `isSignedIn` and `isMisconfigured` that the UI observes.

## Implementing Authentication Flows

All authentication operations flow through the `AccountService`, abstracting the Clerk implementation details from your views.

### Sign In with Google

To present a sign-in button, invoke `signInWithGoogle()` from your SwiftUI view. The implementation in [`AccountPane.swift`](https://github.com/palmier-io/palmier-pro/blob/main/AccountPane.swift) wraps this in a button action:

```swift
Button("Sign in with Google") {
    Task { await AccountService.shared.signInWithGoogle() }
}
.buttonStyle(.capsule(.secondary, size: .regular))

```

Upon successful OAuth completion, the service automatically calls `provisionAndSubscribe()`, which upserts the user into the Convex backend via the `users:upsertFromAuth` action and subscribes to the `"account:get"` query for real-time profile updates.

### Sign Out

The sign-out process clears the Clerk session and resets the local authentication state. Implement the button in your settings UI:

```swift
Button("Sign out") {
    Task { await AccountService.shared.signOut() }
}
.buttonStyle(.capsule(.secondary, size: .regular))

```

## Managing Subscriptions and User Tiers

Palmier Pro supports tiered subscriptions (typically including `pro` and `max` levels) managed through Stripe checkout sessions.

### Upgrading to Paid Tiers

To initiate an upgrade, call `subscribe(tier:)` with the desired tier enum value. This method validates the service configuration, then invokes the Convex action `"billing:createCheckoutSession"` to generate a Stripe checkout URL:

```swift
Task {
    await AccountService.shared.subscribe(tier: .pro)
    // or .max for higher tier
}

```

The `openInBrowser(_:)` method then presents the URL, whitelisting only Stripe domains for security. For existing subscribers, `manageSubscription()` similarly opens the Stripe customer portal via `"billing:createPortalSession"`.

### Accessing User Plan Information

After provisioning, the service exposes convenience properties derived from the `AccountResponse` object:

```swift
let isPaid = AccountService.shared.isPaid
let currentTier = AccountService.shared.tier
let displayName = AccountService.shared.displayPrimaryText

```

These properties update automatically as the Convex subscription pushes changes to `"account:get"`.

## Handling In-App Credit Purchases

The credit system allows users to purchase additional processing credits outside their subscription allocation. The `buyCredits(dollars:)` method validates the amount against `TopOffLimits` (ranging from **$5 to $1000**) before invoking `"billing:createTopOffCheckoutSession"`:

```swift
let purchaseAmount = 50
AccountService.shared.buyCredits(dollars: purchaseAmount)

```

The UI implementation in [`TopOffField.swift`](https://github.com/palmier-io/palmier-pro/blob/main/TopOffField.swift) captures the dollar amount, and [`AccountPane.swift`](https://github.com/palmier-io/palmier-pro/blob/main/AccountPane.swift) displays the resulting `remainingCredits` alongside the credit purchase interface.

## Integrating User Management into the App Lifecycle

Robust user management requires checking state throughout the application. Query the service from any view to conditionally render content:

```swift
if AccountService.shared.isSignedIn {
    // Show premium features
    let credits = AccountService.shared.remainingCredits
    Text("Remaining credits: \(credits)")
} else {
    // Show sign-in prompt
}

```

Error handling follows a reactive pattern where all public methods capture failures into the `lastError` property. The `AccountPane` monitors this property and displays feedback below the main content, ensuring users see immediate feedback when Stripe checkout generation or Convex queries fail.

## Summary

- **Centralized Service**: All user operations flow through `AccountService.shared` defined in [`Sources/PalmierPro/Account/AccountService.swift`](https://github.com/palmier-io/palmier-pro/blob/main/Sources/PalmierPro/Account/AccountService.swift).
- **Clerk Integration**: OAuth authentication via `signInWithGoogle()` and session management through Clerk's SDK.
- **Convex Backend**: Real-time user profiles and subscription status synchronized via `"account:get"` and `"users:upsertFromAuth"`.
- **Stripe Workflows**: Subscription upgrades and credit purchases use `subscribe(tier:)` and `buyCredits(dollars:)` with checkout session generation.
- **SwiftUI Binding**: The service publishes `isSignedIn`, `tier`, and `remainingCredits` for reactive UI updates in [`AccountPane.swift`](https://github.com/palmier-io/palmier-pro/blob/main/AccountPane.swift).

## Frequently Asked Questions

### How does Palmier Pro authenticate users?

Palmier Pro uses **Clerk** as its identity platform. The `AccountService` configures Clerk during app initialization using a publishable key from `BackendConfig`, then observes authentication state changes through a `ConvexClientWithAuth` wrapper. Users sign in via OAuth (Google) through Clerk's native SDK integration.

### Where is user data stored in Palmier Pro?

User profiles, subscription tiers, and credit balances persist in **Convex**. When a user signs in, `provisionAndSubscribe()` upserts their record via the `users:upsertFromAuth` action and establishes a real-time subscription to `"account:get"` that pushes updates to the client automatically.

### How do I check if a user has available credits?

Query `AccountService.shared.remainingCredits` to get the current integer balance. The service updates this property whenever the Convex backend broadcasts changes, allowing you to disable premium features or show warnings when `hasCredits` returns false.

### What happens when a user upgrades their subscription?

Calling `subscribe(tier:)` generates a Stripe checkout session via the Convex action `"billing:createCheckoutSession"`. After successful payment, Stripe webhooks update the user's tier in Convex, which streams back to the app through the existing `"account:get"` subscription, updating `AccountService.shared.tier` and `isPaid` properties immediately.