System Blueprint → Access engine (permissions)
Related documentation: Commercial Model · Permissions Catalog · Backend Core Packages · Backend Core Modules · Backend Core Addons
Permissions and Delegation Design Specification
Section titled “Permissions and Delegation Design Specification”This page defines the permission and delegation model only.
Organization access administration boundary
Section titled “Organization access administration boundary”Tenant organization administration is split by responsibility:
- Promoters, Artists, and Venues provide Users & Access for invitations, Auth catalog-backed module/general permissions, and shared Core business-unit structure plus Auth assignments.
- Giving a user Finance access starts them as a Submitter in each selected business unit.
- Finance owns only Finance workflow elevation: Submitter/Approver, approval limits, primary approver, and Primary Finance Admin.
- Finance cannot create, rename, archive, delete, or assign business units.
- Approver status is DERIVED from the organization level, never chosen (2026-07-21). The persona app sets who someone is; Finance sets how much they may approve.
| Organization level | Set in | Approves |
|---|---|---|
| Tenant super admin | Persona app (company role) | Anywhere in the company |
| Organization admin | Persona app (company role) | Anywhere in the company |
| Business unit admin | Persona app (business-unit role) | That business unit |
| Member | — | Nothing; submits only |
A company-level admin (Organization admin or Tenant super admin) reaches every business unit by role, so the persona app does not offer them a business-unit picker, and their approval limit is company-level rather than per unit. Their existing business-unit rows are preserved rather than cleared, so a demotion restores them.
-
Primary Finance Admin and primary approver are org-level decisions. Setting who is the company’s Primary Finance Admin, or which approver is primary in a business unit that has more than one, is done only by an organization admin or tenant super admin — a business-unit admin runs their own unit but does not appoint company-wide roles. Primary Finance Admin can only be held by an org-level role. A business unit with a single approver has that person as primary by derivation, not by a checkbox. Finance enforces this server-side (403), not only by hiding the controls.
Granting Finance access starts a user as Submitter; they leave that state by being given a level, not by a Finance dropdown. Finance’s update payload carries no role field at all — the route echoes the stored role back, and the validator rejects a role outright, which is what makes it impossible for a Finance edit to change the organization level. The company
FINANCErole does not grant approval: it outranksADMINin Auth’s rank ladder but is a domain role, not a level. Legacy hand-setAPPROVERbusiness-unit rows still approve; nothing writes new ones. -
Business-unit administrators (2026-07-21). The Auth business-unit role
ADMINgrants management of the people in that unit: list them, assign and unassign them within the unit, set their business-unit role, and invite into it. It does not grant company role changes, delegation, Primary Finance Admin, module or permission grants, business-unit create/rename/archive, team management, or any action on Owners, Admins, Tenant Superadmins, or members outside the unit. -
The authority tiers are full (owner / admin / tenant superadmin) → delegated (company-wide, clamped to grantable modules and permissions) → scoped (business-unit administrator).
GET /api/access/authorityreports the caller’s tier so the UI hides what the API refuses. -
The business-unit role column is shared, and
ADMINis persona-owned. Finance writes the samebusiness_unit_memberships.rolefor its invoice workflow (SUBMITTER/APPROVER). Auth and Finance therefore acceptADMINand treat it as approving — Finance shows it read-only and echoes it back unchanged. Rejecting it made Finance writeAPPROVERinstead, silently stripping organization authority on any approval-limit edit. The persona app is where the business-unit role is granted. -
The business-unit administrator IS the department head — one manager per unit.
isDepartmentHeadand the business-unitADMINrole record the same fact and are written together; the persona app offers a single control./auth/me/accessexposesisDepartmentHeadon each business unit and callers accept either signal, so memberships flagged before the role existed keep their authority with no migration. Auth’s partial unique index (business_unit_department_head_idx) is what enforces “one manager”: a second administrator in the same unit is refused. -
ADMINwas chosen deliberately over the alternatives. Finance’sAPPROVER/isPrimaryApproveris invoice-approval routing — coupling it would mean changing who signs invoices silently changes who can add users, and Finance roles must never grant persona-app powers.isDepartmentHeadwas org-chart data only; it has since been merged with this role, because administration duties are not in fact shared — a unit has one manager. -
A deactivated membership is not a membership. Auth keeps the
company_membershipsrow (returned on/auth/mewithisActive: false) and refuses it on/auth/me/access. Every consumer of/auth/mememberships MUST excludeisActive === false— the admin module does, and a persona app that does not will offer the user a company they cannot open (the Promoters company switcher did exactly this, labelling a removed company “Active membership”). Exclude onisActive === false, not on falsiness, so a row from an older Auth that omits the flag is still treated as active. A client that caches/auth/memust also revalidate it (stale-while-revalidate): trusting a cached profile forever grants access that Auth has since revoked — the Finance app kept offering a deactivated business unit in its create-bill picker for exactly this reason. -
Frontend navigation constants are not permission catalogs. Grant UIs must read the Auth permission catalog.
-
Concretely:
NAVIGATION_PERMISSIONS(Frontend-Kisum-Promoters/src/common/contants/menu.ts) holds unprefixed legacy keys used only to decide which sidebar items render —'event.view','avail.view'. The Auth catalog is namespaced —promoter.event.view. The two are not interchangeable, and a grant UI built on the nav constants would write keys no backend check ever matches. Grant UIs readGET /api/access/catalog; navigation constants stay for menu visibility only. -
it is not an implementation page
-
it is not a route/API page
-
it is not a current-state audit
-
it explains how permissions should be designed regardless of current service status
It describes the intended logic for:
- company commercial eligibility
- user permissions
- scope restrictions
- delegation
Access caching strategy (platform-wide)
Section titled “Access caching strategy (platform-wide)”Resolving access is expensive: one GET /auth/me/access costs Auth ~6 queries plus a live Core entitlements call, and Core’s subscription-summary recomputes entitlements a second time. The shared DigitalOcean Postgres cluster is sized at max_connections = 100 (3 reserved for superuser, so 97 usable) and is shared by local, staging and prod, with each service pool deliberately capped at 4. Connections are not the binding constraint today — query volume and connection hold time are — so the lever is fewer, shorter queries.
Every layer caches with a short TTL plus in-flight coalescing (so the ~10 parallel requests of one page load make one upstream call, not ten):
| Layer | What is cached | Default TTL | Env override |
|---|---|---|---|
Auth MeAccess | Resolved AccessOut, keyed user|company|tokenVersion | 30s | AUTH_ACCESS_CACHE_TTL_MS |
| Auth → Core client | GET /internal/companies/{id}/entitlements | 30s | CORE_ENTITLEMENTS_CACHE_TTL_MS |
| Promoters → Auth | Raw /auth/me/access payload, keyed user|company|tokenVersion | 60s | AUTH_ACCESS_CACHE_TTL_MS |
| Promoters → Core | Company snapshot: company + profile + subscription-summary | 60s | COMPANY_SNAPSHOT_CACHE_TTL_MS |
Rules every implementation must follow:
- Never cache a failure. 401/403/503 and empty snapshots stay live, so a revoked membership or a recovering upstream is seen on the very next request — and a transient outage is not pinned for the whole TTL.
- Cache the ungated payload, gate per caller. Promoters caches the raw Auth response and applies the module gate afterwards, so a caller that needs the persona of a company it may not open (the wrong-app redirect) still sees
package/packageAppUrlfrom the same cache entry. tokenVersionis part of the key, so logout and password change invalidate for free.- Writes invalidate explicitly. Auth drops its MeAccess cache for a company inside
bumpAccessPolicyVersion(every access mutation funnels through it) and on membership upsert/remove. x-redis: bypassis the platform-wide “read live” header. It propagates from the browser through Promoters into Auth and drops every cache in the chain for that company. Use it only after a write that changes entitlements (billing) — never on a normal load.
1. Permission model layers
Section titled “1. Permission model layers”The platform should resolve access in layers:
Layer 1: company commercial eligibility
Section titled “Layer 1: company commercial eligibility”Question:
Did the company buy the relevant package, module, or add-on?Layer 2: user module access
Section titled “Layer 2: user module access”Question:
Is the user allowed to access that module?Layer 3: user permission access
Section titled “Layer 3: user permission access”Question:
What actions inside that module are allowed?Permissions derived from the organization level
Section titled “Permissions derived from the organization level”A permission may be granted explicitly (team rule or user rule) or derived from the role the person already holds. Both are ordinary grants: they are filtered by the company’s entitled modules and overridden by an explicit user deny.
Auth computes the derivation in effectiveAccessV2:
| Role held in the company | Receives |
|---|---|
Company TENANT_SUPERADMIN or ADMIN | Every permission of every entitled module |
Business-unit member (SUBMITTER) | finance.invoice.view/edit, finance.bill.view/edit, finance.vendor.view |
Business-unit approver (APPROVER) | The above plus finance.bill.approve, finance.invoice.approve |
Business-unit administrator (ADMIN / department head) | The above plus finance.settlement.view, finance.report.view |
Rationale: the business-unit assignment is itself the grant. A person made a
Submitter is, by that act, the person who submits — requiring a separate
finance.bill.edit tick that no screen offered left every Finance user with the
module enabled and no permission key, so the app opened empty and every route
returned 403.
Record-level scope is unchanged and separate: the derived key opens the screen,
Layer 4 decides which rows are returned. A Submitter sees their own records.
Company entitlement still wins. A derived finance.* key is dropped when the
company has no Finance package, for every role including tenant super admin.
A subsection grant carries the area that opens it
Section titled “A subsection grant carries the area that opens it”Granting promoter.event.<subsection>.<action> without promoter.event.view
produces a permission its holder can never use: the Event area refuses them at the
door, so the tab they were given is unreachable. Auth therefore emits an implied
promoter.event.view rule whenever a membership holds an Event subsection
grant and no Event-area allow of its own.
- The implied rule copies the implying rule’s scope and business units, so the area becomes visible exactly as widely as the subsection already was.
- Only the area key is implied. Holding one Event subsection still says nothing about any other — sales, estimate, commercial, and tax stay independently assignable.
- It is emitted as a rule, not only as a flat permission. Consumers gate with
permissionsand scope withaccessV2.rules; a key present in only one of them fails in a way that looks like an unrelated bug. sourceTypeisDERIVED, so an access audit can tell computed rules fromTEAM/USERrows. An explicit deny still wins.
Note the direction: a child implies its parent. The parent never implies its
children — promoter.event.view opens the area and exposes no subsection.
Layer 4: scope restrictions
Section titled “Layer 4: scope restrictions”Question:
Is the user limited to a company, business unit, workflow slice, or data subset?Layer 5: delegation
Section titled “Layer 5: delegation”Question:
What can this user grant or manage for someone else?This layered model must always be preserved. One layer must not silently replace another.
2. Permission naming rules
Section titled “2. Permission naming rules”Permissions should always be namespaced by module.
Examples:
basic.artists.readbasic.events.createbasic.tasks.managefinance.expenses.readfinance.income.readfinance.bills.readmarket.contracts.readai.predictions.readvenue.availability.read
Each permission should be structured like:
<module>.<resource>.<action>Where:
module= product boundaryresource= business object or feature areaaction= read, create, update, approve, manage, export, etc.
2.1 Permission design rules
Section titled “2.1 Permission design rules”Permission names must:
- remain stable
- be meaningful to backend and frontend teams
- reflect business capability, not UI component names
- never include package names
- never include temporary pricing or campaign language
Good examples:
finance.reports.readbasic.contacts.readmarket.contracts.write
Bad examples:
basic-package-userpromo-finance-editorscreen-4-edit
3. Permission families by module
Section titled “3. Permission families by module”3.1 Basic
Section titled “3.1 Basic”Examples:
basic.artists.readbasic.artists.analyzebasic.contacts.readbasic.events.readbasic.events.createbasic.events.updatebasic.market-study.readbasic.pnl.readbasic.tasks.managebasic.news.read
3.2 Finance
Section titled “3.2 Finance”Examples:
finance.expenses.readfinance.expenses.writefinance.income.readfinance.income.writefinance.bills.readfinance.bills.writefinance.ticketing.readfinance.reports.readfinance.vendors.read
3.3 AI
Section titled “3.3 AI”Examples:
ai.predictions.readai.research.readai.valuations.readai.sales-reports.read
3.4 Touring
Section titled “3.4 Touring”Examples:
touring.routes.readtouring.routes.writetouring.logistics.managetouring.coordination.manage
3.5 Market
Section titled “3.5 Market”Examples:
market.contracts.readmarket.contracts.writemarket.escrow.readmarket.investments.read
3.6 Venue Availability
Section titled “3.6 Venue Availability”Examples:
venue.availability.readvenue.details.readvenue.details.writevenue.calendar.readvenue.calendar.writevenue.bookings.readvenue.bookings.writevenue.customers.readvenue.customers.writevenue.contracts.readvenue.contracts.writevenue.deposits.readvenue.deposits.writevenue.approvals.readvenue.approvals.approvevenue.reports.readvenue.operations.manage
4. Actions and permission semantics
Section titled “4. Actions and permission semantics”The action segment should be consistent across the platform.
Common actions:
readcreateupdatedeleteapprovemanageexportassign
4.1 Action meaning
Section titled “4.1 Action meaning”Use actions consistently:
read= can viewcreate= can create new recordsupdate= can modify existing recordsdelete= can remove or archive recordsapprove= can perform controlled approval workflowsmanage= broad administrative control over a resource familyexport= can export/report/download structured outputassign= can link or grant access to something
5. Scope restrictions
Section titled “5. Scope restrictions”Permissions alone are not always enough.
Some access must also be scoped.
Examples:
- a user can read bills but only for one business unit
- a user can read finance reports but not income details
- a user can update events only inside one operational scope
Possible scope models:
- company scope
- business-unit scope
- workflow scope
- ownership scope
Example:
User has:- finance.bills.read
Scope:- business unit = BU-X only
Result:- user can read bills- but only for BU-X5.1 Important scope rule
Section titled “5.1 Important scope rule”Scope should narrow a permission.
Scope should not create a permission that does not otherwise exist.
Bad design:
No finance permission existsbut BU scope alone gives finance accessGood design:
finance.bills.read existsscope narrows it to BU-X6. Delegation
Section titled “6. Delegation”Delegation must be documented as a separate concept from permission.
6.1 Permission
Section titled “6.1 Permission”Permission means:
What can this user do personally?6.2 Delegation
Section titled “6.2 Delegation”Delegation means:
What can this user grant or manage for someone else?6.3 Example
Section titled “6.3 Example”A manager may have:
finance.bills.readfinance.bills.write
But delegation may allow them to grant only:
finance.bills.read
and not:
finance.bills.writefinance.income.read
So a user may personally have more access than they are allowed to delegate.
7. Delegation fields
Section titled “7. Delegation fields”Delegation should conceptually control:
- grantable modules
- grantable permissions
- whether the user can manage users
- whether the user can buy or assign add-ons
- whether the user can assign access across all business units or only some scopes
Examples:
ADMIN- can grant: basic, finance- can grant permissions: finance.bills.read, finance.expenses.read- canManageUsers: true- canBuyAddons: falseMANAGER- can grant: basic- can grant permissions: basic.events.read, basic.tasks.manage- canManageUsers: false- canBuyAddons: false7.1 Delegation must never exceed personal access
Section titled “7.1 Delegation must never exceed personal access”A user must never be able to grant:
- a module they do not personally control in an allowed way
- a permission they are not allowed to delegate
- a scope broader than their own governance authority
8. Relationship to packages, modules, and add-ons
Section titled “8. Relationship to packages, modules, and add-ons”Permissions are not designed directly from package names.
Use this order:
Package / standalone module / add-on-> commercial eligibility-> module eligibility-> permission-family eligibility-> user permission grants-> scope restrictions-> delegation limits8.1 Package
Section titled “8.1 Package”Package determines what is commercially available.
8.2 Module
Section titled “8.2 Module”Module determines the permission namespace.
8.3 Add-on
Section titled “8.3 Add-on”Add-on may unlock additional capability eligibility.
8.4 Permission
Section titled “8.4 Permission”Permission determines what the user can do.
8.5 Delegation
Section titled “8.5 Delegation”Delegation determines what the user may grant or manage for someone else.
9. Finance examples
Section titled “9. Finance examples”9.1 Bills only
Section titled “9.1 Bills only”Company owns finance moduleUser gets:- finance.bills.readUser does not get:- finance.income.readResult:
- user can access bills
- user cannot access income
9.2 Bills for one business unit only
Section titled “9.2 Bills for one business unit only”Company owns finance moduleUser gets:- finance.bills.readScope:- BU-X onlyResult:
- user can access bills
- only inside BU-X
9.3 Delegation example
Section titled “9.3 Delegation example”Manager has:- finance.bills.read- finance.bills.write
Delegation:- may grant finance.bills.read onlyResult:
- manager can use write access personally
- manager cannot grant write access to other users
10. Summary
Section titled “10. Summary”The access model must stay clean:
- packages, modules, and add-ons decide commercial eligibility
- permissions decide user actions
- scope decides where those actions apply
- delegation decides what can be granted to others
These concerns must stay separate or the access model will become inconsistent very quickly.
Promoters Permissions V2 rollout (2026-07-14)
Section titled “Promoters Permissions V2 rollout (2026-07-14)”Promoter companies now have an authorization-v2 policy in Auth with LEGACY, SHADOW, and ENFORCED modes. The evaluation order is: Core entitlement, module grant, action rule, record scope, workflow rule, then individual deny. Individual denies win over team grants.
Core provisions six renameable and archivable promoter business-unit templates. Auth owns shared teams, multi-BU/team memberships, scoped allow/deny rules, Finance roles and limits, primary approvers, invitation policy snapshots, audit history, and effective-access calculation. The flat modules and permissions response remains available while accessV2 carries the structured policy.
Events and tours support company, BU, and direct-user assignment scopes. New records require an active BU when the company is ENFORCED. Existing records are migrated to Operations before activation. Activation is company-by-company and rollback changes the policy to LEGACY without deleting V2 configuration.
Event creation embedded in another workflow is not an authorization shortcut. For example, a marketplace offer may create a promoter event only when each new show supplies Core BU UUIDs and the caller has promoter.event.create in those BU scopes. The separate offer permission is still required for the offer itself.
The company rollout command accepts only the canonical Core company UUID. It provisions Core defaults and Auth team presets, empties normal-user V2 access, preserves Owner/Admin recovery access, assigns that company’s existing Promoters Events/Tours to Operations, and enters SHADOW. Promoters resolves any domain-storage compatibility internally; operators never provide a legacy company id. Activation checks both Auth preflight and unassigned domain records before switching to ENFORCED.
Default team provisioning sends an explicit map of the six Core business-unit UUIDs to Auth. Policy transitions are persisted as the PostgreSQL access_policy_mode enum. On 2026-07-14, Primuse Entertainment and Manik 360 Limited entered ENFORCED after clean checks. All current companies now use enforced access. On 2026-07-20, browser rollout switching and preflight were retired; the Promoters BFF returns 410 Gone for those routes while internal Auth policy support and runtime enforcement remain active.
Finance uses one primary APPROVER per BU and one primary Finance Admin per company. Within the BU limit the ladder is Approver plus Admin; above the BU limit but within the Admin limit it is Admin plus Tenant Superadmin; above the Admin limit it is Tenant Superadmin only. A null limit remains unlimited.
Implemented cross-module booking scope
Section titled “Implemented cross-module booking scope”Promoters resolves the event scope before forwarding. Artists booking requests and Venue marketplace bookings persist the promoter event UUID, BU UUIDs, acting Auth user UUID, and scope type. These services return metadata only; Promoters applies company, BU, assigned-event, own-record, and individual-deny rules before returning records.
Finance browser routes in ENFORCED mode are centrally mapped to exact finance.* permission families. Bills, income/settlement, payments, reports, accounting, vendors, files, Xero, notifications, tax/currency, and settings fail closed when access is missing or a business route is unmapped. Workflow actions additionally require the Auth Finance role.