Skip to main content

Accounts

Overview

The accounts module manages the full account lifecycle: creation (with automatic JWT re-issue), name editing, account dissolution with confirmation, and leaving accounts as a non-owner member. Lumio supports multi-account switching -- a user can own multiple accounts and be a member of others.

Multi-Account Ownership

Users can own multiple Lumio accounts. This is controlled by two mechanisms:

  1. Feature flag -- the feature:multi_account flag must be enabled for the user's best account (the account whose plan has the highest max_accounts value).
  2. Plan-based limit -- each plan defines a max_accounts field that caps how many accounts the user can own:
    • Free: 1 account
    • Pro: 3 accounts

The effective limit is derived from the user's best plan: MAX(plans.max_accounts) across all accounts they own. Admins can override this per-user via max_accounts_override on the user record; when set, the override replaces the plan-derived limit entirely.

Account Creation Wizard

The dashboard presents a 3-step account creation wizard:

  1. Name & Description -- enter the account name and optional description.
  2. Assign Login Connections -- select which of the user's login connections to assign to the new account and choose a primary provider for the account avatar.
  3. Confirm -- review and create the account.

Primary Login Connection

Each account has an optional primary_login_connection_id that determines the account's avatar. When a primary connection is set, the avatar is pulled from the linked platform profile (e.g. the user's Twitch or YouTube profile picture). When no primary connection is set, the account falls back to the owner's users.avatar_url.

Only the account owner can change the primary connection. A login connection set as primary for an account cannot be disconnected or unassigned until a different primary is selected (or the primary is cleared).

API

ProtocolEndpointDescription
RESTPUT /v1/accounts/primary-connectionSet primary login connection (owner only)
GraphQLsetPrimaryLoginConnection(loginConnectionId, accountId)Set primary connection

Architecture

Dashboard UI / ID App
|
v
Next.js API Proxy (/api/accounts)
|
+---> REST API (POST/GET/DELETE /v1/accounts, POST /v1/accounts/{id}/leave)
| |
| +--> JWT re-issue on create/dissolve
|
+---> GraphQL (AccountQuery / AccountMutation)
|
v
db::overlays, db::auth, db::members, db::roles (PostgreSQL)

