Module Access
A module access key is a permission for a feature, not for data.
Collection permissions answer “which rows and columns may this user touch?” A key answers a different question: “may this user use this feature at all?”
Keys have names like system:logs (open the Logs page) or reports:export
(export a report). You grant a key on a policy. If a user has several policies,
they get every key that any one of those policies grants.
This page is about using keys in your own code. To create keys and grant them to people, see Roles & Policies.
Reading a user’s keys
Call GET /api/permissions/me. The response has a moduleAccess object
listing the keys that user holds:
{
"data": { "...": "collection permissions" },
"moduleAccess": { "system:logs": true, "reports:export": true },
"meta": { "resource_uri": "/tenant:acme" }
}If you use scopes, the keys you get back are the ones that apply to the scope
you asked for. Set the scope with the X-Resource-Uri header (or the
daas_resource_uri cookie). The scope that was used comes back as
meta.resource_uri.
In a React component, don’t fetch this yourself — read it from the permissions context:
const { hasModuleAccess } = usePermissions();
if (hasModuleAccess('reports:export')) {
// show the export button
}Checking a key on the server
The check above only decides what the user sees. It runs in the browser, so it cannot stop a request. Hiding a button does not protect the thing behind it — someone can still call your API directly. Always check the key again on the server.
This matters more for keys than for collection permissions. Collection permissions have a safety net: if the app layer forgets to check, the database checks again (Row Level Security). Keys have no safety net. No table sits behind a key, so the database has nothing to re-check. Your server-side check is the only thing standing there.
Use lib/module-access/enforce.ts in API route handlers and Server Components:
import { enforceModuleAccess, hasModuleAccess, getModuleAccess } from '@/lib/module-access/enforce';
// Stops the request: throws ModuleAccessError (status 403) if the key is missing
await enforceModuleAccess('reports:export');
// Just asks — returns true or false, throws nothing
if (await hasModuleAccess('reports:export')) { /* ... */ }
// Everything at once
const { isAdmin, moduleAccess } = await getModuleAccess();A typical route:
// app/api/reports/export/route.ts
export async function GET() {
await enforceModuleAccess('reports:export');
// ...
}How the check behaves
A list of keys means “any one of these.”
enforceModuleAccess(['reports:export', 'reports:admin']) passes if the user
has either one. This matches how workflow commands treat keys.
Admins pass every check. Be careful here: getModuleAccess() returns
{ isAdmin: true, moduleAccess: {} } for an admin — the key list comes back
empty, because an admin holds all keys rather than a specific set. So check
isAdmin first. If you only look at the list, you will lock admins out of
everything.
If anything goes wrong, access is denied. No one logged in, a failed lookup, an unexpected error — all of them mean “no access”. The check never lets someone through because something broke.
Features and data are two separate checks. One asks may they use this feature, the other asks may they touch these rows. If your route does both, call both:
await enforceModuleAccess('reports:export');
await enforcePermission({ collection: 'reports', action: 'read' });Two things this check does not do
It needs a direct database connection. If your app talks to a DaaS
instance somewhere else instead of its own database, use the moduleAccess
values from /api/permissions/me instead of this helper.
It ignores scopes. Unlike /api/permissions/me, this check looks at all
of the user’s policies at once, whatever scope they belong to. So a key
granted in one tenant will pass the check in every tenant. If a feature must
be limited to one tenant, also check a collection permission, which is
scope-aware.
Using a key in a workflow
A workflow command can list keys in module_access_keys, next to its
policies list. The user needs either one — a matching key or a matching
policy:
{ "module_access_keys": ["workflow:approve"] }See Workflows for the full command format.