Skip to content

Access Control Integration & Enforment Contract

This document is FINAL and ENFORCEABLE.

All backends must implement access control exactly as defined here.

Any deviation must be explicitly approved and documented.


This document defines the final integration and enforcement contract between:

  • Auth Backend
  • Platform Core Backend
  • Base Backend
  • Module Backends
  • Frontend

It exists to remove ambiguity before implementation.

This document does not replace:

  • architecture/documentation.md
  • 2.1.-Backend-Auth.md
  • 2.2.-Backend-Core.md
  • 2.2.1.-Backend-Core-API.md
  • 2.3.-Backend-Base.md

It connects them.


The platform access rule is:

effective_access = company_entitlements ∩ membership_grants

Where:

  • company_entitlements = company commercial ownership from Platform Core
  • membership_grants = user/company access assignments from Auth
  • effective_access = final merged access result computed by Auth
  • enforcement = done by Base / Module Backends

Auth is the source of truth for:

  • users
  • identity
  • login
  • JWT issuance
  • sessions
  • refresh token lifecycle
  • company memberships
  • tenant roles
  • module grants per membership
  • permissions per membership
  • delegation rules
  • invitations
  • effective access computation

Platform Core is the source of truth for:

  • packages
  • add-ons
  • subscriptions
  • module catalog
  • package → module mapping
  • add-on → module mapping
  • company entitlements
  • company enabled modules
  • entitlement versioning

Base backend owns:

  • business logic
  • business routes
  • business data reads/writes
  • enforcement of effective access returned by Auth

Base backend does not own:

  • identity
  • company entitlements
  • membership truth
  • permissions truth
  • package/subscription truth
  • access computation

Each module backend owns its own business data and business execution.

Examples:

  • Finance backend → finance business records
  • Market backend → market business records
  • Venue backend → venue business records
  • Touring backend → touring business records
  • AI backend → AI outputs and AI business flows

Module backends do not own:

  • user identity
  • package truth
  • entitlement truth
  • final access computation

Frontend owns:

  • UX decisions
  • module visibility
  • route visibility
  • local state for rendering

Frontend does not own:

  • final access truth
  • security enforcement
  • package truth
  • permission truth

Frontend is never the source of truth.


This platform uses a 3-layer access model.

Question answered:

Did the company buy this module?

Owned by:

  • Platform Core

Examples:

  • company has Basic
  • company has Finance
  • company has Market
  • company does not have AI

If the company did not buy a module, no user in that company may use it.

This is the upper bound.


Question answered:

Inside the modules the company owns, which modules can this membership access?

Owned by:

  • Auth

Examples:

  • company bought Basic + Finance + Market
  • membership granted only Finance

Then that membership only gets Finance.


Question answered:

Inside a module the user can access, what exactly can the user do?

Owned by:

  • Auth

Examples:

  • basic.event.view
  • basic.event.create
  • finance.expense.view
  • finance.expense.edit
  • market.contract.approve

Permissions are valid only if the parent module is valid in effective access.


These concepts must never be confused.

Role defines:

  • administrative authority
  • management scope
  • delegation authority

Examples:

  • TENANT_SUPERADMIN
  • ADMIN
  • MANAGER
  • USER
  • SUBMITTER

Role does not automatically define which modules the user can access.


Module grant defines:

  • product access inside one company

Examples:

  • Basic only
  • Finance only
  • Basic + Market
  • Finance + Market

A user can be USER and still have Finance only. A different USER can have Basic + Market.

That is valid.


Permission defines:

  • what a user can do inside an enabled module

Examples:

  • view
  • create
  • edit
  • delete
  • approve
  • export
  • manage

Role = authority
Module = product access
Permission = action access

These are separate layers.


Tenant roles define what a user can grant to others.

  • has access to all modules the company owns
  • may assign module access to others
  • may assign permissions to others
  • may define delegation boundaries for lower roles
  • may buy add-ons / manage subscriptions

Still limited by company entitlements.

If company did not buy AI, even TENANT_SUPERADMIN cannot use or grant AI.


  • operational company administrator
  • may manage users
  • may assign module access
  • may assign permissions
  • may not buy add-ons
  • may not grant beyond what Superadmin delegated

This must be enforced by delegation policy, not only by role name.


  • limited delegation role
  • may manage users below them only within delegated scope
  • may not buy add-ons
  • may not exceed what Admin/Superadmin delegated

  • no delegation by default
  • only receives granted modules and permissions
  • normal consuming user

A grant action is valid only if all are true:

  1. actor role allows that kind of grant
  2. actor delegation policy allows that target role / module / permission
  3. company owns the module
  4. actor is authorized in the active company
  5. actor is not exceeding delegated scope

Delegation belongs only to Auth.

Base and Core do not enforce delegation policy as a source of truth.


Base and module backends must resolve effective access from Auth.

Recommended contract:

GET /auth/me/access

This is the canonical access resolution endpoint.


