Overview
The Members & Invites module provides team management for Lumio accounts. Account owners and administrators can invite users to their account, assign roles to members, and manage the team roster. The invite system supports invite links with configurable expiry and maximum uses, role assignment at invite time, and user search across the platform. Members can be part of multiple accounts, enabling account switching for multi-account users.
Architecture
Backend
- GraphQL (
apps/api/src/graphql/members.rs) -- Queries for listing members, listing invites, and searching users. Mutations for updating member roles, removing members, creating/deleting invites, and accepting invites.
- Database (
apps/api/src/db/members.rs) -- PostgreSQL operations for memberships, invites, user search, and platform connections lookup.
- Permission Cache -- When a member's role is changed or a member is removed, the Redis permission cache is invalidated via
lo_api::middleware::auth::invalidate_permission_cache().
Frontend
- Team management page with member list and role badges.
- 4-step invite wizard: select role, choose method (link/email/user search), configure options, copy invite link.
- Account switcher for users with multiple account memberships.
API
GraphQL Queries
| Query | Permission | Description |
|---|
members | members:read | List all members of the current account (includes user info and role details) |
invites | members:read | List all invites for the current account |
inviteByCode(code) | Public (no auth) | Public invite details (account name, role, expiry/use status) for the accept page |
searchUsers(query, limit?) | members:create | Search users by name, email, or platform username. Minimum 3 characters. Excludes existing account members. Max 10 results. Returns user info with platform connections. |
GraphQL Mutations
| Mutation | Permission | Description |
|---|
updateMemberRole(input: UpdateMemberRoleInput) | members:edit | Change a member's role. Cannot change the account owner's role or assign the owner role. Invalidates Redis permission cache. |
removeMember(membershipId: UUID) | members:delete | Remove a member from the account. Cannot remove the account owner. Invalidates Redis permission cache. |
createInvite(input: CreateInviteInput) | members:create | Create an invite with role assignment, optional email/user targeting, max uses (default 1), and expiry (default 168 hours / 7 days). Cannot create invites with the owner role. |
deleteInvite(inviteId: UUID) | members:delete | Delete an invite |
acceptInvite(code: String) | AuthGuard (any authenticated user) | Accept an invite by code. Validates expiry, max uses, and duplicate membership. Creates a new membership. |
REST Endpoints
All paths live under /v1. Bodies are snake_case and mirror the GraphQL inputs.
| Method | Path | Permission | Description |
|---|
GET | /v1/accounts/{id}/members | members:read | List members of an account |
PATCH | /v1/accounts/{id}/members/{membership_id}/role | members:edit | Change a member's role |
DELETE | /v1/accounts/{id}/members/{membership_id} | members:delete | Remove a member |
POST | /v1/invites | members:create | Create an invite (role + optional email/user + expiry) |
GET | /v1/invites | members:read | List account invites |
GET | /v1/invites/{code}/info | Auth | Public lookup used by the accept page |
DELETE | /v1/invites/{id} | members:delete | Revoke an invite |
POST | /v1/invites/{code}/accept | Auth | Accept an invite as the authenticated user |
CreateInviteInput:
| Field | Type | Default | Description |
|---|
roleId | UUID | (required) | Role to assign to the invited user |
email | String? | null | Target email address |
invitedUserId | UUID? | null | Target user ID (from user search) |
maxUses | i32? | 1 | Maximum number of times the invite can be used |
expiresInHours | i32? | 168 (7 days) | Expiry duration in hours |
Permissions
| Permission | Description |
|---|
members:read | View member list and invites |
members:create | Create invites, search users |
members:edit | Change member roles |
members:delete | Remove members, revoke invites |
Database
| Table | Database | Description |
|---|
account_memberships | PostgreSQL | Links users to accounts. Fields: id, user_id, account_id, role_id, joined_at |
account_invites | PostgreSQL | Invite records. Fields: id, account_id, invited_by, invited_user_id, email, invite_code (unique), role_id, max_uses, use_count, expires_at, accepted_at, created_at |
users | PostgreSQL | User profiles (display_name, email, avatar_url) |
login_connections | PostgreSQL | Platform connections for user search results (provider, username) |
Data Flow
- Administrator creates an invite via the 4-step wizard, selecting a role and configuring options.
- An invite record is created with a unique
invite_code.
- The invite link is shared with the target user.
- Target user clicks the link, which calls
acceptInvite(code).
- The system validates the invite (expiry, max uses, no duplicate membership).
- A new
account_membership record is created, linking the user to the account with the specified role.
- The invite
use_count is incremented and accepted_at is set.
Role Change Flow
- Admin calls
updateMemberRole with the membership ID and new role ID.
- System validates: membership belongs to account, target is not the owner, role exists and is not "owner".
- Role is updated in the database.
- Redis permission cache is invalidated for the affected user/account pair.
Key Files
| Path | Description |
|---|
apps/api/src/graphql/members.rs | GraphQL queries and mutations |
apps/api/src/db/members.rs | Database operations for memberships and invites |
apps/api/src/db/roles.rs | Role lookup for validation |