Lumen Docs

Role + tier model

Lumen has three orthogonal dimensions of access. Every user has values on all three; combined, they determine what the user can see and do.

1. Platform role

Who you are to the Lumen platform itself.

| Role | What it means | Who gets it | |---|---|---| | superadmin | Bootstrap owner. Bypasses every permission check. Singleton, non-transferable. | The first person who runs the wizard. | | admin | Platform manager. Can manage users, departments, projects, share requests. Cannot touch LLM providers. | Assigned by superadmin. | | engineer | Platform engineer. Manages LLM providers, workers, logs, system config. Does not manage users. | Assigned by superadmin. | | none | Regular user. Access comes from CEO/owner/grant only. | Default. |

Superadmin is created once during bootstrap and cannot be assigned via POST /users — the API rejects it with forbidden_role. This is intentional: we want exactly one root account.

2. Org position

Your position in the org chart. Independent of platform role.

| Position | Meaning | |---|---| | ceo | Cross-department observer. Read-only use tier on every project regardless of department. Singleton. | | manager | Leads a department. Full use + edit + full in granted projects. Can create groups under their dept. | | member | Team member. Access via dept, groups, and direct grants. |

A single user has one position. ceo and superadmin are both singletons — enforced by partial unique indexes (single_ceo, single_superadmin).

3. Access tier

Tier on a specific project.

| Tier | Means | |---|---| | use | Read docs, chat, view project — can't edit settings or docs. | | edit | Everything in use, plus add/remove docs, edit project instructions. | | full | Everything in edit, plus grant access to others, change settings, delete project. |

Tiers are monotonic: full ⊇ edit ⊇ use. Helper: hasTier(tier, "edit") returns true for edit and full.

How tiers are obtained

A user's tier on a project resolves through six sources in priority order:

  1. Platform roleadmin or superadmin or engineerfull
  2. CEOuse (cross-dept observer)
  3. Ownerproject.ownerId === user.idfull
  4. Direct grantProjectGrant row with userId = user.id
  5. Group grant — user is in a group that has a grant
  6. Department grant — user's department has a grant
  7. Public baselineproject.isPrivate = falseuse

The first matching source wins. Direct beats group beats dept. Public is a baseline only — any explicit grant upgrades it.

See Resolver priority for the full decision table and source code pointer.

Where it's enforced

| Surface | Enforcement | |---|---| | Frontend | Sidebar nav gated by platformRole (in components/layout/sidebar.tsx). Hides Admin / Engineer areas from regular users. | | API | requirePlatformRole([…]), requireProjectAccess(param, tier), requireCeoOrManager(param) middleware in apps/api/src/middleware/access.ts. | | DB | Partial unique indexes enforce singletons. CHECK constraint grant_one_target on project_grants ensures exactly one of (userId, groupId, departmentId) is set per row. |

Never trust the client — the source of truth for tier is the resolver, called server-side on every protected read or write.

Bootstrap singleton flows

The wizard at /onboarding/first-login creates the superadmin and CEO in a single transaction, then flips bootstrap_state.is_initialized. After that:

  • POST /bootstrap/init always 409s
  • requireNotBootstrapped middleware blocks any endpoint that assumes a fresh DB
  • Middleware requireBootstrapped gates every user-facing API

This means you cannot run two instances of bootstrap concurrently or re-bootstrap an existing DB without a manual DB reset.