Account And Session
Status: Running today.
RuntimeAccountServiceowns the local machine's account session truth, custody, login lifecycle, and first-party scoped binding issuance underK-ACCSVC-*.
RuntimeAccountService is the runtime authority for local first-party account identity: who is logged in on this machine, which account is currently active, how login / refresh / logout flow, and how short-lived access tokens get projected into admitted local first-party apps.
Authority Boundary
| Owns | Does NOT own |
|---|---|
| Local machine account session truth | App-side session state |
| Login / refresh / logout / switch lifecycle | App-issued tokens (apps don't issue them) |
| Refresh token custody | Refresh tokens being held by apps (apps may not hold them) |
| Daemon restart recovery | External principal sessions (those are RuntimeAuthService) |
| First-party short-lived app access-token projection | App workspace state |
| First-party scoped app binding issuance + revocation | Per-app conversation truth |
Account subject_user_id derivation | Caller-supplied subject_user_id (the runtime never trusts caller-supplied subject identity) |
App session and external-principal session live in RuntimeAuthService (K-AUTHSVC-*). The two services are not interchangeable.
Method Surface
RuntimeAccountService methods are frozen:
| Method | Purpose |
|---|---|
GetAccountSessionStatus | Current account session state |
SubscribeAccountSessionEvents | Event stream of session transitions |
BeginLogin | Start a login attempt |
CompleteLogin | Complete the login proof |
GetAccessToken | Get a runtime-issued short-lived access token |
RefreshAccountSession | Proactive / reactive session refresh |
Logout | Revoke local credentials + bindings |
SwitchAccount | Atomic active-account switch |
IssueScopedAppBinding | Issue a scoped binding for a first-party app |
RevokeScopedAppBinding | Revoke a previously issued binding |
New methods require an explicit kernel rule admission. The set is not extensible by app convention.
Session State Machine
| State | Meaning |
|---|---|
anonymous | No account session |
login_pending | A login attempt is in flight |
authenticated | Valid account material + projection |
refresh_pending | Refreshing account material |
expired | Material expired; cannot authorize work |
reauth_required | Needs user action to continue |
switching | Atomic active-account switch in progress |
logging_out | Revoking local material + bindings |
unavailable | Cannot safely decide / custody account state — fail-close |
Single-active-account invariant: one Runtime instance has at most one authenticated account at a time. SwitchAccount is atomic; two valid account projections cannot coexist.
Why Apps Don't Hold Refresh Tokens
The refresh token is the durable auth secret. If apps held it, every admitted local first-party app would be a separate refresh-token custody site — every app would have to implement secure storage, every app would be a separate compromise vector, and revocation would need to fan out per app.
Runtime owns refresh token custody once. Apps get short-lived access tokens via GetAccessToken and use them directly with admitted Realm data APIs. When the access token expires, the app asks the runtime for a new one. Refresh stays in the daemon.
Reader Scenario: Login On A Fresh Machine
- State
anonymous. No session exists. - App calls
BeginLogin. Runtime emitslogin.started; state moves tologin_pending. The app receives a pending attempt handle; repeatingBeginLoginbefore expiry returns the same pending attempt. - User completes the proof (web flow / device code / etc.).
- App calls
CompleteLogin. Runtime validates the proof, writes account material into custody, and emitslogin.completed+account.status. State moves toauthenticated. - App calls
GetAccessTokenwhen it needs to talk to Realm. Runtime returns a short-lived token.
The app never saw the refresh token. The app never wrote auth material to disk. The app does not own the account session.
Reader Scenario: Token Refresh Mid-Use
An app is in the middle of a long-running operation; the access token is about to expire.
- State
authenticated. App holds a short-lived access token. - Refresh starts. Runtime emits
refresh.started; state moves torefresh_pending. Only one refresh per account is in flight at a time. - Refresh succeeds. Runtime atomically swaps the new token for the old one. Emits
refresh.completed+account.status. State returns toauthenticated. - App's next
GetAccessTokenreturns the new token.
If refresh fails recoverably, state moves to reauth_required and the binding is suspended or revoked. The app cannot pretend the refresh succeeded.
Reader Scenario: Account Switch
A user has two Nimi accounts (work + personal) and switches.
- App calls
SwitchAccount. State moves toswitching. - Atomic transition. Runtime invalidates the previous account's projection and admits the new account's projection. No moment of "two accounts authenticated."
- State returns to
authenticated. Subscribers receiveaccount.statusreflecting the new active account.
Apps that subscribed to SubscribeAccountSessionEvents see the switch and re-derive their per-account state. There is no shared "current user" string apps can read across accounts.
Reader Scenario: Daemon Restart Recovery
- Daemon restarts. All in-process state is lost.
- Account custody persists. Refresh token + minimum required account state were durably stored by the runtime.
- State recovers to
authenticated(orexpired/reauth_requiredper the durable state). - Apps reconnect. Subscribed apps receive an
account.statusevent reflecting the recovered state. They do not have to re-prompt the user.
If the recovered state is expired, the app's next GetAccessToken fails closed — no implicit re-login.
Scoped App Bindings
IssueScopedAppBinding issues a binding that ties an admitted first-party app to the active account with a limited scope. The binding is what the app presents when it wants to act under the account's authority for a bounded purpose. RevokeScopedAppBinding takes it away.
Bindings:
- are runtime-issued, not app-issued
- are scoped (the binding declares its purpose)
- can be revoked by the runtime independently of logout
- get revoked automatically on logout
What Account Service Does Not Do
- It does not proxy every Realm data request — admitted local first- party apps may continue to call Realm data APIs directly with the short-lived token.
- It does not own app session truth — that's
RuntimeAuthService. - It does not own external principal sessions — also
RuntimeAuthService. - It does not accept caller-supplied
subject_user_id.
Boundary Summary
| Concern | Owner |
|---|---|
| Local machine account session + custody + lifecycle | RuntimeAccountService (K-ACCSVC-*) |
| App session + external principal session | RuntimeAuthService (K-AUTHSVC-*) |
| Token validation (incoming bearer JWT) | AuthN token validation (K-AUTHN-*) |
| Token authorization / ownership (who owns what) | AuthZ ownership |