How to Manage Users in Palmier Pro: Authentication, Subscriptions, and Credits
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, the entry point calls configure() to initialize the Clerk SDK with your publishable key and establish the Convex client connection:
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 wraps this in a button action:
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:
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:
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:
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":
let purchaseAmount = 50
AccountService.shared.buyCredits(dollars: purchaseAmount)
The UI implementation in TopOffField.swift captures the dollar amount, and 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:
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.shareddefined inSources/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:)andbuyCredits(dollars:)with checkout session generation. - SwiftUI Binding: The service publishes
isSignedIn,tier, andremainingCreditsfor reactive UI updates inAccountPane.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.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →