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:
- Feature flag -- the
feature:multi_accountflag must be enabled for the user's best account (the account whose plan has the highestmax_accountsvalue). - Plan-based limit -- each plan defines a
max_accountsfield 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:
- Name & Description -- enter the account name and optional description.
- 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.
- 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
| Protocol | Endpoint | Description |
|---|---|---|
| REST | PUT /v1/accounts/primary-connection | Set primary login connection (owner only) |
| GraphQL | setPrimaryLoginConnection(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
- User calls
POST /v1/accountsorcreateAccountmutation with an optionalplan_id(defaults to the Free plan). - The system checks
feature:multi_accountis enabled and the user has not exceeded theirmax_accountslimit. - The account is created with the user's display name and the specified
plan_id. - Default roles (Owner, Administrator, Moderator, Viewer) are created via
db::roles::create_default_roles. - The user is added as a member with the Owner role.
- All active sessions for the user are switched to the new account (
active_account_idupdated). - A fresh JWT is issued containing the new
account_idand returned alongside the account data.
Account Dissolution Flow
- User (must be owner) calls
DELETE /v1/accounts/{id}ordissolveAccountmutation withconfirm_namematching the account name exactly. - All sessions referencing this account have
active_account_idset toNULL. - The account is deleted (CASCADE handles related tables: memberships, roles, overlays, etc.).
- A fresh JWT without
account_idis returned so the user stays authenticated but can select another account.
Leave Account Flow
- Non-owner members can call
POST /v1/accounts/{id}/leaveorleaveAccountmutation. - Ownership check prevents the owner from leaving (they must dissolve instead).
- The user's membership row is deleted.
API
REST Endpoints
| Method | Path | Description | Auth |
|---|---|---|---|
POST | /v1/accounts | Create a new account | Authenticated |
GET | /v1/accounts/{id} | Get account details | Active account must match |
DELETE | /v1/accounts/{id} | Dissolve account (owner only) | Owner + confirm_name |
POST | /v1/accounts/{id}/leave | Leave an account | Authenticated member |
PUT | /v1/accounts/primary-connection | Set primary login connection | Owner 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
| Query | Args | Returns | Permission |
|---|---|---|---|
account | id: UUID | Account? | account:read |
Users can only query their own active account.
GraphQL Mutations
| Mutation | Args | Returns | Permission |
|---|---|---|---|
createAccount | input: CreateAccountInput! | CreateAccountResult! | AuthGuard (any authenticated) |
updateAccount | id: UUID!, name: String! | Account! | account:edit |
dissolveAccount | confirmName: String! | DissolveAccountResult! | account:delete |
leaveAccount | -- | LeaveAccountResult! | AuthGuard (any authenticated) |
setPrimaryLoginConnection | loginConnectionId: UUID, accountId: UUID! | Boolean! | Owner only |
createAccountreturns error extension{ "code": "ACCOUNT_CREATION_DISABLED" }with message"Account creation is currently disabled"if thesystem:account_creationflag is globally off or the user has adenyoverride.updateAccountvalidates that the name is not empty and the target account matches the active account.dissolveAccountrequires exact name confirmation and owner status.leaveAccounthas 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
| Permission | Description |
|---|---|
account:read | View account details |
account:edit | Edit account name |
account:delete | Dissolve 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
| Column | Type | Description |
|---|---|---|
id | UUID (PK) | Account ID |
owner_id | UUID (FK) | Account owner user ID |
name | TEXT | Account display name |
plan_id | UUID (FK) | References the plans table |
primary_login_connection_id | UUID (FK, nullable) | Login connection used for the account avatar. ON DELETE SET NULL. |
created_at | TIMESTAMPTZ | Creation timestamp |
updated_at | TIMESTAMPTZ | Last update timestamp |
Related tables (CASCADE delete):
account_memberships-- User-account membership with roleaccount_roles-- Custom roles for the accountaccount_role_permissions-- Permissions assigned to rolesoverlays,channel_rewards,se_tokens,integration_configs, etc.
Key Files
| File | Purpose |
|---|---|
apps/api/src/graphql/accounts.rs | GraphQL queries and mutations |
apps/api/src/routes/accounts.rs | REST endpoints with JWT re-issue |
apps/api/src/db/auth.rs | User lookup, session management |
apps/api/src/db/roles.rs | Default role creation |
apps/api/src/db/members.rs | Membership management |
crates/lo-auth/src/rbac.rs | Permission constants (account:read/edit/delete) |