Skip to main content

RBAC Overview

Open Mercato enforces access control through a two-layer RBAC system: role assignments scoped to organizations/tenants and feature flags that gate specific capabilities. Modules describe the features they expose, and runtime metadata or services evaluate whether a request is allowed.

  • Roles bundle feature strings. Users inherit role features per organization/tenant.
  • User overrides (per-tenant) add or revoke individual feature strings without changing roles.
  • Super admin flag grants all features globally.
  • Organization visibility list optionally narrows which organizations a user can act on, even if the feature check passes.

Declaring Features in Modules

Every module must list the features it exposes in src/modules/<module>/acl.ts (or the package equivalent). Export a features array with strings following the <module>.<action> convention:

export const features = [
'auth.users.view',
'auth.users.create',
'auth.users.edit',
'auth.users.delete',
];

The generators collect these declarations into modules.generated.ts, making them available to admin UIs and DI services. Avoid introducing features dynamically at runtime—keep the list static so tooling can reason about permissions.

Using Feature Guards in Metadata

Pages, layouts, and API handlers can declare access requirements through colocated metadata files (page.meta.ts, <route>.meta.ts, etc.) or by exporting metadata directly:

import type { PageMetadata } from '@open-mercato/shared/modules/registry';

export const metadata: PageMetadata = {
requireAuth: true,
requireRoles: ['admin'],
requireFeatures: ['auth.users.edit'],
};
  • requireAuth blocks anonymous access.
  • requireRoles checks membership in any of the listed roles within the organization.
  • requireFeatures ensures the caller has every listed feature through roles or user-level grants.

Metadata-driven enforcement keeps module pages independent and discoverable by the auto-generated registry.

Server-Side Checks in Handlers

When you need conditional logic beyond declarative metadata—such as enabling certain operations on a page—you can use the DI-provided RBAC service. Inject it through handlers or controllers via Awilix:

const canEdit = await rbacService.userHasAllFeatures(userId, ['auth.users.edit'], {
tenantId,
organizationId,
});
  • Always pass tenantId and organizationId to respect multi-tenant boundaries.
  • Use userHasAllFeatures for strict gates. Prefer userHasAnyFeature (if exposed) for "at least one" checks.
  • Combine RBAC checks with domain validation; denying access early avoids unnecessary database work.

Designing Features for Modules

Keep module features granular enough to capture real application actions:

  • View / edit / create / delete patterns work for most CRUD screens.
  • Add specific features for sensitive workflows (billing.invoices.refund) instead of overloading broad ones.
  • If a module exports backend jobs or CLI commands, guard their execution with features to ensure parity with UI access.

When extending another module, declare additional features in your module's acl.ts. Features remain scoped to the defining module even if multiple modules contribute to the same UI flow.

Testing RBAC Behavior

Include feature-aware tests alongside APIs or pages:

  • Seed or mock users with explicit features/roles.
  • Verify that protected handlers return the expected authorization errors when features are missing.
  • Confirm that super admin and feature overrides behave as intended.

Because RBAC relies on generated module metadata, run yarn modules:prepare whenever you add or remove features before executing tests or type checks.

Managing ACLs in the admin

Editing role features

Roles aggregate feature flags. Administrators assign features per role in the Roles screen, then apply roles to many users at once.

Editing user ACL overrides

User-level overrides let you fine-tune access for individuals—grant additional features or revoke ones inherited from roles without creating more roles.