Command access
Every command has a Permission Level (Everyone, Administrators, Moderators, Server Managers, or custom permissions). Under it, in the same Permissions section of the command's page, are role rules:
- Also allow these roles — members with any of these roles can run the command even without the permission level.
- Never allow these roles — members with any of these roles cannot run it, whatever else they have.
The rule, in order:
- A server administrator can always run it.
- A member with a denied role cannot. (A role in both lists is denied.)
- A member with an allowed role can.
- Otherwise the permission level decides.
So "Moderators, plus the Helper role, but never Muted" is: level Moderators, allow
Helper, deny Muted. It works on custom commands, and on built-ins like /timebox
and /quorum through their per-server permission override.
How it is enforced
One check, memberMayUseCommand in src/lib/discord/command-access.ts, runs at the
top of command dispatch in POST /api/discord/interactions, before anything
runs, for custom, built-in and integration commands alike. It used to run only for
custom commands, so a built-in handled early (/quorum, /timebox, /room) relied
on Discord's own advisory gate alone. /help uses the same function to list only
what a member can run.
Discord hides a command from members who lack its permission level, and an allowed role that lacks it could not see the command. So when a command has allowed roles and a permission level, it is registered without the permission gate and this check is the only gate. Denied roles do not change registration.
Where it is stored
access_rules, a JSON column on commands and on built_in_command_overrides
(migration 0073): {"allow_roles": [...], "deny_roles": [...]}, empty when unset.
The next rules (specific members, channels, categories, time windows) are more keys
in the same object, not more migrations.
Not yet: the command export and import files do not carry role rules, since role ids belong to one server.