Users & Roles
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.
User Roles
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). |
Permissions
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 |
| Delete audit records | - | - | - | No (append-only) |
admin label is a dashboard key, not a permission — and that includes the admin "convenience" actions outside the data tables. Looking up another customer's orders requires an ORDER read grant; resending an order email requires ORDER update; resending a welcome email, re-issuing an onboarding token, or resetting a user's 2FA requires a cross-user USER update grant; and running low-stock checks or inventory alerts requires PRODUCT update. An admin without a custom role granting the matching table permission gets a 403.Custom Roles
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:
- Blogger - Manages articles and assets. Reads categories and subscribers (to see who'd get notified on publish). Gets BCC'd on every article create/update.
- Shop Manager - Runs the storefront: products, categories, coupons, and the order admin (read + fulfillment updates — refunds remain super-only).
- Comms Director - Owns inbound and outbound communication: subscribers, contacts, homepage slides, and feedback. Email notifications on every change.
- MOAS Buyer - Procurement role that opts into pay-by-invoice via the
moas.canExpenseflag. Pairs with a MOAS Manager who approves the resulting expense orders. - MOAS Manager - Approves expense orders placed by MOAS Buyers.
moas.canApprove+approverFor: ["moas-buyer"].
super or admin roles, and no user can modify their own roles. User deletion remains super-only. A non-super who is allowed to create or edit custom roles also can't use one to escalate: a custom role may not grant any table/action the role's author doesn't already hold themselves (Super admins define the ceiling and are exempt).Per-user record filters
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- Any other top-level field on the User record (e.g.
${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.
Creating Users & Onboarding
Users can be created in two ways: self-registration (if sign-up is enabled in Site Config) or admin onboarding (an authorized admin creates the account manually).
Self-Registration
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.
Admin Onboarding
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:
- Admin enters the new user's name and email.
- The system creates the account with
pendingstatus and generates a secure onboarding token. - A welcome email is sent with the subject "Welcome to [Site Name] - Set up your account".
- The email contains a link to the onboarding page where the user sets their password.
- The onboarding token expires after 7 days. If it expires, an admin can issue a new one — re-issuing a token counts as user management, so it requires the same USER UPDATE grant (a baseline admin without one gets a 403).
- Once the user completes setup, their status changes to
activeand they can sign in normally.
User Status
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. |
Revoking Access
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:
- Immediate prevention of new sign-ins - The user cannot authenticate.
- Active session termination - On the next session refresh, the system re-checks the user's status from the database. If the status is no longer
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.
Sessions
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:
- Session duration - Sessions expire after 30 minutes of inactivity. Active usage refreshes the token automatically.
- Token refresh - On each refresh, the system re-fetches the user's current data from the database, ensuring role changes and status updates take effect without requiring a manual sign-out.
- Session payload - Includes user ID, name, email, roles, status, theme preference, avatar, and 2FA status.
- Secure cookies - Session tokens are stored in HTTP-only cookies with a configurable prefix.
Managing Users
From the Users table, authorized users can:
- View all users - See account details, roles, status, and activity. (Requires custom role with USER READ_MANY permission)
- Create users - Onboard new users with name, email, and role assignment. (Requires custom role with USER CREATE permission)
- Assign roles - Edit role assignments on other users. (Requires custom role with USER UPDATE permission.) Non-super users cannot assign the
superoradminroles, and nobody can modify their own roles. - Update profiles - Edit user details like name, email, or account balance. (Requires custom role with USER UPDATE permission)
- Change status - Suspend, deactivate, or reactivate accounts. (Super only)
- Delete users - Remove user accounts. (Super only)
User Page
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).
Account section
- Full name - Legal/full name. Used for invoices and admin records — not shown publicly.
- Email - Sign-in email. Email changes may require re-authentication.
- Phone - Optional contact number.
- Shipping & billing addresses - Pre-filled during checkout.
- Theme preference - Light, Dark, or System (follows the device's
prefers-color-scheme). Rendered as a 3-segment switcher next to the page heading. New users default to System.
Profile section (public)
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.
- Handle - The URL slug for the public profile. Optional — if left blank, the server defaults it to the user's internal ID. The form offers a Sparkles button (slugify from full name) and a Dice button (roll a silly random name).
- Publish profile - Toggle that controls whether the profile is live at the handle URL.
- Avatar - Upload a profile image (max 1 MB, 1:1 aspect ratio).
- Display name (
publicName) - The name shown publicly. Falls back to@handlewhen blank. - Location (
publicLocation) - Optional public location string. - Bio (
publicBio) - Optional public bio. - Socials (
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.System fields (admin-only)
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.
Two-Factor Authentication
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.
- Setup - User scans a QR code with their authenticator app and confirms with a verification code.
- Backup codes - Generated during setup for account recovery if the authenticator is lost.
- Admin visibility - Users with appropriate permissions can see whether a user has 2FA enabled but cannot configure it on their behalf.
- Admin reset - If a user loses access to their authenticator and backup codes, an admin with a user-management grant (Super, or a custom role granting USER UPDATE) can reset their 2FA settings, allowing the user to set it up again. The baseline admin label alone is denied, and only a Super can reset another Super's 2FA.
Roles Table
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.
Built-in role lockdown
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.
B2B role permissions: Can expense / Can approve / Approver for
In addition to the table-level permission array, each role has three top-level fields that power the B2B procurement features:
- Can expense orders — users in this role see a "Pay by Invoice" payment option at checkout (skips Stripe; invoice is emailed). Also requires Site Config > Checkout > Allow Pay-by-Invoice to be enabled at the site level. (The field is named "Can expense orders" internally — Pay by Invoice and Expense Order refer to the same flow.)
- Can approve orders — users in this role can see
/dashboard/approvalsand act on its queue. Doesn't by itself say WHICH orders they approve — pair it with "Approver for" below. - Approver for — the list of buyer-role slugs this role approves expense orders for. When a buyer with role X places an expense order, every role whose "Approver for" includes X gets the approval-request email. Notification target is the role's
emailfield (shared inbox) if set, else all individual users holding the role. Example: amanagerrole 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.
