User management is controlled by role-based permissions. Super admins have full access, while other users need explicit custom role permissions (e.g. a "Prospect Manager" role) to manage users from Admin > Users.
The platform has four built-in roles. Their behavior is fixed by code, so the role editor for these is locked down — open them in Admin > Roles and you'll see an info banner instead of a form (Visitor is the one exception; see below). Extend a user's access by assigning custom roles on top of the built-in:
| Role | Description | Manager Access | Editor |
|---|---|---|---|
visitor | Not signed in. Can only view published content. Default permissions are tunable (e.g. allow visitors to submit contact forms or subscribe). | None | Permissions field editable; other fields locked. |
user | Signed in. Can manage their own profile, view their own orders, and view their own carts. Self-only scoping is enforced in code. | None | Banner only (locked). Extend via custom roles. |
admin | Grants access to the admin dashboard for data management, plus visitor + user-level data access. Table-level permissions (CRUD on Articles, Products, etc.) come from custom roles paired alongside Admin. | Dashboard entry only — table CRUD requires custom roles | Banner only (locked). Extend via custom roles. |
super | Full access to all tables and actions. Hardcoded — cannot be revoked or narrowed via the UI. | Full | Banner only (locked). |
Base permissions by role. Admins can gain additional permissions through custom roles assigned by a Super admin:
| Action | Visitor | User | Admin (base) | Super |
|---|---|---|---|---|
| Read public records | Yes | Yes | Yes | Yes |
| Read own user data | - | Yes | Yes | Yes |
| Update own profile | - | Yes | Yes | Yes |
| CRUD data tables | - | - | Via custom roles | Yes |
| Manage users | - | - | Via custom roles | Yes |
| Assign roles | - | - | Via custom roles (cannot assign super/admin) | Yes |
| Delete records | - | - | - | Yes |
| Edit site config | - | - | - | Yes |
Custom roles grant specific table-level permissions to admins. A Super admin creates custom roles from Admin > Roles and assigns them to users. For example:
moas.canExpense flag. Pairs with a MOAS Manager who approves the resulting expense orders.moas.canApprove + approverFor: ["moas-buyer"].super or admin roles, and no user can modify their own roles. User deletion remains super-only.A custom role's table permission can include record filters that scope which rows a signed-in user can see or modify. Filter values support ${user.<field>} placeholders that resolve at read time to the actual signed-in user's data — so the same role definition produces different per-user results.
Supported placeholders include any field on the user record:
${user.id} - The user's database ID${user.email} - Their email address${user.handle} - Their public handle (e.g. jdoe)${user.name} - Their display name${user.region} if you've added a custom region field)Concrete patterns this unlocks:
| Use case | Custom role | Table | Filter |
|---|---|---|---|
| Author-only article editing | Contributor | article | authoredBy equals ${user.handle} |
| Sales rep order ownership | Sales Rep | order | assignedRepId equals ${user.id} |
| Regional content scoping | Editor (West Coast) | article | region equals ${user.region} |
| Vendor portal (multi-supplier) | Vendor | product | supplierId equals ${user.supplierId} |
| Helpdesk ticket assignment | Support Rep | contact | assignedTo equals ${user.id} |
contact.email contains ${user.email}). It's available to any custom role on any table — pair the right filter with the table you want to scope.Fail-safe behavior: if a placeholder can't be resolved (e.g. the field doesn't exist on the user record, or the signed-in user lacks that field), the filter is left as the literal placeholder string. The downstream query then matches nothing — so the role never leaks unscoped data when a placeholder is misconfigured. Check the server logs for [role-filter] Unresolved placeholder warnings if a role isn't returning the expected rows.
Users can be created in two ways: self-registration (if sign-up is enabled in Site Config) or admin onboarding (a Super admin creates the account manually).
When a visitor signs up, they provide their name, email, and password. The account is created with the user role by default and an active status. Sign-up can be disabled entirely from Site Config > Security > Allow sign up.
Users with the appropriate custom role permissions (e.g. "Prospect Manager") or Super admins can create accounts on behalf of others from the Users table. This triggers the welcome email flow:
pending status and generates a secure onboarding token.active and they can sign in normally.Every user account has a status that controls their ability to sign in and access the platform:
| Status | Can Sign In | Description |
|---|---|---|
active | Yes | Normal operating state. Full access based on assigned roles. |
pending | No | Account created via admin onboarding but user has not completed setup. |
suspended | No | Temporarily blocked by an admin. User cannot sign in until reinstated. |
inactive | No | Deactivated account. User cannot sign in until reactivated. |
To revoke a user's access, a Super admin changes their status to suspended or inactive from the Users table (user status is a super-only field). This has two effects:
active, the session is invalidated and the user is signed out.To reinstate access, change the status back to active. The user can then sign in again with their existing credentials.
The platform uses JWT-based sessions. When a user signs in, a JSON Web Token is created containing their identity, roles, and status. Key behaviors:
From the Users table, authorized users can:
super or admin roles, and nobody can modify their own roles.Every signed-in user has access to their user page at Dashboard > User. The page is split into two sections — Account (private; only the user and admins see it) and Profile (opt-in public surface).
prefers-color-scheme). Rendered as a 3-segment switcher next to the page heading. New users default to System.These fields appear at /user/<handle> when the Publish profile toggle is on. Fields are marked with a Globe badge in the form — solid when published, dimmed when hidden.
publicName) - The name shown publicly. Falls back to @handle when blank.publicLocation) - Optional public location string.publicBio) - Optional public bio.publicSocials) - List of public social media URLs.readUserProfile() uses an explicit Prisma select that names only the public* columns + avatar + handle. Adding a column to the User table does NOT expose it publicly until it's added to that select list.accountBalance, roles, status, and confirmed are filtered out of the self-edit surface entirely — even supers see them only via the admin DataManager at /admin/data/user. Password reset and 2FA management live on the dedicated Security tab.
Users can enable 2FA from the Security tab using a TOTP authenticator app (Google Authenticator, Authy, etc.). Once enabled, sign-in requires both the password and a time-based one-time code.
The Roles table (Admin > Roles) defines the available roles in the system. The four built-in roles (visitor, user, admin, super) are seeded once and locked. Super admins manage the Roles table and create custom roles for everything else.
Each role has a name, description, optional notification email, and a set of table-level permissions (with optional record filters and hidden fields). Roles are assigned to users as an array, so a user can hold multiple roles. Permissions from all assigned roles are unioned — the combined set of granted actions determines effective access.
Opening a built-in role in the editor renders an info banner instead of a form. This is intentional — built-in role behavior is enforced in code, so editing the stored permissions, slug, name, or status would have no effect (or worse, cause a silent break). The exception is visitor, where the permissions field stays editable so admins can tune what non-authenticated users are allowed to do (e.g. submit contact forms, subscribe).
Built-in roles also cannot be deleted from the Roles table — the API rejects any deletion attempt on a record where static is true.
Custom roles like "Blogger", "Shop Manager", or "Comms Director" allow fine-grained delegation without granting full admin or super access. They support all the same per-user record filters described above.
In addition to the table-level permission array, each role has three top-level fields that power the B2B procurement features:
/dashboard/approvals and act on its queue. Doesn't by itself say WHICH orders they approve — pair it with "Approver for" below.email field (shared inbox) if set, else all individual users holding the role. Example: a manager role with "Approver for" = ['junior-buyer'] approves every junior buyer's expense order.All three respect the built-in role lockdown — only custom roles can toggle them. SSO providers that map IdP group claims to role slugs will auto-grant these capabilities through normal role assignment, which means the entire approval topology is configured once on the Role records and SSO-assigned users inherit automatically.