04 β€” Feature Isolation: Licensing & RBAC

← Index

Beyond compute and data isolation (02, 03), tenants are isolated at the feature level: which modules a tenant may use, and which a given user may touch. This is enforced by two orthogonal gates β€” the license (tenant-level) and RBAC permissions (principal-level) β€” both of which must pass on every request.

1. The license model

Defined in src/core-api/src/types/license.ts.

Authoring & storage (control plane)

During provisioning (TenantProvisioningService, :134-135):

Consumption (data plane)

src/tenant-api/src/services/license/TenantLicenseService.ts:

⚠️ Two behaviours to know. (1) License expiry is not enforced β€” the valid_until check is commented out (:259-265). (2) The check fails open: on any error it returns allowed:true (:298-305), and licenseMiddleware does the same. So a core-api outage degrades to permissive access, not denial.

2. Module/base gating on the API

The middleware that enforces the license is wired per route in src/tenant-api/src/routes/index.ts. A representative route:

router.get('/member-requests',
  tenantMiddleware,                                   // resolve req.tenantId
  authenticateJWT,                                    // identity + req.user.permissions
  requireModule('members:requests'),                  // LICENSE gate
  requirePermission(MEMBER_REQUESTS_PERMISSIONS.MEMBERS_REQUESTS_READ),  // RBAC gate
  memberRequestController.list);

The license gates come from licenseMiddleware.ts:

The API catalog and base filtering

src/tenant-api/src/docs/api-catalog.generated.ts is a generated manifest of all routes (β‰ˆ1657), each annotated with modules, anyModules, permissions, requiredBases, anyGroups, auth, tenant, core. The catalog does not enforce anything at request time β€” it's metadata. Its requiredBases / anyGroups are used by apiDocs.ts to filter the published OpenAPI surface to the bases the deployment/vertical supports (isShown, :50-56), independent of any one tenant's license. So there are two filters with different scopes: per-tenant license gating (runtime) and per-vertical base filtering (docs/surface).

A separate gate, functionAuthMiddleware.ts (requireFunction), governs public/unauthenticated endpoints by checking the function id against a domain's unauthenticatedFunctions allowlist (and validating the domain belongs to the tenant β€” anti-spoofing).

3. Exposable modules

"Exposable modules" govern what a root/reseller tenant (one with can_create_tenants) may grant to the child tenants it provisions, and what each tenant surfaces to its portals.

Portal reflection

The runtime enabled_modules array reaches the portal on the authenticated user object. A portal ModuleService.isModuleEnabled(name) does prefix-aware matching β€” enabling a base (e.g. members) implicitly enables its sub-modules (members:ranks) β€” with a set of always-available CORE_MODULES. The portal shows/hides navigation items and route guards off this, so a tenant only sees the features its license grants. The tenant-api applies the identical prefix rule server-side (services/tenant-setup/moduleScope.ts).

4. Permissions / RBAC

The full gate, end to end

core-api:  createTenant β†’ resolveEnabledModules (∩ DEPLOYMENT_AVAILABLE_MODULES, βˆͺ users/roles/settings)
                        β†’ createLicense β†’ encrypt β†’ store (Tenant.encrypted_license)
                                   β”‚
                  GET /v1/license/validate/{tenant}   (or DB-decrypt fallback)
                                   β–Ό
tenant-api: TenantLicenseService cache (1h) β†’ checkModuleAccess(enabled_modules)
            per request:  tenantMiddleware β†’ authenticateJWT
                          β†’ requireModule / requireAnyModule        (LICENSE gate)
                          β†’ requirePermission                        (RBAC gate)
                          β†’ controller
                                   β”‚ enabled_modules also embedded in user.license on the JWT
                                   β–Ό
portal:     ModuleService.isModuleEnabled (prefix-aware) β†’ show/hide nav & guard routes
docs:       apiDocs.isShown filters the OpenAPI surface by requiredBases/anyGroups vs the vertical's bases

Key source paths: src/core-api/src/types/license.ts, src/core-api/src/services/tenant/TenantProvisioningService.ts (:983-1099), src/core-api/src/constants/available-modules.ts, src/tenant-api/src/services/license/TenantLicenseService.ts, src/tenant-api/src/middleware/{licenseMiddleware,authMiddleware,functionAuthMiddleware}.ts, src/tenant-api/src/constants/permissions.ts, src/tenant-api/src/docs/{api-catalog.generated.ts,apiDocs.ts}, src/tenant-api/src/routes/index.ts.

Next: 05 β€” Billing & financials β†’