What Is AccountService and How Does It Manage User Authentication in Palmier Pro
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, 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
-
App Launch:
AccountService.shared.configure()initializes the client from the app bootstrap (usually from@mainorAppDelegate). -
Observation Begins:
startAuthObservation()waits for Clerk to load, then monitors theconvex.authStatestream. -
Authentication Detected: When Convex reports
.authenticated,authStateupdates andisSignedInreflects the change for UI components. -
User Provisioning:
provisionAndSubscribe()calls theusers:upsertFromAuthmutation to create or update the user record with Clerk profile data. -
Data Streaming:
startAccountSubscription()begins streaming theaccount:getquery, whileavailablePlanspopulates subscription options. -
OAuth Completion:
signInWithGoogle()triggers Clerk's OAuth flow; upon success, Clerk sets a session that Convex recognizes, closing the authentication loop. -
Session Termination:
signOut()clears the Convex subscription, resetsauthStateto unauthenticated, and removes local data viaclearAccount().
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:
import PalmierPro
@main
struct PalmierProApp: App {
init() {
AccountService.shared.configure()
}
// …
}
Triggering Google Sign-In
From a SwiftUI button:
Button("Sign in with Google") {
Task {
await AccountService.shared.signInWithGoogle()
}
}
.disabled(AccountService.shared.isLoading)
Reacting to Authentication State
Binding to observable properties:
@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
Button("Sign Out") {
Task {
await AccountService.shared.signOut()
}
}
.disabled(!account.isSignedIn)
Accessing Account Tiers and Credits
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 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 theusers:upsertFromAuthmutation with retry logic (lines 105-130)- Real-time account data flows through the
account:getsubscription to the observableaccountproperty - Google OAuth is handled via
signInWithGoogle()(lines 99-114), withsignOut()(lines 115-129) clearing both Clerk and local state - UI helpers like
displayPrimaryTextprovide 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 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.
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 →