Authentication
Better Auth owns identity, sessions, OAuth, passkeys, and service JWTs. Boja owns everything product: servers, memberships, invitations, roles, and entitlements. Never wrap the app in a global sessionPending spinner.
Startup Sequence
The startup sequence is engineered around a single constraint: the user must see a rendered UI immediately. Auth revalidation and sync catch-up happen concurrently in the background — never on the critical path to first paint.
Better Auth Owns
Better Auth is the identity provider and session manager. Nothing outside this list is Better Auth's concern — all product data is Boja's responsibility.
- User identity — canonical user record, email, display name in the auth context
- Linked social / provider accounts — OAuth provider rows and linked account metadata
- Credential verification — password hashing, passkey challenge/response, TOTP
- Sessions and revocation — session tokens, expiry, forced-logout across devices
- OAuth state / PKCE — state parameter generation, validation, and PKCE code verifier storage
- Native SecureStore session cache — encrypted session token persistence in device Keychain/SecureStore
- Optional passkeys, 2FA, SSO, SCIM — plug-in surface; not in v1 but the plugin interface owns it when enabled
- Short-lived JWT / JWKS — for native sync authentication and service-to-service calls; rotated via JWKS endpoint
Boja Owns
Everything that has product meaning lives in Boja's own tables. Better Auth knows nothing about these entities — there is no mirroring or synchronization.
- Servers and memberships — the canonical server registry and
server_membershipsjoin table - Invitations — invite tokens, invite limits, acceptance flow, expiry
- Owners — server ownership records; tracked separately from roles
- Membership lifecycle — join, leave, ban, kicked states
- Product roles —
rolestable with rank,membership_rolesassignments - Channel permissions —
channel_role_overridesandchannel_member_overrides - Entitlements — effective capability grants derived from billing subscriptions
- Course enrollment —
course_enrollments,lesson_progress, completion rewards - Stream access — room tokens, participant records, broadcast permissions
- Moderation and audit events — timeout, ban, tombstone, and audit log rows
- Sync visibility — Zero publication definitions that determine what each actor receives
Critical Boundary
Better Auth's Organization plugin provides rows for organizations, memberships, roles, and invitations. These rows are NOT canonical for Boja's product servers, memberships, invitations, owners, or roles. App tables own those records. Duplicating them into auth-owned tables creates authorization drift: two sources of truth that diverge under partial failures, migrations, and role changes. The Organization plugin is not used for any product authorization decision.
Security Posture
| Surface | Approach |
|---|---|
| Web sessions | HttpOnly secure cookies. No localStorage bearer tokens. SameSite=Strict. Tokens never accessible to JavaScript. |
| Native sessions | Expo SecureStore / iOS Keychain-backed encrypted storage. Session token is never stored in plain AsyncStorage. |
| Service JWTs | Short-lived tokens (minutes, not hours). JWKS rotation. Used for native sync client authentication and service-to-service calls. Never long-lived API keys in environment variables. |
| OAuth | Standard PKCE flow. State parameter validated server-side. Redirect URIs pinned to registered origins. |
| SSO / SCIM | Deferred until required. Not in v1 plugin surface. Better Auth plugin interface owns it when enabled; no custom SAML/SCIM code in v1. |
Startup Invariants
Cold offline launch renders cached state immediately. No network request is on the critical path to first paint. The Better Auth session cache and the Zero local replica are opened concurrently; whichever completes first unblocks its dependent work, but neither blocks the initial UI render.
No component may render a global "session loading" spinner that blocks the entire app. Auth state is revalidated in the background. Components that require a confirmed authenticated session show skeleton states or degrade gracefully — they do not block the navigation shell from rendering.
- Account switching cannot expose previous user's local rows. Zero storage is keyed by
userId. Switching accounts either opens a separate storage namespace or triggers deletion of the previous user's data before the new session is activated. - Schema upgrade behavior is tested before release. Any Zero schema migration is tested in a scenario where the client upgrades while holding existing local data. Upgrade must not corrupt or silently drop user data.
- Auth credential refresh does not recreate Zero storage. Refreshing the sync JWT (step 4 in the startup sequence) must leave the Zero client and its local database intact. This is validated in Spike S4.