Base / module backends must preserve user context and company context.

Authorization: Bearer <USER_ACCESS_TOKEN>
x-org: <COMPANY_ID>

Choose one:

X-Internal-API-Key: <SERVICE_KEY>

or mTLS / internal service token if your infra uses that.


The system must not leave ambiguity about this call.

At implementation time, the Auth contract must clearly define whether Base uses:

same GET /auth/me/access endpoint with:

  • user bearer token
  • x-org
  • internal service credential

dedicated internal route such as:

GET /internal/auth/access

with:

  • user bearer token
  • x-org
  • internal service credential

The contract must be explicit in OpenAPI / API docs before implementation.


Auth must:

  • validate user JWT
  • validate service credential
  • validate x-org
  • resolve membership
  • call Core for entitlements
  • merge effective access
  • return normalized access result

7. Required Auth effective access response

Section titled “7. Required Auth effective access response”

The exact full response may vary by product, but Auth must return enough information for enforcement.

Minimum recommended shape:

{
"userId": "uuid",
"companyId": "uuid",
"tokenVersion": 1,
"entitlementVersion": 7,
"tenantRole": "ADMIN",
"modules": ["basic", "finance"],
"permissions": [
"basic.event.view",
"finance.expense.view"
],
"delegation": {
"canManageUsers": true,
"canBuyAddons": false,
"grantableModules": ["basic"],
"grantablePermissions": [
"basic.event.view"
]
}
}

At minimum, Base/backend enforcement needs:

  • userId
  • companyId
  • tokenVersion
  • modules
  • permissions

Strongly recommended:

  • tenantRole
  • entitlementVersion
  • delegation

Auth must not compute effective access unless membership exists.

Membership is created through:

  • invitation acceptance
  • internal admin actions
  • company membership upserts in Auth

Without valid membership:

effective access must not be computed

Result should be deny / forbidden according to route context.


8. Base / module middleware flow (MANDATORY)

Section titled “8. Base / module middleware flow (MANDATORY)”

Every tenant-scoped business request must follow this flow.

Read:

  • Authorization
  • x-org

If route is tenant-scoped and x-org missing:

  • reject request

Validate:

  • signature
  • expiration
  • issuer
  • audience
  • token format

If invalid:

  • return 401

Base/module backends must not issue or refresh tokens.


Validate:

  • x-org is present
  • x-org is in valid canonical format
  • tenant route has explicit company context

If invalid/missing:

  • return 400

Base must not invent default company silently.


Call Auth.

If Auth resolution fails:

  • return 503

No local access guess is allowed.


Confirm required module exists in effective modules.

Examples:

  • basic
  • finance
  • market
  • touring
  • venue
  • ai

If missing:

  • return 403

Confirm required permission exists.

Examples:

  • basic.event.view
  • finance.expense.view
  • market.contract.approve

If missing:

  • return 403

Only after successful checks.


JWT validation
→ x-org validation
→ Auth effective access
→ module check
→ permission check
→ execute or deny

All endpoints in Base and module backends must follow this rule.

For every protected request:

  1. validate JWT
  2. validate x-org (tenant routes)
  3. resolve effective access from Auth
  4. verify module
  5. verify permission
  6. execute or deny

A request MUST be rejected if:

  • JWT is missing or invalid → 401
  • x-org is missing on tenant route → 400
  • user does not belong to company → 403
  • required module missing → 403
  • required permission missing → 403
  • effective access cannot be resolved → 503

This typically includes:

  • Auth service unavailable
  • timeout calling Auth
  • network failure between backend and Auth
  • Auth failing because Core is unavailable
  • corrupted upstream response during access resolution

In all such cases, fail closed.


Backends must NEVER:

  • allow access based on old package logic
  • allow access based on old subscription logic
  • allow access based on local permission tables as source of truth
  • allow access based on local role assumptions
  • infer access from JWT alone
  • trust frontend visibility as authorization

The platform must use a strict fail-closed model.

If any of the following cannot be verified:

  • JWT validity
  • company context
  • membership
  • effective access
  • permission presence

Then the request MUST be denied.

Under no circumstance should the backend:

  • guess access
  • fallback to optimistic assumptions
  • allow temporary bypass
  • trust stale UX state as authority

Auth depends on Core for company entitlements.

Frontend → Base/Module Backend → Auth → Core

or for frontend bootstrap:

Frontend → Auth → Core

If Core cannot provide entitlements:

  • Auth must fail access resolution
  • Base/module backend must fail request
  • response must be 503

No degraded privilege mode by default.


Base/module backends must not call Core directly for final user authorization.

Core only provides:

  • company commercial state
  • enabled modules
  • entitlement version

Auth merges that with membership grants and permissions.


x-org = canonical company id

It identifies the active company context for a tenant-scoped request.


x-org:

  • must be present on tenant routes
  • must not be guessed
  • must not silently fallback in Base/module backends
  • must use canonical company id format everywhere

