Skip to main content

Members & Invites

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, public invite lookup by code, and searching users. Mutations for updating member roles, removing members, creating/deleting invites, and accepting invites.
  • REST (apps/api/src/routes/members.rs) -- The same operations under /v1/accounts/{id}/members and /v1/invites.
  • Database (apps/api/src/db/members.rs) -- PostgreSQL operations for memberships, invites, user search, and platform connections lookup.
  • Permission Cache -- The GraphQL updateMemberRole and removeMember mutations and the REST role-change and remove-member endpoints invalidate the Redis permission cache via lo_api::middleware::auth::invalidate_permission_cache(), so a role change (grant or revocation) takes effect immediately instead of lingering until the cache TTL expires.
  • WebSocket -- Members and invites have no WebSocket channel; channel_gate_for in crates/lo-websocket/src/gate.rs defines no members/invites channel type. Roster changes are picked up on the next query.

Frontend

  • Team management page at /dashboard/members with member list and role badges (apps/web/src/app/(main)/(app)/dashboard/members/).
  • 4-step invite wizard (invite-dialog.tsx), in order: MethodRoleOptionsResult. The method step offers three cards: Invite Link (generate a shareable link), Invite Token (generate a code to share manually), and Invite Person (search by username, email, or platform and target a specific user). The result step shows the generated link/code to copy.
  • Account switcher (apps/web/src/components/account-switcher.tsx) for users with multiple account memberships.

API

GraphQL Queries

QueryPermissionDescription
members: [Member!]!members:readList all members of the current account (includes user info, role details, and linked platforms)
invites: [Invite!]!members:readList all invites for the current account
inviteByCode(code: String!): InviteDetail!Public (no auth)Public invite details (account name, inviter, role, isExpired / isFull) for the accept page
searchUsers(query: String!, limit: Int! = 10): [UserSearchResult!]!members:createSearch users by name, email, or platform username. Minimum 3 characters (shorter queries error). Excludes existing account members. limit clamps to 1–10. Returns user info with platform connections.

GraphQL Mutations

MutationPermissionDescription
updateMemberRole(input: UpdateMemberRoleInput!): MemberMutationResult!members:editChange a member's role. Cannot change the account owner's role or assign the owner system role. Invalidates the Redis permission cache.
removeMember(membershipId: UUID!): MemberMutationResult!members:deleteRemove a member from the account. Cannot remove the account owner. Invalidates the Redis permission cache.
createInvite(input: CreateInviteInput!): Invite!members:createCreate 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!): MemberMutationResult!members:deleteDelete an invite
acceptInvite(code: String!): AcceptInviteResult!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.

MethodPathPermissionDescription
GET/v1/accounts/{id}/membersmembers:readList members of an account
PATCH/v1/accounts/{id}/members/{membership_id}/rolemembers:editChange a member's role. Returns 204 No Content. Invalidates the Redis permission cache.
DELETE/v1/accounts/{id}/members/{membership_id}members:deleteRemove a member. Returns 204 No Content. Invalidates the Redis permission cache.
POST/v1/invitesmembers:createCreate an invite (role + optional email/user + expiry)
GET/v1/invitesmembers:readList account invites
GET/v1/invites/{code}/infoPublic (no auth)Invite lookup used by the accept page — the REST mirror of inviteByCode
DELETE/v1/invites/{id}members:deleteRevoke an invite
POST/v1/invites/{code}/acceptAuthAccept an invite as the authenticated user

Input Types

CreateInviteInput (GraphQL camelCase; the REST body uses the snake_case equivalents):

FieldTypeDefaultDescription
roleIdUUID!(required)Role to assign to the invited user
emailStringnullTarget email address; when set, an AccountInvite email is sent fire-and-forget
invitedUserIdUUIDnullTarget user ID (from user search); when set, an in-dashboard invite notification is created
maxUsesInt1Maximum number of times the invite can be used
expiresInHoursInt168 (7 days)Expiry duration in hours

UpdateMemberRoleInput: membershipId: UUID!, roleId: UUID!.

Permissions

PermissionDescription
members:readView member list and invites
members:createCreate invites, search users
members:editChange member roles
members:deleteRemove members, revoke invites

Database

TableDatabaseDescription
account_membershipsPostgreSQLLinks users to accounts. Columns: id, user_id, account_id, role_id (nullable FK to account_roles), created_at. UNIQUE(user_id, account_id). Queries alias created_at as joined_at, which is the name the API exposes.
account_invitesPostgreSQLInvite records. Columns: id, account_id, invited_by, invited_user_id, email, invite_code (unique), role_id, accepted_by, max_uses, use_count, expires_at, accepted_at, created_at
account_rolesPostgreSQLRoles that memberships and invites reference
usersPostgreSQLUser profiles (display_name, email, avatar_url)
login_connectionsPostgreSQLPlatform connections shown on members and user-search results (provider, username, display_name)

Data Flow

  1. An administrator creates an invite via the 4-step wizard, choosing a method, a role, and options.
  2. An invite record is created with a unique invite_code.
  3. Delivery depends on the input: an email triggers the AccountInvite transactional email; an invitedUserId creates an in-dashboard invite notification (plus the same email when the user has an address on record); a plain link or token is copied from the result step and shared manually.
  4. The target user opens the link, which calls acceptInvite(code) (or POST /v1/invites/{code}/accept).
  5. The system validates the invite: not expired, use_count below max_uses, and no existing membership for that user/account pair.
  6. A new account_memberships row is created, linking the user to the account with the invite's role.
  7. The invite use_count is incremented and accepted_at is set.

An invited user can also resolve the invite straight from the notification bell: the invite notification carries accept_invite / decline_invite actions. See Notifications.

Role Change Flow

  1. A caller with members:edit invokes updateMemberRole with the membership ID and new role ID.
  2. The system validates: the membership belongs to the active account, the target is not the account owner, the role exists, belongs to the same account, and is not the owner system role.
  3. The role is updated in the database.
  4. Both the GraphQL mutation and the REST endpoint invalidate the Redis permission cache for the affected user/account pair, so the change takes effect on the next request instead of after the cache TTL.

Key Files

PathDescription
apps/api/src/graphql/members.rsGraphQL queries and mutations
apps/api/src/routes/members.rsREST handlers for members and invites
apps/api/src/db/members.rsDatabase operations for memberships, invites, and user search
apps/api/src/db/roles.rsRole lookup for validation
apps/api/src/services/invite_notifications.rsIn-dashboard invite notification + email side channel
apps/web/src/app/(main)/(app)/dashboard/members/Team page and invite wizard