Account
Version: 0.1
Date: June 2026
Author: kasunben
Purpose: Canonical specification for the Sovereign Account plugin — the single source of truth for its manifest, access model, functional requirements, data model, and build plan.
Status: v0.1 implemented — Profile, Security, and Preferences (ACC-01–11; see Changelog for per-requirement task attribution)
Account is the personal settings plugin for every authenticated Sovereign user. It covers profile management (display name, avatar), credentials (password change, active sessions, TOTP/passkey security), and preferences (timezone, appearance, sidebar customisation).
The plugin ships in the monorepo (type: platform) and is accessible at /account. It is separate from Console — Console is admin-only platform administration; Account is per-user self-service. Every authenticated user has access to Account regardless of role.
Sidebar rendering: In the sidebar bottom section, the Account plugin's slot renders the user's avatar image (or a monogram from their initials) rather than the generic icon.svg from the manifest. The shell hardcodes this special rendering for fs.sovereign.account. The icon.svg is used in the Launcher grid only.
Contents
- Identity and manifest
- Access control
- Functional requirements
- Directory structure
- Data model
- SDK dependencies
- UI
- Build plan
- Open questions
- Changelog
Identity and manifest
| Property | Value |
|---|---|
id | fs.sovereign.account |
name | Account |
type | platform |
runtime | native |
routePrefix | /account |
shell | overlay |
shellConfig | { overlaySize: "fixed" } |
adminOnly | omitted (false) |
icon | icon.svg |
permissions | auth:session, db:readWrite |
compatibility.minPlatformVersion | 0.4.0 |
Proposed manifest.json:
{
"schemaVersion": 1,
"id": "fs.sovereign.account",
"name": "Account",
"version": "0.1.0",
"description": "Personal profile, preferences, and credential management.",
"type": "platform",
"runtime": "native",
"routePrefix": "/account",
"shell": "overlay",
"shellConfig": { "overlaySize": "fixed" },
"icon": "icon.svg",
"permissions": ["auth:session", "db:readWrite"],
"compatibility": {
"minPlatformVersion": "0.4.0"
}
}No repository field — platform plugins live in the monorepo.
Access control
Account is available to all authenticated users. There is no admin-only gate.
Data is strictly per-user: a user can only read and modify their own profile, preferences, and sessions. There are no shared resources between users in this plugin.
Functional requirements
Requirements are versioned to their milestone. IDs are stable — never renumber or reuse an ACC-* id.
v0.1 — Core
Profile
| ID | Requirement |
|---|---|
| ACC-01 | View profile: display name, email address (read-only — email changes are out of scope in v1), and current avatar image or monogram. |
| ACC-02 | Change display name. The new name is validated (non-empty, max 100 chars). |
| ACC-03 | Upload or replace avatar image. Accepted formats: JPEG, PNG, WebP. Max size: 2 MB. The image is stored and served by the platform; the stored URL is written to the user record. |
Credentials
| ID | Requirement |
|---|---|
| ACC-04 | Change password. Requires current password confirmation. Delegates the credential update to better-auth. The current session is preserved after the change. |
| ACC-05 | View active sessions: device/browser hint, IP address, last-active timestamp, and whether the session is the current one. |
| ACC-06 | Revoke any active session except the current one. |
| ACC-11 | Sign out of the current session ("Log out"): ends the active session via better-auth and clears the session-cache cookies so it takes effect immediately, then redirects to login. Complements ACC-06 (which revokes other sessions only). Implements AUTH-02 in the Account UI; the same action is also reachable from the shell avatar menu. Both controls are plain form POSTs to the runtime logout route, so they work without JS. |
| ACC-09 | Two-factor authentication (TOTP): enroll via QR code, verify, enable/disable. Managed through better-auth's 2FA plugin. |
Preferences
| ID | Requirement |
|---|---|
| ACC-07 | Set timezone from the IANA timezone database (searchable dropdown). Stored in account_prefs. The platform uses this value for all date and time display across plugins. |
| ACC-08 | Set appearance: System (follow OS), Light, or Dark. Stored in account_prefs. The shell applies the corresponding data-theme attribute on the <html> element. Preference is applied immediately on change. |
| ACC-10 | Sidebar customisation: pin, unpin, and reorder plugins in the sidebar middle section. Preferences stored in account_prefs.sidebar_plugins (task 2.13). |
Not an SRS-numbered ACC requirement, but shipped alongside ACC-08 and stored in the same table: an in-app text-size control (Default / Large / Larger), tracked as epic task 10.2 (docs/epics/accessibility.md). Scales --sv-text-size-scale via data-text-size on <html>, discharging the accessibility debt from runtime/app/layout.tsx's userScalable: false. Same applied-pre-paint, applied-immediately shape as ACC-08.
Directory structure
Account lives in the monorepo under plugins/account/. The plugin has grown from the original 3-tab layout to 7 sections, driven by app/_lib/sections.ts's ACCOUNT_SECTIONS (the single source of truth for both the desktop rail and the mobile drill-down index — see UI):
plugins/account/
├── manifest.json
├── icon.svg # Account icon (user silhouette or similar)
├── app/
│ ├── layout.tsx # Vertical rail nav (desktop) / mobile header + back-link — task 14.5
│ ├── page.tsx # Bare /account — mobile drill-down index / desktop empty-state (task 14.6; no longer a redirect)
│ ├── profile/
│ │ └── page.tsx # Display name + avatar
│ ├── security/
│ │ └── page.tsx # Password change, 2FA, passkeys, active sessions
│ ├── preferences/
│ │ └── page.tsx # Timezone, appearance, text size, sidebar customisation
│ ├── notifications/
│ │ └── page.tsx # Notification + communication-email preferences, push opt-in
│ ├── billing/
│ │ └── page.tsx # Entitlement / billing status
│ ├── data/
│ │ └── page.tsx # Export/import, backup destinations, git-backed restore
│ ├── activity/
│ │ └── page.tsx # Personal activity log
│ ├── _components/ # AvatarUpload, SessionList, TimezoneSelect, ThemeControl,
│ │ # TextSizeControl, TotpSection, PasskeySection, and more
│ └── _lib/
│ └── sections.ts # ACCOUNT_SECTIONS — single source of truth for the rail and drill-down index
└── db/
└── schema.ts # account_prefs tableComponents live under app/_components/ — a private App Router folder, not a sibling components/ as originally sketched — because the generate script composes only each plugin's app/ tree into the runtime (see the Changelog). There is no migrations/ directory: db/schema.ts marks the plugin's db/ directory, but the authoritative Drizzle definition for account_prefs lives in packages/db until sdk.db exposes a scoped, plugin-owned client.
Data model
One table, prefixed account_ (platform plugin convention). Carries tenant_id per the platform architectural rule.
account_prefs
| Column | Type | Notes |
|---|---|---|
user_id | string | PK. FK → users. |
tenant_id | string | |
timezone | string | IANA timezone identifier (e.g. America/New_York). Default: UTC. |
theme | string | Enum: system | light | dark. Default: system. |
sidebar_plugins | text | JSON-serialised Array<{ id, hidden }>; null = default install order (task 2.13). |
text_size | string | Enum: default | large | larger. Default: default (task 10.2). |
updated_at | timestamp |
One row per user; upserted on any preference change.
Avatar storage: Avatar images are stored outside this table. The exact storage mechanism (platform file storage, object store, base64 in a separate account_avatars table) is an open question — see Open questions §1. The user record in packages/db (maintained by the platform, not this plugin) holds the avatar_url string that the shell and other plugins read for display.
SDK dependencies
| SDK surface | Used for | Available from |
|---|---|---|
sdk.auth | Current user session; password change via better-auth; session listing/revoke | Task 0.4.2 |
sdk.db | Read/write account_prefs; read/write avatar_url on the user record | Task 0.5.5 |
UI
Account consumes @sovereignfs/ui exclusively.
Layout: A vertical rail nav on desktop (NavList variant="static", task 14.5) lists all 7 sections — Profile, Security, Preferences, Notifications, Billing, Data, Activity — beside the routed content. Mobile has no persistent rail: the bare /account route renders a NavList variant="drilldown" index instead (task 14.6), and every other section shows a ‹ Account back link above its content. Uses shell: overlay (RFC 0001, overlaySize: "fixed"): clicking the avatar opens Account as a dialog over the current page (a full-screen sheet on mobile); a hard load of /account renders the same content as a full page. Every in-dialog navigation link uses router.replace, not push, since the dialog is dismissed via router.back() — a push would stack history and require more than one back to exit. Because the root plugin serves / as a full page, an overlay plugin is not eligible as the root plugin (CON-11).
Net-new primitives likely needed in packages/ui:
- Avatar upload — image picker with circular crop preview and upload progress. Broadly reusable wherever user-generated images appear.
- Timezone select — searchable dropdown over 600+ IANA timezone entries with UTC offset display. Complex enough to warrant a shared component.
- Segmented control — three-option horizontal toggle for Light / Dark / System mode. Reusable for other multi-choice preference settings.
- Session row — a table row showing session metadata with a contextual "Revoke" action for other sessions and a "Log out" action for the current one (ACC-11). Could generalise to other "active credential" listings.
The current-session "Log out" (ACC-11) shares the platform logout action with the shell avatar menu (the primary entry point); see Task 0.5.12.
Build plan
Two milestones.
v0.1 — Core (ACC-01–08)
Profile (display name + avatar upload), password change, active session listing with revoke, timezone setting, appearance toggle.
Done when: A user can update their display name, upload an avatar, change their password, revoke other sessions, set their timezone, and toggle light/dark mode — with all preferences persisted and applied immediately.
Logout (ACC-11) — Task 0.5.12
Self sign-out, closing the long-standing AUTH-02 gap (specified but never built). Adds sdk.auth.signOut() and a "Log out" action on the current-session row, sharing the platform logout flow with the shell avatar menu (better-auth sign-out + session-cache-cookie clear + redirect to login). Sequenced after v0.1.
v0.2 — Security and customisation (ACC-09–10)
TOTP two-factor authentication via better-auth 2FA plugin. Sidebar customisation (pin/unpin/reorder) — see ACC-10.
Open questions
Avatar storage mechanism. ✅ Resolved (Task 0.4.6 part 1): option (b). Avatars are stored on disk at
data/avatars/<user_id>.<ext>(workspace-rootdata/, resolved like the SQLite DB) and served by the runtime routeGET /api/account/avatar/[userId]. Upload goes toPOST /api/account/avatar(validates JPEG/PNG/WebP ≤ 2 MB), which writes the file and sets the user record'simageto the cache-busted serve URL via better-auth. Migration to an object store remains a clean future swap behind the same routes.Email change. Email is currently read-only (ACC-01). Changing email requires re-verification and potentially
better-authworkflow changes. Flag as a v0.2 consideration rather than a v0.1 open question — out of scope for now.Display name vs. username. The platform may have both a
name(display name, changeable) and ausernameoremail(login identifier, immutable). ACC-02 targets the display name only. Confirm the platform user schema distinguishes these two fields before implementing.Appearance preference and SSR. ✅ Resolved (Task 0.4.6 part 1). The choice is written to both
account_prefs(authoritative) and ansv-themecookie. An inline script in the runtime root layout (runtime/app/layout.tsx) runs before first paint and setsdata-themefrom the cookie —light/darkdirectly,system(or unset) followingprefers-color-scheme. This avoids a flash and resolvessystem(which the server can't, lacking the OS hint) without a per-request DB call. TheThemeControlalso applies the new value instantly on change before the persist round-trip.
Changelog
| Version | Date | Change |
|---|---|---|
| 0.1 | Jun 2026 | Initial draft — per-user profile and preferences plugin. |
| 0.1 | Jun 2026 | Part 1 implemented (Task 0.4.6): Profile (ACC-01/02/03) + Preferences (ACC-07/08). Resolved Q1 (avatar → disk + Next route) and Q4 (theme → account_prefs + sv-theme cookie + pre-paint inline script). Deviation: components live under app/_components/ (composition copies only app/). Security (ACC-04–06) follows in part 2. |
| 0.1 | Jun 2026 | Part 2 implemented (Task 0.4.6): Security tab — password change (ACC-04) + active-session list/revoke (ACC-05/06). Extended sdk.auth with changePassword/listSessions/revokeSession (wrap better-auth with cookie + Origin). v0.1 complete. |
| 0.1 | Jun 2026 | Specified ACC-11 (self sign-out / "Log out") to close the AUTH-02 gap — current-session Log out action + shell avatar menu, sdk.auth.signOut(), better-auth sign-out + session-cache-cookie clear. Scheduled as Task 0.5.12 (not yet implemented). |
| 0.1 | Aug 2026 | Implemented task 10.2 (in-app text-size control): text_size column on account_prefs, TextSizeControl in Preferences alongside ThemeControl, applied pre-paint via the same sv-text-size cookie + inline-script mechanism as sv-theme. Also backfilled the sidebar_plugins column (task 2.13) into this doc's data-model table, which had drifted out of sync with the schema. |
| 0.1 | Sep 2026 | Corrected the Functional requirements section: ACC-09 (TOTP, task 1.4/RFC 0012, shipped at platform v0.5.27) and ACC-10 (sidebar customisation, task 2.13, shipped at platform v0.9.3) were still listed under a "v0.2 — Security and customisation (post-v1)" heading as unbuilt, despite both being live in the shipped code (TotpSection.tsx/PasskeySection.tsx in security/page.tsx; SidebarControl.tsx in preferences/page.tsx) — confirmed by reading both route files directly, not assumed from file presence alone. Moved both into the v0.1 Core tables (Credentials and Preferences respectively) and removed the now-empty v0.2 heading; corrected ACC-10's own wording, which had described a since-abandoned separate account_sidebar_prefs table rather than the account_prefs.sidebar_plugins column task 2.13 actually shipped. |