If legacy company identifiers exist:

  • normalize them before authorization logic
  • avoid mixed Mongo _id vs UUID usage inside enforcement

Target state:

one canonical company id everywhere

Base/module backends:

  • always require explicit x-org for tenant routes

Frontend/Auth bootstrap:

  • may implement company selection UX
  • may store last selected company in client state

But that selection must become explicit x-org on business requests.


Caching is allowed only as a performance optimization.

It must never become the source of truth.

access:{userId}:{companyId}:{tokenVersion}

Strongly recommended richer key or stale-check:

access:{companyId}:{membershipId}:{accessVersion}:{entitlementVersion}

Cached access must be considered valid only if all required inputs still match current context.

Minimum checks:

  • userId matches
  • companyId matches
  • tokenVersion matches

Strongly recommended:

  • accessVersion matches
  • entitlementVersion matches

Invalidate cached access when any of these change:

  • login
  • refresh
  • tokenVersion change
  • membership change
  • module grant change
  • permission change
  • delegation policy change
  • company subscription change
  • company add-on change
  • entitlementVersion change

If Auth is unavailable:

DO NOT USE STALE CACHE TO ALLOW REQUESTS

Return 503.

Fail closed.


TTL may be short-lived, for example:

  • 30 seconds
  • 60 seconds
  • 120 seconds

TTL improves performance but is not a correctness mechanism.

Correctness depends on invalidation.


14. Access must never be inferred from JWT alone

Section titled “14. Access must never be inferred from JWT alone”

JWT proves identity and session context.

JWT does not contain authoritative access state.

JWT must not be used alone to decide:

  • module access
  • permission access
  • company entitlement
  • delegation rights

All access decisions must come from Auth effective access resolution.


Every backend route group should be classified.

TypeRequires JWTRequires x-orgUses Auth effective accessNotes
TenantYesYesYesStandard business routes
VendorYesNoNoVendor token model
PublicNoNoNoOpen endpoints
MixedYesConditionalConditionalExplicit rules required

Tenant routes:

  • require JWT
  • require x-org
  • require Auth effective access
  • require module and permission enforcement

Vendor routes:

  • validate vendor token class
  • do not use tenant company membership model unless explicitly designed
  • do not assume x-org

Public routes:

  • do not require JWT
  • do not call Auth for access checks
  • are not allowed to expose protected data

Mixed routes must explicitly document:

  • allowed principal types
  • whether x-org is required
  • whether effective access is required
  • whether vendor tokens are allowed

No implicit fallback.


Frontend uses access for UX only.

16.1 Frontend may use Auth access response for:

Section titled “16.1 Frontend may use Auth access response for:”
  • module cards / app tiles
  • route guards
  • showing/hiding buttons
  • settings pages
  • user management screens

  • cached access is permanent truth
  • local state overrides backend checks
  • token contains modules/permissions
  • visible UI equals authorized API access

Backends remain the final authority.


  • login completes
  • access token refresh completes
  • company changes
  • app reload happens
  • user performs grant/revoke actions
  • admin changes company add-ons
  • request returns access-changed / forbidden due to updated state

17. Auth already implemented — clarification

Section titled “17. Auth already implemented — clarification”

When the docs say Auth is already implemented and should not be changed blindly, this means:

  • do not refactor Auth carelessly
  • do not break existing contracts unnecessarily

It does not mean:

  • Auth is frozen forever
  • /auth/me/access must remain incomplete
  • Core integration cannot be added

Required work still includes:

  • proper Core integration
  • correct access merging
  • Redis caching/invalidation
  • invitation / membership / delegation support
  • internal service-to-service contract hardening

QA must validate at minimum:

  • valid JWT
  • valid x-org
  • valid membership
  • valid module
  • valid permission
  • request succeeds
  • invalid JWT → 401
  • expired JWT → 401
  • wrong issuer/audience → 401
  • missing x-org on tenant route → 400
  • malformed x-org400
  • no membership in company → 403
  • module not enabled → 403
  • permission not granted → 403
  • Auth unavailable → 503
  • Core unavailable → Auth fails → backend returns 503
  • tokenVersion change invalidates cache
  • entitlementVersion change invalidates cache
  • stale cache present + Auth unavailable → request denied, not allowed

  • GET /auth/me/access finalized
  • Core entitlements call integrated
  • effective access merge implemented
  • delegation included if required
  • cache + invalidation implemented
  • JWT validation middleware
  • x-org enforcement middleware
  • Auth access-resolution middleware
  • module guard middleware
  • permission guard middleware
  • fail-closed behavior
  • same enforcement model as Base
  • no direct Core access for authorization
  • route classification documented
  • access bootstrap after login
  • access refresh after company switch
  • never trust UI alone

Base / module backends enforce
Auth decides
Core provides entitlements
Frontend is not trusted

Company purchase enables modules.
Membership grants decide who can use them.
Permissions decide what they can do inside them.
Roles and delegation decide who can grant access to others.