Onboarding and offboarding an admin
Three role recipes built from real permission keys, the order to create and assign a role in, how long a change takes to bite, and the six-step offboarding that actually closes every door.
Roles and permissions explains the model — how a key is built, where it is enforced, what Super Admin is. This page is the task: which keys to actually tick for a support agent, a finance approver and a compliance reviewer, and how to take them all away again when the person leaves.
Two facts shape everything below.
Every account has exactly one role. There is no per-user override, no second role and no inheritance. Two people who need different access need two different roles.
access.admin is not a formality. The URL map covers 245 admin paths
explicitly — a count that grows with almost every release; anything under
/admin that is not listed falls back to access.admin alone. That bucket is
not small — it includes Trading Settings, the page builder, individual user
profile pages, ecommerce settings and the affiliate Conditions screen, which
is where commission rates are set. The
data on those pages is still fetched through the API and still refused without
the right view.* key, so an under-privileged operator sees an empty shell
rather than real records — but treat access.admin as "may walk the whole admin
area", not as "may see the dashboard".
Three recipes
Keys sort by verb, not by area, in the permission picker. So you will tick
the access. block, then the view. block, then the edit. block — not one
area at a time. Plan for that or you will miss half of a pairing.
Almost every admin screen is a data table, and it asks two questions before it
fetches anything: does this role hold the screen's access key, and does it hold
its view key. Grant access.user without view.user and the operator gets the
Users page, the header, the filters and an empty table — forever, with no error
and nothing in the console. This is the commonest support case on the whole
permission system.
Support agent
Answers tickets. Can look a customer up to understand the ticket. Cannot touch their money.
| Key | What it unlocks |
|---|---|
access.admin |
The admin area and its dashboard |
access.support.ticket |
The support console at /admin/crm/support and the archive at /admin/crm/support/tickets |
view.support.ticket |
The tickets themselves, and the per-ticket page |
edit.support.ticket |
Reply, assign, set priority, close and reopen |
access.user + view.user |
Open a customer's record to see who is asking |
access.kyc.application + view.kyc.application |
Read where a verification got to, so "why is my account limited" can be answered |
Deliberately absent, and each for a specific reason:
edit.user— that one key also covers blocking accounts and resetting two-factor. An agent who can reset a customer's 2FA can take over the account.edit.wallet— adjusting a customer balance up or down.edit.withdraw/edit.deposit— deciding the money queues.edit.kyc.application— approving identity verification.create.api.key— see the warning at the bottom of this page.delete.support.ticket— closing a ticket is the reversible action; deleting it destroys the conversation.
Finance approver
Works the deposit and withdrawal queues. Reads the ledger. Does not configure the platform.
| Key | What it unlocks |
|---|---|
access.admin |
The admin area |
access.withdraw + view.withdraw + edit.withdraw |
The withdrawal queue and the approve/reject decision — the key that moves money out of the platform |
access.deposit + view.deposit + edit.deposit |
The deposit queue and its decision |
access.transaction + view.transaction |
The full ledger, read-only |
access.wallet + view.wallet |
Customer wallets, read-only |
access.user + view.user |
Identify the customer behind a transaction |
access.admin.audit |
The audit trail, so they can see their own decisions and yours |
Add edit.wallet only if this person is allowed to adjust balances by hand
and to freeze a wallet. It is a separate decision from deciding the queues; see
Customer wallets and balance adjustments.
Deliberately absent:
edit.settings— the fee, withdrawal-approval and two-factor policy that govern the queue they work. An approver should not be able to switch off the approval requirement they exist to satisfy.delete.withdraw/delete.deposit/delete.transaction— deleting a queue row destroys the evidence of the decision.create.api.key.
Compliance reviewer
Decides identity verification and country policy. Reads the audit trail. Does not approve payouts.
| Key | What it unlocks |
|---|---|
access.admin |
The admin area |
access.kyc.application + view.kyc.application + edit.kyc.application |
The application queue and the approve / reject / request-more-info decision |
access.kyc.level + view.kyc.level |
The verification levels and their required fields, read-only |
access.user + view.user |
The account behind an application |
access.geo.restriction + view.geo.restriction |
Geo restrictions and the geo policy screen |
access.geo.restriction.log |
The geo access log and its CSV export — the evidence trail for a refused visitor |
access.admin.audit |
The audit trail |
Add edit.geo.restriction (and create.geo.restriction / delete.geo.restriction)
only if this person owns country policy rather than reviewing it. Add
edit.user only if they are the person who blocks accounts — remembering that
the same key resets two-factor.
Geo-restriction settings are refused by the generic settings endpoint even for a
edit.settings holder. They are writable only from Geo Restrictions →
Policy, behind edit.geo.restriction. So a compliance reviewer with geo keys
does not need edit.settings, and giving them edit.settings would not get
them geo policy anyway.
Onboarding, in order
-
Sign in as a Super Admin. Nobody else can create, rename or delete a role, whatever keys they hold — the route checks the caller's role name, not their
create.rolegrant. A non-Super-Admin getsForbidden - Only Super Admins can create new roles. -
Create the role at
/admin/crm/role. Users → Roles & Permissions → User Roles. Name it for the job, not the person. -
Tick the keys, verb block by verb block. The Permissions field is a multi-select over every seeded key, sorted alphabetically — which means by verb. Work down the
access.block first, thenview., thenedit., using the recipe above as your checklist rather than trying to find one area's keys together. -
Save. The permission set is replaced wholesale by what is in the field: anything unticked is removed. The save writes an
ADMIN_CRMrow to the audit trail with the title Create role or Update role, so you can prove afterwards who granted what. -
Assign the role on
/admin/crm/user. Edit the account and pick the role. Only a Super Admin may change an account's role, and the form makes them re-enter their own password first — a hijacked admin session must not be able to promote a second account quietly. -
Have them sign out and back in. The role change revokes every session the target account holds, so they will be signed out anyway. A fresh sign-in is the cleanest way for them to pick up the new menu.
How long the change takes to bite
There are three copies of "which role may do what", and they do not converge at the same speed.
| Where | When it sees your change |
|---|---|
| The API, on the worker thread that served the save | Immediately — the save reloads the in-memory role map on that thread |
| The API, on every other worker thread | Within 60 seconds, on each thread's own refresh timer |
| The page gate and the admin menu (Next.js middleware) | Within 5 minutes — it caches the role list for that long |
Sixty seconds is fine for a grant. It is not fine when you are taking access away from somebody you no longer trust, and the five-minute front-end cache means the menu and the page gate lag further still. If the revocation matters right now, restart the API process and remove the doubt:
pm2 restart backendpm2 restart backend is enough on its own. The cron process serves no traffic
and the front end needs no restart. Full detail in
Processes and ports.
Verify the grant landed
Do not trust the success toast. Check two things:
/admin/system/audit, filtered to ModuleADMIN_CRM. Your save is there as Create role or Update role, with your name on it. If it is not, the save did not happen.- Have the person open one screen from each block you granted and confirm
the table has rows in it. An empty table with no error is the
access-without-viewsymptom described above, and it is silent from every other angle.
Two fences you will hit
Super Admin cannot be scoped, and cannot be handed out
Every gate compares the role's name against the literal string
Super Admin and returns true before permissions are consulted. There is no way
to narrow it.
The role is also fenced off from the screens: it is excluded from the Roles list so it cannot be edited or renamed there, it cannot be deleted, its permission set cannot be synced through the API, and it is excluded from the role picker on the Users screen — so no account can be promoted into it from the panel at all. Super Admin rows are hidden from the user list for everyone below Super Admin.
Give it to people, not to service accounts, and not to an integration.
39 settings keys refuse anyone but a Super Admin
A separate fence sits behind edit.settings. These keys reject a save from any
role but Super Admin, even one holding edit.settings, and the refusal is a 403
for the whole save — though unchanged keys are dropped before the check
runs, so the rest of a page still saves normally. Key matching is
case-insensitive, so renaming a key does not get around it.
| Group | Keys |
|---|---|
| Withdrawal policy | withdrawAutoApprove (and withdrawApproval, its legacy twin — same guard, written automatically when the first is saved) |
| Withdrawal two-factor | withdrawTwoFactorRequired, withdrawTwoFactorChallenge, withdrawTwoFactorAppAllowed, withdrawTwoFactorEmailAllowed, withdrawTwoFactorSmsAllowed |
| P2P two-factor | p2pTwoFactorRequired, p2pTwoFactorChallenge |
| Transfer money | walletTransferFee, walletTransferFeePercentage, walletTransferSpread |
| TransFi ramps and custody | transfiOnrampEnabled, transfiOfframpEnabled, transfiOnrampCustody, transfiOfframpCustody, transfiIbanEnabled |
| Trading bots | tradingBotEnabled, tradingBotLiveTradingEnabled, tradingBotPlatformFeePercent, tradingBotTradingFeePercent |
| AI Support | aiSupportEnabled, aiSupportAutonomy, aiSupportProvider, aiSupportDailyBudgetUsd, aiSupportMonthlyBudgetUsd, aiSupportAccountToolsEnabled, aiSupportOperationsEnabled, aiSupportWorkflowsEnabled, aiSupportWorkflowKeys |
| DEX / Swap | dexEnabled, dexAllowlistMode, dexKycRequired, dexDirectPoolsEnabled, dexPoolRiskAckRequired |
| FX routing | fxTradingExternalRoutingEnabled |
| KYC | kycFeatureEnforcement |
| Demo mode | DEMO_STATUS, NEXT_PUBLIC_DEMO_STATUS |
So "the save says Forbidden" is expected behaviour for these, not a bug. When a delegated admin reports it, the answer is that a Super Admin has to make that particular change — not that their role is broken.
The two demo-mode keys are on the list but govern nothing: demo mode is read
from the environment (NEXT_PUBLIC_DEMO_STATUS in .env) and no reader
consults a settings row. Do not go looking for the screen that edits them.
Offboarding
This is the half nobody writes down, and the order matters — each step is harder once the one before it has been done.
Once you have blocked the account or emptied the role, their assigned tickets are still assigned to them, and the queue now shows an owner who can never answer. The archive screen does not offer a per-agent filter, so tracking them down afterwards is manual work you can avoid by doing it while you still know what to look for.
-
Reassign their open support tickets.
/admin/crm/support/tickets. The Assigned Agent column is on every row. Select the departing agent's tickets and use Unassign selected from the bulk menu — that returns them toPENDINGand back into the unassigned pool, where the rest of the desk picks them up. (The other bulk option is Assign to me, so the colleague taking the work over can claim them directly.)CLOSEDtickets are left alone by design; re-assigning a resolved ticket would drag it back into the open queue. -
Take the role away.
/admin/crm/user, edit the account, and move them to a role with no permissions — the seeded User role is exactly that, and it ships empty. Do not leave them on an admin role "until the handover is finished".Changing the role revokes every session that account holds, immediately. They are signed out of every browser and every device on the spot.
-
Revoke their API keys.
/admin/api/key. A key acts as its owner and does not care what role the owner is on now, so a key minted while they were an admin keeps working after the role is gone. Delete every key issued to their account — and if they heldcreate.api.key, check for keys they issued against other accounts too. -
Set the account status.
/admin/crm/user, the row's status control, or the Block User action if you want a recorded reason and an optional duration.BANNEDandSUSPENDEDboth revoke sessions; a block requires a reason, which is what lands on the audit trail and in their block history. You cannot change your own status, and a bulk status change that includes your own account is refused outright. -
Restart the backend if this is a for-cause departure. Otherwise the worker threads that did not serve your save keep enforcing the old permissions for up to a minute, and the page gate for up to five. See the warning above.
-
Read the trail.
/admin/system/audit, filtered to ModuleADMIN_CRM, confirms the role change and the block are both recorded with your name on them. Then filter the Admin column to the departing account and read the last few weeks of what they did — this is the moment to do it, while you still have the context.
/admin/api/key creates a key against a chosen userId, with whatever scopes
the form was given. A key acts as its owner, so an operator holding
create.api.key can mint working credentials for any account on the
platform, including scopes that move money — and those credentials survive the
operator's own offboarding until somebody deletes them. Grant it only to roles
you would trust with the accounts themselves, and audit it at step 3 above.
Related
- Roles and permissions — the model, the key syntax, and what an update does to your grants.
- The admin audit trail — reading
ADMIN_CRMrows, and theForbiddenrun that means a role is wrong. - The user desk — the Users screen in full.
- Processes and ports — what
pm2 restart backendactually restarts.