Identity vs Auth vs Analytics: Three-Concern Separation
Identity vs Auth vs Analytics: Three-Concern Separation
Problem
The AuthRepository trait conflated three distinct concerns:
- User identity — “who is this person?”
- Remote service auth — “can we make authenticated API calls?”
- Usage analytics — “how are they using the product?”
This caused get_current_user() to require an HTTP call to /auth/v1/user, meaning the app couldn’t even identify the
user when offline. Token expiry was treated as “not authenticated” rather than “can’t reach the server.”
Root Cause
Desktop apps are not web browsers. A web app can redirect to login when a session expires, but a desktop app has persistent local storage and must work without connectivity. The original design treated auth tokens as the sole proof of identity, with no local persistence of user profile data.
Solution
Three separate abstractions
1. IdentityStore (application layer) — offline-permanent
trait IdentityStore: Send + Sync { fn get_identity() -> Option<UserIdentity>; // Always works offline fn store_identity(identity: UserIdentity); // Persists locally fn clear_identity(); // On sign-out}- Stored in settings/SQLite (not keychain — not a secret)
- Captured on first sign-in, persists indefinitely
- Powers local personalization + analytics attribution
2. AuthRepository (application layer) — connectivity-dependent
trait AuthRepository: Send + Sync { fn ensure_valid_token() -> AuthResult<String>; // For remote calls only fn refresh_session() -> AuthResult<String>; fn sign_out() -> AuthResult<()>; // ... token management}- Tokens stored in OS keychain (secrets)
- Only needed at the network boundary (sync, cloud API calls)
- Failure = “can’t sync right now”, NOT “user is unknown”
3. Analytics (offline-first queue)
- Pseudonymous:
user_id(UUID) only, no PII - Queue events locally (SQLite table, like
sync_queue) - Flush when connected; flush on sign-out (session end)
- On by default, opt-out available
- Deletion requests anonymize (break user link), preserve aggregate data
Flow
On first sign-in:
- Authenticate via remote auth provider
- Fetch user profile →
IdentityStore::store_identity() - Store tokens in keychain →
AuthRepository::store_session()
On subsequent app launches:
IdentityStore::get_identity()→ local, instant, offlineAuthRepository::ensure_valid_token()→ only when making remote calls
On sign-out:
- Flush analytics queue
- Clear tokens (keychain)
- Clear local identity
Prevention
Best Practices
- Supabase tokens should only matter at the network boundary
- Local SQLite doesn’t need a JWT to read/write
- Never gate local operations on token validity
- “Logged in” = identity exists locally; “Can sync” = valid token exists
Warning Signs
- Any code path that calls
ensure_valid_token()for a local operation get_current_user()making network requests- User seeing “not authenticated” when they’re simply offline
References
- Linear: INK-154 (IdentityStore split), INK-155 (analytics foundation)
- Linear project: Cloud Infrastructure & DevOps Review
Identifier Entity-Type Dispatch: Caller-Provided vs Embedded Next
Infrastructure Naming Discipline Across Architecture Layers
Was this page helpful?
Thanks for your feedback!