Skip to content

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:

RoleDescriptionManager AccessEditor
visitorNot signed in. Can only view published content. Default permissions are tunable (e.g. allow visitors to submit contact forms or subscribe).NonePermissions field editable; other fields locked.
userSigned in. Can manage their own profile, view their own orders, and view their own carts. Self-only scoping is enforced in code.NoneBanner only (locked). Extend via custom roles.
adminGrants 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 rolesBanner only (locked). Extend via custom roles.
superFull access to all tables and actions. Hardcoded — cannot be revoked or narrowed via the UI.FullBanner only (locked).

Permissions

Base permissions by role. Admins can gain additional permissions through custom roles assigned by a Super admin:

ActionVisitorUserAdmin (base)Super
Read public recordsYesYesYesYes
Read own user data-YesYesYes
Update own profile-YesYesYes
CRUD data tables--Via custom rolesYes
Manage users--Via custom rolesYes
Assign roles--Via custom roles (cannot assign super/admin)Yes
Delete records---Yes
Edit site config---Yes
Delete audit records---No (append-only)
The 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.canExpense flag. 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"].
Non-super users can never assign the 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).
Custom Roles is a paid feature on your plan. Once any custom role exists, the toggle in Site Config can't be turned off until those roles are deleted or replaced. The Site Config form surfaces the count and a cleanup link when this happens. While the feature is off, creating, editing, and deleting custom roles is blocked for everyone except Super admins (the four built-in roles stay editable as always).

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 caseCustom roleTableFilter
Author-only article editingContributorarticleauthoredBy equals ${user.handle}
Sales rep order ownershipSales ReporderassignedRepId equals ${user.id}
Regional content scopingEditor (West Coast)articleregion equals ${user.region}
Vendor portal (multi-supplier)VendorproductsupplierId equals ${user.supplierId}
Helpdesk ticket assignmentSupport RepcontactassignedTo equals ${user.id}
The same machinery powers the built-in User role's order/cart scoping (e.g. 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:

  1. Admin enters the new user's name and email.
  2. The system creates the account with pending status and generates a secure onboarding token.
  3. A welcome email is sent with the subject "Welcome to [Site Name] - Set up your account".
  4. The email contains a link to the onboarding page where the user sets their password.
  5. 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).
  6. Once the user completes setup, their status changes to active and they can sign in normally.
The welcome email uses the site's branding (logo, colors, fonts) configured in Site Config. It is sent from the no-reply email address with the support email as reply-to.

User Status

Every user account has a status that controls their ability to sign in and access the platform:

StatusCan Sign InDescription
activeYesNormal operating state. Full access based on assigned roles.
pendingNoAccount created via admin onboarding but user has not completed setup.
suspendedNoTemporarily blocked by an admin. User cannot sign in until reinstated.
inactiveNoDeactivated 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 super or admin roles, 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 @handle when blank.
  • Location (publicLocation) - Optional public location string.
  • Bio (publicBio) - Optional public bio.
  • Socials (publicSocials) - List of public social media URLs.
The data layer treats public fields as a strict whitelist: 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.

Record filters always apply — they are core security that scopes which rows a user can read or modify. Hidden fields are part of the separate Advanced Permissions paid feature: when that toggle is off in Site Config, field redaction is skipped and full records are returned (the row-level filters still apply). Turn on Advanced Permissions to enforce per-role field hiding.

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.

Need a User or Admin to do more? Don't try to edit the built-in role — create a custom role with the desired table permissions and assign it to the user alongside their built-in role. Permissions stack additively.

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/approvals and 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 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.

Self-approval is blocked at the API level. A user who holds a role that's an Approver for one of their other roles still cannot approve their own orders — the order skips them and routes to the next eligible approver.