Tenant Service — User Stories
Service: tenant-service Story prefix: TENANT-US Last updated: 2026-04-18
Stories
TENANT-US-001 — Tenant create, reactivate, and subscription management
| Field | Value |
|---|---|
| Issue type | Story |
| Summary | Super admin creates tenant, reactivates after suspension, and updates subscription |
| Epic link | TENANT-EPIC-01 |
| Status | To Do |
| Priority | Must |
| Story points | 5 |
| Labels | service:tenant, type:backend, type:api, slice:S0 |
| Components | lifecycle-module |
| FR references | FR-TENANT-001, FR-TENANT-004, FR-TENANT-011, FR-TENANT-013 |
| Legacy FR refs | FR-TEN-001, FR-TEN-004, FR-TEN-011, FR-TEN-013 |
| Dependencies | — |
User story: As a Super Admin, when I onboard a new healthcare organization, I want to create a tenant with a unique slug, manage reactivation after suspension, and update subscription terms so that the tenant's commercial relationship stays accurate.
Acceptance criteria (Gherkin):
- Given a valid create payload with unique slug, when
POST /api/v1/admin/tenantsis called, then tenant is persisted withstatus=pending;tenant.tenant.created.v1emitted. - Given a suspended tenant, when
POST /api/v1/admin/tenants/:id/reactivateis called, then status becomesactive;tenant.tenant.reactivated.v1emitted. - Given a valid subscription patch, when
PATCH /api/v1/admin/tenants/:id/subscriptionis called, then tier and dates are updated;tenant.subscription.updated.v1emitted exactly once. - Given a duplicate slug, when create is attempted, then
409 TENANT_SLUG_DUPLICATEreturned.
Technical notes:
- Slug:
[a-z0-9-]{3,100}globally unique; immutable after creation. - Subscription tier enum:
STARTER,PROFESSIONAL,ENTERPRISE.
Definition of Done:
- Tenant-isolation integration test passes.
tenant.tenant.created.v1schema test green.- Coverage ≥ 80%.
TENANT-US-002 — Tenant activate, suspend, terminate, and expiry
| Field | Value |
|---|---|
| Issue type | Story |
| Summary | Orchestrated activation, suspension with IAM invalidation, and termination |
| Epic link | TENANT-EPIC-01 |
| Status | To Do |
| Priority | Must |
| Story points | 8 |
| Labels | service:tenant, type:backend, type:api, slice:S0 |
| Components | lifecycle-module, activation-saga |
| FR references | FR-TENANT-002, FR-TENANT-003, FR-TENANT-005, FR-TENANT-006, FR-TENANT-012 |
| Legacy FR refs | FR-TEN-002, FR-TEN-003, FR-TEN-005, FR-TEN-006, FR-TEN-012 |
| Dependencies | TENANT-US-001, cross-service: IDENT-EPIC-01, FAC-EPIC-01 |
User story: As a Super Admin, when I activate, suspend, or terminate a tenant, I want the platform to orchestrate downstream effects so that licenses, hierarchy, sessions, and users reflect the correct state.
Acceptance criteria (Gherkin):
- Given a
pendingtenant, whenPOST /api/v1/admin/tenants/:id/activatesucceeds, then status=active,root_node_idset,tenant.tenant.activated.v1emitted. - Given an active tenant, when
POST /api/v1/admin/tenants/:id/suspendis called, then status=suspended;tenant.tenant.suspended.v1emitted; identity-service invalidates sessions within token TTL. - Given subscription
end_date <= today, when cron runs, thentenant.subscription.expired.v1emitted once per qualifying tenant. - Given
POST /admin/tenants/:id/terminate, when called, then status=terminated(terminal); all downstream deactivation effects are idempotent.
Technical notes:
- Activation saga: hierarchy node → identity admin user → license seeds. Bounded retry (3x). Tenant stays
PENDINGon exhaustion. - Termination does not delete data (data retained for audit/legal).
Definition of Done:
- Activation saga integration test (success + failure paths) passes.
- Outbox integration test green.
tenant.tenant.activated.v1andtenant.tenant.suspended.v1schema tests green.
TENANT-US-003 — Tenant profile and config KV management
| Field | Value |
|---|---|
| Issue type | Story |
| Summary | Tenant admin updates profile and governed config key-value pairs |
| Epic link | TENANT-EPIC-02 |
| Status | To Do |
| Priority | Must |
| Story points | 3 |
| Labels | service:tenant, type:backend, type:api, slice:S0 |
| Components | config-module |
| FR references | FR-TENANT-007, FR-TENANT-008 |
| Legacy FR refs | FR-TEN-007, FR-TEN-008 |
| Dependencies | TENANT-US-001 |
User story: As a Tenant Admin, when managing my organization's settings, I want to update display name, contact email, locale, and configuration values so that the platform reflects my organization's policies.
Acceptance criteria (Gherkin):
- Given a Tenant Admin for own tenant, when
PATCH /api/v1/tenants/:idis called, then profile fields update;tenant.tenant.updated.v1emitted. - Given a config key outside the allow-list, when
PUT /api/v1/tenants/:id/config/:keyis called, then400 TENANT_CONFIG_KEY_UNKNOWNreturned. - Given a valid allowed key, when value is set or changed, then
tenant.config.changed.v1emitted includingoperationandkey.
Technical notes:
- Allowed keys:
mfa_required,session_timeout_minutes,branding.primary_color,branding.logo_url,max_failed_login_attempts. - Config values stored as JSONB.
Definition of Done:
- Allow-list validation integration test passes.
tenant.config.changed.v1schema test green.
TENANT-US-004 — Country-to-profile default and tenant isolation
| Field | Value |
|---|---|
| Issue type | Story |
| Summary | Country determines default hierarchy profile; cross-tenant access blocked |
| Epic link | TENANT-EPIC-02 |
| Status | To Do |
| Priority | Must |
| Story points | 3 |
| Labels | service:tenant, type:backend, slice:S0 |
| Components | config-module |
| FR references | FR-TENANT-009 |
| Legacy FR refs | FR-TEN-009 |
| Dependencies | TENANT-US-001 |
User story: As a Super Admin, when activating a tenant in Afghanistan, I want the platform to automatically select the AFG_MOPH hierarchy profile so that downstream org structure matches national standards; and cross-tenant access must be blocked at all times.
Acceptance criteria (Gherkin):
- Given tenant with
country_code=AFand no explicit profile, when activation runs, thenhierarchy_profile_id=AFG_MOPHis used. - Given country without direct mapping, when profile resolves, then
PRIVATE_HOSPITALfallback is applied. - Given Tenant Admin requesting data for a different tenant ID, when endpoint is called, then
403 TENANT_CROSS_TENANTreturned.
Technical notes:
- Profile map:
AF → AFG_MOPH,AE → UAE_DOH,* → PRIVATE_HOSPITAL. - RLS policy enforced; mandatory
tenant-isolation.spec.tstest.
Definition of Done:
- Tenant-isolation integration test passes.
- Profile resolution unit test covers all mapping cases.
TENANT-US-005 — Activation idempotency token
| Field | Value |
|---|---|
| Issue type | Story |
| Summary | Idempotency-Key on activation prevents duplicate downstream side effects |
| Epic link | TENANT-EPIC-03 |
| Status | To Do |
| Priority | Must |
| Story points | 3 |
| Labels | service:tenant, type:backend, slice:S0 |
| Components | activation-saga |
| FR references | FR-TENANT-ENH-001 |
| Legacy FR refs | FR-TEN-ENH-001 |
| Dependencies | TENANT-US-002 |
User story: As a Super Admin, when retrying a failed activation request, I want idempotency-key support so that the activation saga does not execute twice and create duplicate hierarchy nodes or users.
Acceptance criteria (Gherkin):
- Given same
Idempotency-Keyand same request body, when activation is retried, then original result is returned without re-running downstream calls. - Given reused key with different payload, when request is sent, then
409 TENANT_IDEMPOTENCY_MISMATCHreturned. - Given first-time token, when activation succeeds, then token record stored with tenant ID and 24h expiry.
Technical notes:
- Idempotency key stored in
idempotency_keystable (key → result, expires_at). - Saga steps are idempotent on their own; idempotency token prevents saga re-entry.
Definition of Done:
- Idempotency integration test passes (retry returns original; different body returns 409).
TENANT-US-006 — Suspension and IAM session invalidation verification
| Field | Value |
|---|---|
| Issue type | Story |
| Summary | Verify IAM session invalidation is measurable and auditable after suspension |
| Epic link | TENANT-EPIC-03 |
| Status | To Do |
| Priority | Must |
| Story points | 5 |
| Labels | service:tenant, type:backend, type:ops, slice:S0 |
| Components | lifecycle-module |
| FR references | FR-TENANT-003 |
| Legacy FR refs | FR-TEN-003, FR-TEN-016 |
| Dependencies | TENANT-US-002, cross-service: IDENT-EPIC-01 |
User story: As a Security Operator, when a tenant is suspended, I want verifiable session invalidation within JWT TTL so that suspended tenant users cannot continue accessing the platform.
Acceptance criteria (Gherkin):
- Given a suspension request, when processing completes, then
tenant.tenant.suspended.v1event is emitted and identity-service invalidation endpoint is called. - Given invalidation does not confirm within token TTL (15 min), when monitor checks, then SRE alert fires within 5 min.
- Given successful invalidation, when audit-service is queried, then suspension record includes tenant ID, actor ID, and completion timestamp.
Technical notes:
tenant.tenant.suspended.v1consumed by identity-service inbox; Redis revocation set populated.- Alert on identity consumer lag > 30 s.
Definition of Done:
- Suspension + invalidation E2E test passes in staging.
- Alert configured and tested.
TENANT-US-007 — Hierarchy node creation and tree management
| Field | Value |
|---|---|
| Issue type | Story |
| Summary | Tenant admin creates, updates, and archives org hierarchy nodes |
| Epic link | TENANT-EPIC-04 |
| Status | To Do |
| Priority | Must |
| Story points | 5 |
| Labels | service:tenant, type:backend, type:api, slice:S1 |
| Components | hierarchy-module |
| FR references | FR-TENANT-HIER-001..004 |
| Legacy FR refs | — |
| Dependencies | TENANT-US-002 |
User story: As a Tenant Admin, when setting up my org, I want to create departments, wards, and beds as hierarchy nodes under my facility so that users can be assigned to the correct clinical contexts.
Acceptance criteria (Gherkin):
- Given a valid parent node in same tenant, when
POST /api/v1/tenants/:id/nodesis called, then node created;tenant.hierarchy_node.created.v1emitted. - Given a parent node from a different tenant, when create is attempted, then
422 TENANT_NODE_CROSS_TENANTreturned. - Given an active node with children, when
POST /api/v1/tenants/:id/nodes/:nodeId/archiveis called, then node archived; children inheritarchivedflag;tenant.hierarchy_node.archived.v1emitted.
Technical notes:
- Node type constraints from HierarchyProfile (e.g., AFG_MOPH: organization → facility → department → ward).
- Ancestor chain cached in Redis 5 min; invalidated on node create/archive.
Definition of Done:
- Hierarchy integrity integration test (parent cross-tenant blocked) passes.
tenant.hierarchy_node.created.v1schema test green.
TENANT-US-008 — Ancestor chain query for license resolver
| Field | Value |
|---|---|
| Issue type | Story |
| Summary | Internal ancestor chain endpoint for identity licensing resolver |
| Epic link | TENANT-EPIC-04 |
| Status | To Do |
| Priority | Must |
| Story points | 3 |
| Labels | service:tenant, type:backend, type:api, slice:S1 |
| Components | hierarchy-module |
| FR references | FR-TENANT-HIER-005 |
| Legacy FR refs | — |
| Dependencies | TENANT-US-007 |
User story:
As the identity-service license resolver, when computing the effective license set for a node, I want to call GET /internal/tenant/nodes/:id/ancestors so that I can walk the hierarchy without direct database access.
Acceptance criteria (Gherkin):
- Given a node at depth 3, when ancestor endpoint is called, then ordered list of ancestor node IDs is returned (root first).
- Given cache warm, when repeated calls within 5 min, then response comes from Redis cache (trace shows cache hit).
- Given node archival event, when processed, then ancestor cache for affected subtree is invalidated.
Technical notes:
- Response cached
redis key: hierarchy:ancestors:{nodeId}TTL 5 min. - Cluster-internal IP restriction; no JWT required.
Definition of Done:
- Ancestor chain unit test covers root, mid-tree, and leaf nodes.
- Cache invalidation integration test passes.
TENANT-US-009 — User profile JIT creation and invite flow
| Field | Value |
|---|---|
| Issue type | Story |
| Summary | JIT user profile creation on identity event and tenant admin invite flow |
| Epic link | TENANT-EPIC-05 |
| Status | To Do |
| Priority | Must |
| Story points | 5 |
| Labels | service:tenant, type:backend, type:api, slice:S1 |
| Components | profile-module |
| FR references | FR-TENANT-USR-001..003 |
| Legacy FR refs | FR-IAM-USR-001 |
| Dependencies | TENANT-US-007, cross-service: IDENT-EPIC-01 |
User story: As a Tenant Admin, when I invite a new clinical user to my organization, I want their profile created in the system and an invitation email triggered so that they can complete onboarding.
Acceptance criteria (Gherkin):
- Given
identity.user.registered.v1event arrives for a user expected in the tenant, when inbox consumer processes it, thenUserProfileis created and linked touserId;tenant.user_profile.created.v1emitted. - Given a Tenant Admin POST to
/api/v1/tenants/:id/users, when called, then profile created;tenant.user.invited.v1emitted; communication-service sends invitation email. - Given a duplicate
(tenantId, userId)pair, when JIT creation runs again, then idempotent — no duplicate profile created.
Technical notes:
- Inbox consumer:
UserRegisteredConsumer— idempotent viaUNIQUE(tenant_id, user_id). tenant.user.invited.v1consumed by communication-service.
Definition of Done:
- Inbox idempotency integration test passes.
tenant.user_profile.created.v1schema test green.
TENANT-US-010 — Org membership assignment and removal
| Field | Value |
|---|---|
| Issue type | Story |
| Summary | Assign and remove users from hierarchy nodes with status lifecycle |
| Epic link | TENANT-EPIC-05 |
| Status | To Do |
| Priority | Must |
| Story points | 5 |
| Labels | service:tenant, type:backend, type:api, slice:S1 |
| Components | membership-module |
| FR references | FR-TENANT-USR-004..006 |
| Legacy FR refs | — |
| Dependencies | TENANT-US-009 |
User story: As a Tenant Admin, when managing staff placement, I want to assign users to specific departments and wards and remove them when they transfer so that authorization context stays accurate.
Acceptance criteria (Gherkin):
- Given an existing user profile, when
POST /api/v1/tenants/:id/users/:userId/membershipsis called withnodeId, then membership created;tenant.org_membership.created.v1emitted. - Given a membership, when
DELETE /api/v1/tenants/:id/users/:userId/memberships/:nodeIdis called, then status=removed;tenant.org_membership.removed.v1emitted. - Given
identity.user.deactivated.v1event, when consumed, then all memberships for user are removed idempotently; profile anonymized.
Technical notes:
UNIQUE(user_id, node_id)constraint prevents duplicate memberships.identity.user.deactivated.v1triggers cascade remove.
Definition of Done:
- GDPR erasure cascade integration test passes.
tenant.org_membership.removed.v1schema test green.
TENANT-US-011 — Built-in role seeding and custom role management
| Field | Value |
|---|---|
| Issue type | Story |
| Summary | Seed built-in roles and allow tenant admin to create custom roles |
| Epic link | TENANT-EPIC-06 |
| Status | To Do |
| Priority | Must |
| Story points | 3 |
| Labels | service:tenant, type:backend, type:api, slice:S0 |
| Components | rbac-module |
| FR references | FR-TENANT-ACC-001..003 |
| Legacy FR refs | FR-ACPOL-001..003 |
| Dependencies | TENANT-US-001 |
User story: As a Tenant Admin, when managing access controls, I want to use the platform's built-in clinical roles and optionally create custom roles for my organization so that permissions align with our workflows.
Acceptance criteria (Gherkin):
- Given a new active tenant, when activation completes, then built-in roles (
TENANT_ADMIN,CLINICIAN,NURSE,PATIENT) are seeded automatically. - Given a Tenant Admin, when
POST /api/v1/tenants/:id/rolesis called with a custom role definition, then role persisted; permissions validated against platform vocabulary. - Given a built-in role, when delete is attempted, then
422returned (INV-06).
Technical notes:
- Built-in roles:
is_builtin=true; domain invariant blocks deletion. - Permission strings:
{resource}:{action}(e.g.,patient_chart:read).
Definition of Done:
- Built-in role delete invariant integration test passes.
- Role seed integration test passes on activation.
TENANT-US-012 — Role assignment at node
| Field | Value |
|---|---|
| Issue type | Story |
| Summary | Assign and revoke role for user at a specific hierarchy node |
| Epic link | TENANT-EPIC-06 |
| Status | To Do |
| Priority | Must |
| Story points | 3 |
| Labels | service:tenant, type:backend, type:api, slice:S0 |
| Components | rbac-module |
| FR references | FR-TENANT-ACC-004..006 |
| Legacy FR refs | FR-ACPOL-004..006 |
| Dependencies | TENANT-US-011, TENANT-US-010 |
User story: As a Tenant Admin, when granting clinical privileges, I want to assign a role to a user at a specific node so that their access is scoped to the correct part of the org hierarchy.
Acceptance criteria (Gherkin):
- Given a user with membership at a node, when
POST /api/v1/tenants/:id/users/:userId/rolesis called withroleIdandnodeId, then role assignment created;tenant.role_assignment.created.v1emitted. - Given a user without membership at target node, when role assignment is attempted, then
422 TENANT_MEMBERSHIP_REQUIREDreturned. - Given an existing role assignment, when
DELETE /api/v1/tenants/:id/users/:userId/roles/:roleIdis called, then assignment removed;tenant.role_assignment.removed.v1emitted.
Technical notes:
UNIQUE(user_id, role_id, node_id)constraint.- Role change events consumed by identity-service access-context cache invalidation.
Definition of Done:
- Membership prerequisite invariant integration test passes.
tenant.role_assignment.created.v1schema test green.- access-context cache invalidation test passes (cross-service contract).
TENANT-US-013 — RBAC evaluate() decision endpoint
| Field | Value |
|---|---|
| Issue type | Story |
| Summary | POST /access/evaluate returns allow/deny decision for subject + resource + action |
| Epic link | TENANT-EPIC-06 |
| Status | To Do |
| Priority | Must |
| Story points | 5 |
| Labels | service:tenant, type:backend, type:api, slice:S0 |
| Components | rbac-module |
| FR references | FR-TENANT-ACC-007..009 |
| Legacy FR refs | FR-ACPOL-007..009 |
| Dependencies | TENANT-US-012 |
User story: As any platform service or UI, when checking authorization, I want to call a single evaluate() endpoint with subject, resource, and action so that I receive a deterministic allow/deny decision with reasoning.
Acceptance criteria (Gherkin):
- Given a CLINICIAN user with membership at a node, when
POST /api/v1/tenants/:id/access/evaluateis called withresource=patient_chart, action=read, then{ decision: "allow", reasons: [...] }. - Given same user at a node they are NOT a member of, when evaluate is called, then
{ decision: "deny", reasons: ["no membership at node"] }. - Given evaluate() p95 > 100 ms for 5 min, when alert fires, then SRE investigates DB query or Redis cache.
Technical notes:
- Deny-on-timeout policy (500 ms query timeout).
POST /internal/tenant/evaluatefor service-to-service calls (IP-restricted).
Definition of Done:
- RBAC evaluate unit tests cover allow, deny, and timeout paths.
- p95 latency < 100 ms in staging load test.
TENANT-US-014 — Quality gates and observability
| Field | Value |
|---|---|
| Issue type | Story |
| Summary | Coverage ≥ 80%, OTel instrumentation, activation SLO, and event publish alerting |
| Epic link | TENANT-EPIC-07 |
| Status | To Do |
| Priority | Must |
| Story points | 5 |
| Labels | service:tenant, type:ops, slice:S0 |
| Components | cross-cutting |
| FR references | FR-TENANT-NFR-001..003 |
| Legacy FR refs | NFR-TEN-001..003 |
| Dependencies | All TENANT-US |
User story: As an Engineering Lead, when releasing tenant-service, I want quality gates, observability, and activation SLOs to pass so that release risk is controlled and the platform's trust boundaries are verifiable.
Acceptance criteria (Gherkin):
- Given CI runs
pnpm test:cov, when pipeline completes, then zero failed suites; statements ≥ 80%; lines ≥ 80%. - Given activation path performance test, when p95 is calculated, then activation latency ≤ 500 ms excluding retry waits.
- Given event publisher metrics stream, when publish failure rate exceeds 1% over 5 min, then on-call alert emitted.
Technical notes:
@ghasi/telemetryinitialized inmain.tsbeforeNestFactory.- Saga steps instrumented with OpenTelemetry spans.
Definition of Done:
- Coverage gate green in CI.
- OTel traces visible in staging Grafana.
- SLO burn rate alerts configured.
TENANT-US-015 — Rate limiting and abuse protection
| Field | Value |
|---|---|
| Issue type | Story |
| Summary | Per-tenant rate limits on lifecycle endpoints to prevent control-plane abuse |
| Epic link | TENANT-EPIC-07 |
| Status | To Do |
| Priority | Should |
| Story points | 3 |
| Labels | service:tenant, type:ops, slice:S4 |
| Components | cross-cutting |
| FR references | FR-TENANT-NFR-004 |
| Legacy FR refs | NFR-TEN-ENH-001 |
| Dependencies | TENANT-US-014 |
User story: As an SRE, when lifecycle APIs are stressed by automation or misconfigured scripts, I want per-tenant rate limits so that noisy tenants cannot degrade platform control-plane stability for others.
Acceptance criteria (Gherkin):
- Given request rate exceeds per-tenant threshold, when Kong rate-limit plugin triggers, then
429returned withRetry-Afterheader. - Given request rate within threshold, when lifecycle API is called, then no throttle response.
- Given monitoring interval closes, when telemetry is queried, then per-tenant throttle metrics exported.
Technical notes:
- Kong rate-limit plugin:
POST /admin/tenants/:id/activate→ 5/min/IP. - General write endpoints: 120/min/tenant via Kong consumer group.
Definition of Done:
- Rate limit integration test passes.
- Per-tenant throttle metric visible in Grafana.