PRD: Per-merchant scoped authorization
| Module | Permissions | PRD ID | PRD-EFF-001 |
| Status | Ready to dev | FEAT | EFF · BANA-1436 |
| Epic | — | Plane | BANA-1548 |
| Date | 2026-06-04 | Version | v1.0 |
| Packages | @nx/identity | URD | EFF |
| Surface | Backend | ||
| Owner | Phát Nguyễn | ||
What per-merchant scoped authorization is
Centralizes authorization on a per-merchant policy engine so every verifier service enforces grants within the active merchant chosen per request, and resolves a user's reachable merchants/organizers from the authorization policy store instead of scattered request-context filtering. The result: "what can this user see and do here" becomes one consistent, policy-backed answer across commerce, finance, and inventory - with per-merchant isolation and soft-delete honoured.
Why centralize it
Authorization in BANA is originally driven by request-context filtering: each service derives a user's reachable organizers and merchants from the request and hand-filters its queries. This makes "what can this user see" implicit, duplicated across services, and impossible to reason about in one place - and it has no single home to resolve a grant within the active merchant a request operates in.
A central scoped-RBAC engine across the verifier services closes this gap: route-level authorization is enforced the same way everywhere through the policy engine, and HQ merchant reach is expanded from organizer/merchant membership rather than per-request guesswork. Because BANA needs soft-delete-aware, per-merchant policy loading, the platform core supplies the entity contract, the soft-delete flag, and the per-merchant domain resolver to the framework's scoped Casbin adapter - no framework change required. Inventory was the first pilot surface; coarse role grants are now seeded across every module/verifier service.
The easiest place to get it wrong
The active merchant decides scope, not the user's permissions
A user's permissions (what they can do) and the request's active merchant (where they're acting) are two different questions. A grant on a merchant only takes effect while the user is acting on that exact active merchant of the request - switching to another merchant, the grant no longer applies, even though the user is still the same person.
This is also why an Owner who owns several merchants still has to switch the active merchant on screen to act on a different one, instead of seeing every merchant's data merged together.
1. Goals & Non-Goals
Goals
- Enforce authorization across every verifier service, resolving grants within the active merchant selected per request.
- Drive route-level authorization through the policy store; resolve the per-request active merchant (used for row-level data scoping) from the active-merchant header, and expand HQ reach from organizer/merchant membership, instead of bespoke per-service request parsing (commerce, finance, core).
- Configure the framework's scoped Casbin adapter from the platform core - supplying the soft-delete-aware, per-merchant entity contract and domain resolver - requiring no framework changes.
- Roll out per-merchant enforcement starting with inventory and now across every verifier service: seed coarse role permissions per module and grant owners merchant membership at merchant creation.
- Keep the engine aligned with the platform's scoped RBAC model (scoped-model correctness, the scoped policy loader, policy-cache invalidation on token refresh).
Non-Goals
- Wildcard / regex permissions - out of scope (see Non-Goals in the URD).
- Permission categories / UI grouping, and role templates / bundles.
- Time- or shift-based permissions and permission audit logging.
- The resource / action / domain hierarchy and declaration toolkit - owned by PRD-HIER-001 (
HIER,DECL).
2. Success Metrics
| Metric | Target / signal |
|---|---|
| Central enforcement | 100% of verifier-service authorization decisions flow through the central engine, not ad-hoc request filtering |
| Scope correctness | Merchant/organizer reach resolved from policy matches the user's actual grants; no foreign-merchant data leaks |
| Tenant isolation | Policy/membership rows never cross a merchant boundary |
| Soft-delete fidelity | Revoked (soft-deleted) grants are never enforced as active |
| Pilot stability | Inventory operates fully under per-merchant authorization with no regression in allowed-merchant resolution |
3. Personas & Use Cases
| Persona | Goal in this feature |
|---|---|
| Owner | Reach only their own organizer and its merchants - resolved consistently from policy |
| Employee / Cashier | Act only within assigned merchants, enforced per active merchant |
| Platform admin | Trust one central enforcement path across all verifier services |
| Service (verifier) | Resolve allowed merchant IDs from policy instead of bespoke per-request filtering |
Core scenarios: a request arrives selecting an active merchant → the verifier enforces the grant within that merchant via the central engine → reachable merchants/organizers are resolved from the policy store → soft-deleted grants are excluded and tenant rows stay isolated per merchant.
4. User Stories
| # | As a | I want | So that |
|---|---|---|---|
| 01 | platform admin | authorization enforced centrally in the verifier | every service answers "can this user do this here" the same way |
| 02 | owner | my reachable merchants resolved from policy | I see exactly my organizer's merchants and nothing foreign |
| 03 | employee | grants enforced within the active merchant | my access is scoped to the merchant I'm acting on |
| 04 | service developer | to resolve allowed merchant IDs from the policy store | I stop hand-filtering queries from request context |
| 05 | platform admin | revoked grants to stop applying immediately (no soft-deleted rows enforced) | access revocation is trustworthy |
| 06 | platform admin | a refreshed token to clear the policy cache | role/permission changes take effect cleanly |
5. Functional Requirements
| # | Requirement | Status | URD ref |
|---|---|---|---|
FR-001 | Every business area of the system (sales, inventory, finance, platform admin...) applies the same central authorization rule. No area rolls its own permission check. | 🚧 | URD-EFF-004 |
FR-002 | A grant on a merchant only takes effect while the user is acting on that exact active merchant of the request. Switching to another merchant, the grant no longer applies. | ✅ | URD-EFF-004 · URD-ROLE-005..006 |
FR-003 | A user's effective permissions are their direct grants plus role-inherited grants, merged and deduplicated. A lookup can filter to direct only, inherited only, or both. | ✅ | URD-EFF-001..002 |
FR-004 | Access to a system function is resolved consistently from one place for every request, not customized per business area. The active merchant used for row-level data scoping is taken from the merchant the user has selected on screen. | ✅ | URD-EFF-003 |
FR-005 | Granting or revoking a permission can be repeated any number of times without creating duplicate rows or errors. A user cannot grant themselves or anyone else a permission higher than what they already hold. | 🔶 | URD-GRANT-001..007 |
FR-006 | A removed permission or membership is never treated as still in effect. The record is kept for history. | ✅ | URD-EFF-004 |
FR-007 | A membership is only looked up under its own scope type (organizer or merchant). No type leaks into another type or into another merchant. | ✅ | URD-ROLE-007..008 |
FR-008 | An Owner assigned at an organizer's headquarters merchant automatically reaches every sibling merchant of that organizer. No separate grant needed per merchant. | ✅ | URD-ROLE-009 |
FR-009 | When a new merchant is created, its owner is immediately granted default role-based management permissions across every business area. No manual grant needed. | ✅ | URD-EFF-003..004 |
FR-010 | After a user's role or permissions change, their next sign-in or session refresh applies the change immediately. No additional wait or action. | ✅ | URD-EFF-004 |
5.1 Acceptance Criteria
- An action is denied for missing permission in the sales area; the same missing permission tried in inventory or finance → also denied the same way.
- An employee granted a permission on merchant A tries the same action while on merchant B, not granted → denied; switching back to merchant A → the action succeeds.
- An account holds a role with a permission and is also granted a separate permission directly; looking up effective permissions → both show up, no duplicate rows; filtering to direct only → only the separately granted permission shows; filtering to inherited only → only the role-based permission shows.
- Switching to a different merchant on screen (changing the active merchant) → the data shown changes to match the newly selected merchant, with no other merchant's data mixed in.
- Granting a permission that was already granted, a second time → no duplicate grant row, no error; revoking a permission that was already revoked → no error, the permission stays revoked.
- A user without permission-management rights tries to grant themselves a higher permission than they hold → denied.
- Revoking a user's permission → the very next action by that user is denied, even though the old grant record is kept in history.
- Looking up a user's organizer-level membership → never mixes in another user's merchant-level membership list, and vice versa.
- An Owner assigned at the headquarters merchant of an organizer with several sibling merchants → can see and act on every sibling merchant, even without ever being granted permission on each one individually.
- Creating a new merchant → its owner immediately sees default management permissions across every business area, with no one granting them by hand.
- A user's role or permissions change, then they sign in again or refresh their session → they act under the new permissions right away, with no wait or extra step.
6. Non-Functional Requirements
| Area | Requirement |
|---|---|
| Central enforcement | All authorization decisions in verifier services go through one central engine; no per-service ad-hoc filtering remains as the source of truth |
| Tenancy & authz | Every grant resolves within one active merchant per request; policy/membership rows are isolated per merchant |
| Soft-delete fidelity | The scoped policy loader excludes soft-deleted policy/membership rows; revocation is honoured without physical deletes |
| Consistency | Cache invalidation on token refresh keeps enforcement aligned with current policy |
| Framework alignment | Configuring the framework's scoped Casbin adapter requires no framework changes and tracks the platform's scoped RBAC model |
| i18n | User-facing role/permission labels remain bilingual (English & Vietnamese) |
7. UX & Flows
Key surfaces are backend: central enforcement lives in the platform core; commerce/finance/core resolve scope from policy; inventory is the pilot service. There is no dedicated end-user screen - this increment is the authorization substrate other modules rely on.
8. Data & Domain
| Entity | Role |
|---|---|
| Policy grant | A grant: subject, resource, action, scoped to a merchant |
| Membership | User→role, user→org/merchant, role→org/merchant; tenant-isolated by type |
| Scoped Casbin adapter (framework, configured by core) | Loads policy/membership rows soft-delete-aware and per-merchant |
| Active merchant | The merchant a request operates in, selected per request |
| Scoped RBAC model | Defines how grants resolve within a merchant |
Conceptual only - full policy schema and loader contract in the developer Casbin Authorization docs and RBAC docs.
9. Dependencies & Assumptions
Depends on
| # | Feature | What it depends on |
|---|---|---|
| 01 | Fixed & custom roles + grants (URD-ROLE · URD-GRANT) | the policy rows the engine enforces. |
| 02 | Commerce | organizers and merchants are the scopes grants attach to. |
| 03 | Identity | sign-in token carries role context; token refresh triggers cache invalidation. |
| 04 | Platform framework | the scoped RBAC model and scoped Casbin adapter the platform core configures. |
Assumptions
| # | Assumption | What breaks if wrong |
|---|---|---|
| 01 | Requests carry an active merchant selecting the scope. | Without an active merchant, the engine has no merchant to resolve the grant within - every authorization decision loses the domain to anchor on. |
| 02 | A merchant's policy/membership rows exist under per-merchant isolation. | If isolation is wrong, one merchant's data or permissions can leak into another merchant. |
| 03 | Owner roles are backfilled and inventory role permissions seeded for the pilot. | Missing the backfill or seed means the inventory pilot doesn't enforce correctly - an owner doesn't have default management permissions ready the moment their merchant is created. |
10. Release Plan & Launch Criteria
| Aspect | Plan |
|---|---|
| Phase | P2 (EFF + GRANT, Built) - see URD feature catalog |
| Rollout | Inventory pilot first; coarse role grants now seeded across every verifier service |
| Migration | Seed coarse role permissions per module; grant owners merchant membership at merchant creation |
| Launch criteria | Central enforcement verified in the verifier; scope resolved from policy matches grants; soft-delete and per-merchant isolation verified; inventory allowed-merchant resolution correct end-to-end |
| Monitoring | Authorization decision path coverage, foreign-merchant access attempts, cache invalidation on token refresh, pilot error rate |
References
- URD: Permissions - Effective Permissions & Scope · Grant / Revoke · Fixed Roles
- Related PRD: Resource, Action & Domain Hierarchy
- Module: Permissions - URD
- Developer: @nx/core · @nx/identity · Casbin Authorization
Risks & Open Questions
| # | Risk / question | Mitigation / status |
|---|---|---|
| 01 | Framework policy loader ignores soft-delete | Platform core configures the framework's scoped adapter with a soft-delete-aware entity contract; no framework change |
| 02 | Cross-merchant policy leakage | Membership lookup constrains the type; rows isolated per merchant |
| 03 | Some list-reading paths (merchant/organizer pickers before entering a context, a few lookup screens) still do their own scope filtering instead of going through the central engine | Open - unifying these paths onto the central engine is outstanding work (FR-001) |
| 04 | Privilege-escalation blocking currently applies only on the role-based grant path; the direct per-user permission grant path does not compare against the grantor ceiling | Open - the direct path is only open to system accounts so the practical risk is low, ceiling comparison to be added (FR-005) |
| 05 | Active merchant (active-merchant context) is trusted as-is | Open - the per-request merchant-ownership verification is currently commented out in code; isolation relies on the header being correct. Re-enabling the ownership check is outstanding |
| 06 | Stale enforcement after role/permission change | Policy cache cleared on token refresh |
| 07 | Framework RBAC model drifts from app expectations | Track the scoped model; migrate as the upstream scoped adapter changes |
| 08 | Per-merchant domain cascade not yet built | Coarse role grants are seeded across every module, but organizer→merchant domain inheritance is still pending - see PRD-HIER-001 |
Frequently Asked Questions
| # | Question | Answer |
|---|---|---|
| 01 | Where is authorization enforced now? | Centrally - every verifier service enforces through the same engine instead of hand-filtering from request context. |
| 02 | How is "which merchant" decided? | Per request, by the active merchant chosen for that request; a grant resolves only within that merchant. |
| 03 | Why configure the framework's scoped adapter instead of changing the framework? | Core supplies a soft-delete-aware, per-merchant entity contract and domain resolver to the framework's scoped Casbin adapter, giving both soft-delete and per-merchant isolation with no framework change. |
| 04 | Does revoking a grant take effect immediately? | Revocation soft-deletes the policy row, which the scoped adapter excludes; a token refresh clears the policy cache so changes apply cleanly. |
| 05 | Why pilot on inventory first? | It validated end-to-end per-merchant enforcement before coarse role grants were seeded across every module. |
| 06 | Is wildcard / hierarchy permission part of this? | No - coarse grants via the resource/action/domain hierarchy are owned by PRD-HIER-001. |