Skip to content
Documentation GitHub
Architecture

Identity vs Auth vs Analytics: Three-Concern Separation

Identity vs Auth vs Analytics: Three-Concern Separation

Problem

The AuthRepository trait conflated three distinct concerns:

  1. User identity — “who is this person?”
  2. Remote service auth — “can we make authenticated API calls?”
  3. 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:

  1. Authenticate via remote auth provider
  2. Fetch user profile → IdentityStore::store_identity()
  3. Store tokens in keychain → AuthRepository::store_session()

On subsequent app launches:

  • IdentityStore::get_identity() → local, instant, offline
  • AuthRepository::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

Was this page helpful?