Browse documentation

Two separate concepts live here, and mixing them up is the usual confusion:

LayerStoreWhat it means
Console usersBetter Auth (SQLite, under PROJECTS_DIR)Operators who log into the Console
Tenant orgsconsole-orgs.db (SQLite)Organization → projects grouping, Supabase-style
Project ACLBetter Auth organization modelOne org per project; org.name holds the project id. Used as the access list, not as a tenant

console/services/org-store.ts says it explicitly: the tenant store is deliberately separate from the Better Auth organization model that the Console already uses as the per-project ACL.

Console users

console/services/users.service.ts wraps Better Auth's low-level adapters rather than its HTTP layer — this is a trusted server-side admin service, and going through the adapter avoids needing an admin session to seed the very first owner.

interface User {
  id; email
  role: 'owner' | 'admin' | 'developer' | 'read_only' | 'member'
  projects: string[]
  disabled: boolean
  mustChangePassword: boolean
  createdAt: string
}

Mapping to Better Auth:

Console fieldBetter Auth
role, disabledAdditional fields on user
projectsOrganization memberships — one org per project, org.name = project id
passwordA credential account holding a bcrypt hash (console/auth/password.ts)

Available operations: list, findByEmail, findById, create, provision, update, setPassword, verifyPassword, revokeSessions, delete, ownerCount, projectsOf, seedOwner, seedOwnerWithHash.

Note

An unknown or malformed role coerces to read_only (coerceRole), never to something more privileged. Role and project list are reloaded live from Better Auth on every request — never trusted from the client.

Capabilities

console/auth/capabilities.ts defines nine capabilities and maps roles onto them:

Capabilityowneradmindevelopermemberread_only
project.readyesyesyesyesyes
content.writeyesyesyesyes—
project.createyesyes———
settings.writeyesyes———
runtime.manageyesyes———
secrets.manageyesyes———
project.deleteyesyes———
audit.readyesyes———
users.manageyes————

Only owner can manage users. Guards:

requireCapability(cap)         // 401 without session, 403 without the capability
requireProjectCapability(cap)  // same, plus project scope
hasProjectScope(role, projects, projectId)
  // owner and admin see every project; everyone else only their assigned list

Password rotation

enforcePasswordRotation blocks a session with mustChangePassword from doing anything except changing it. The allowed routes are the password-change endpoint plus session-critical auth routes (get-session, sign-out, and the three two-factor verify routes). MIN_PASSWORD is 8 characters anywhere the Console sets or changes one.

Seeding an operator with passwordTemporary: true (or CONSOLE_ADMIN_PASSWORD_TEMPORARY=1) puts them in exactly that state on first login.

Tenant organizations

OrgStore is a small SQLite database (<PROJECTS_DIR>/console-orgs.db, WAL mode) with three tables:

organization (id, name, slug UNIQUE, createdAt)
org_member   (orgId, userId, role, createdAt)   -- PK (orgId, userId)
project_org  (project PRIMARY KEY, orgId)

Org roles are owner, admin, member; anything else coerces to member. Slugs are lowercased, hyphenated, alnum-only, capped at 48 characters, with uniqueness resolved at create time (slugify).

Schema creation is idempotent and runs at every boot. backfill() moves an existing single-tenant install into a default org so the org switcher always has one and current logins keep seeing their projects.

Endpoints

MountRouter
/api/platform/usersconsole/routes/users.routes.ts
/api/platform/orgsconsole/routes/orgs.routes.ts

Mutations are recorded in the audit log; GET, HEAD and OPTIONS are not.