Account Creation Flow

  1. User calls POST /v1/accounts or the createAccount mutation. CreateAccountInput carries an optional name (defaults to the user's display name) and an optional planId (defaults to the Free plan).
  2. The system checks feature:multi_account is enabled and the user has not exceeded their max_accounts limit.
  3. The account is created with the user's display name and the specified plan_id.
  4. Default roles (Owner, Administrator, Moderator, Viewer) are created via db::roles::create_default_roles.
  5. The user is added as a member with the Owner role.
  6. All active sessions for the user are switched to the new account (active_account_id updated).
  7. A fresh JWT is issued containing the new account_id and returned alongside the account data.

Account Dissolution Flow

  1. User (must be owner, holding account:delete) calls DELETE /v1/accounts/{id} or dissolveAccount mutation with confirm_name matching the account name exactly. The REST path {id} must be the caller's active-session account — both protocols operate on the session account, and both require the account:delete permission (ZAF-450 S2).
  2. All sessions referencing this account have active_account_id set to NULL.
  3. The account's chat PII is erased from TimescaleDB (platform_chat_messages — viewer names and message text) before the account row is removed; that table lives outside the main database, so the CASCADE never reaches it. See Chat › Erasure (GDPR Art. 17).
  4. The account's stream-history statistics are retained, not deleted. The channel_history, channel_history_streams and channel_history_stats rows are broadcast statistics over a public stream (session duration, viewer curve, message and event counts, top emotes) rather than personal data of the account holder, so account deletion does not remove them. Any still-open session is only closed (its ended_at is stamped) so the sampler and startup recovery stop treating it as a live row for a now-deleted account. The manual operator purge that would delete these rows (delete_all_account_history) is no longer wired into dissolution (founder ruling ZAF-568).
  5. The account is deleted (CASCADE handles related tables: memberships, roles, overlays, etc.).
  6. A fresh JWT without account_id is returned so the user stays authenticated but can select another account.

Leave Account Flow

  1. Non-owner members can call POST /v1/accounts/{id}/leave or leaveAccount mutation.
  2. Ownership check prevents the owner from leaving (they must dissolve instead).
  3. The user's membership row is deleted.

API

REST Endpoints

MethodPathDescriptionAuth
POST/v1/accountsCreate a new accountAuthenticated
GET/v1/accounts/{id}Get account detailsActive account must match
DELETE/v1/accounts/{id}Dissolve accountaccount:delete + owner + confirm_name + path id = active account
POST/v1/accounts/{id}/leaveLeave an accountAuthenticated member
GET/v1/accounts/{id}/enabled-featuresEnabled feature keys for the accountAuthenticated (own account)
GET/v1/accounts/{id}/feature-statusesPer-feature resolution detail (global / plan / override)Authenticated (own account)
GET/v1/accounts/login-assignmentsList login-connection assignments on the accountlogin-assignments:read (own assignments always visible)
POST/v1/accounts/login-assignmentsAssign a login connection to the accountlogin-assignments:create (own always allowed)
DELETE/v1/accounts/login-assignments/{provider}Remove a login assignmentlogin-assignments:delete (own always allowed)
PUT/v1/accounts/primary-connectionSet primary login connectionOwner only

Member management (GET /v1/accounts/{id}/members, DELETE /v1/accounts/{id}/members/{membership_id}, PATCH /v1/accounts/{id}/members/{membership_id}/role) is documented under Members.

POST /v1/accounts

{
"plan_id": "uuid-of-plan"
}

When plan_id is omitted, the account is created on the Free plan.

Response (201):

{
"data": { "id": "...", "name": "...", "plan_id": "...", ... },
"token": "lm_eyJ..."
}

Response (403) — account creation disabled:

{
"error": "Account creation is currently disabled",
"error_code": "account_creation_disabled"
}

Account creation can be disabled globally by an administrator via the system:account_creation feature flag, or for a specific user via the per-user override on the user's admin detail page. The equivalent GraphQL error is extensions.code: "ACCOUNT_CREATION_DISABLED". See Feature Flags for the resolution chain.

DELETE /v1/accounts/{id}

Requires the account:delete permission and account ownership; the path {id} must equal the caller's active-session account (mirrors GraphQL dissolveAccount, which operates on the session account). Returns 403 when the permission is missing or the id does not match the active account. This destructive action — like POST /v1/accounts/{id}/leave and PUT /v1/accounts/primary-connection — additionally requires a first-party logged-in user session: a popout/overlay/widget/extension token is rejected with 403, so the owner authorization runs against the real session principal, not a popout-bound identity (ZAF-469).

{
"confirm_name": "My Account Name"
}

GraphQL Queries

QueryArgsReturnsPermission
accountid: UUID!Account?account:read
accountFeatures--AccountFeatureQuery!AuthGuard (works with popout-token auth)

Users can only query their own active account. accountFeatures is a namespaced query object exposing enabledFeatures: [String!]! and featureStatuses: [AccountFeatureDetail!]!; it deliberately needs no account:read so popout tokens can resolve their own feature set. The account is always taken from the auth context, never from a client parameter.

Account.plan is the plan slug ("free", "pro"), not the plan UUID. The primary_login_connection_id column is not exposed on the Account GraphQL type — it is set through setPrimaryLoginConnection and read back through the account avatar resolution in me.

GraphQL Mutations

MutationArgsReturnsPermission
createAccountinput: CreateAccountInput!CreateAccountResult!AuthGuard (any authenticated)
updateAccountid: UUID!, name: String!Account!account:edit
dissolveAccountconfirmName: String!DissolveAccountResult!account:delete
leaveAccount--LeaveAccountResult!AuthGuard (any authenticated)
setPrimaryLoginConnectionloginConnectionId: UUID, accountId: UUID!Boolean!Owner only
  • createAccount returns error extension { "code": "ACCOUNT_CREATION_DISABLED" } with message "Account creation is currently disabled" if the system:account_creation flag is globally off or the user has a deny override.
  • updateAccount validates that the name is not empty and the target account matches the active account.
  • dissolveAccount requires exact name confirmation and owner status.
  • leaveAccount has no permission guard beyond authentication -- any member can leave.

GraphQL Types

type Account {
id: UUID!
ownerId: UUID
name: String!
plan: String!
createdAt: String!
updatedAt: String!
}

type DissolveAccountResult {
success: Boolean!
}

type LeaveAccountResult {
success: Boolean!
}

Permissions

PermissionDescription
account:readView account details
account:editEdit account name
account:deleteDissolve account (also requires owner check)

Included in: Owner role has all three. Administrator has account:read and account:edit. The leaveAccount mutation bypasses permission guards (only requires authentication).

Database

Table: accounts

ColumnTypeDescription
idUUID (PK)Account ID
owner_idUUID (FK)Account owner user ID
nameTEXTAccount display name
plan_idUUID (FK)References the plans table
primary_login_connection_idUUID (FK, nullable)Login connection used for the account avatar. ON DELETE SET NULL.
created_atTIMESTAMPTZCreation timestamp
updated_atTIMESTAMPTZLast update timestamp

Related tables (CASCADE delete):

  • account_memberships -- User-account membership with role
  • account_roles -- Custom roles for the account
  • account_role_permissions -- Permissions assigned to roles
  • overlays, channel_rewards, se_tokens, integration_configs, etc.

Key Files

FilePurpose
apps/api/src/graphql/accounts.rsGraphQL queries and mutations
apps/api/src/routes/accounts.rsREST endpoints with JWT re-issue
apps/api/src/db/auth.rsUser lookup, session management
apps/api/src/db/roles.rsDefault role creation
apps/api/src/db/members.rsMembership management
crates/lo-auth/src/rbac.rsPermission constants (account:read/edit/delete)