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 createAccount mutation with an optional plan_id (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) calls DELETE /v1/accounts/{id} or dissolveAccount mutation with confirm_name matching the account name exactly.
  2. All sessions referencing this account have active_account_id set to NULL.
  3. The account is deleted (CASCADE handles related tables: memberships, roles, overlays, etc.).
  4. 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 account (owner only)Owner + confirm_name
POST/v1/accounts/{id}/leaveLeave an accountAuthenticated member
PUT/v1/accounts/primary-connectionSet primary login connectionOwner only

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}

{
"confirm_name": "My Account Name"
}

GraphQL Queries

QueryArgsReturnsPermission
accountid: UUIDAccount?account:read

Users can only query their own active account.

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!
planId: UUID
primaryLoginConnectionId: UUID
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)