Skip to main content

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

FieldValue
Issue typeStory
SummarySuper admin creates tenant, reactivates after suspension, and updates subscription
Epic linkTENANT-EPIC-01
StatusTo Do
PriorityMust
Story points5
Labelsservice:tenant, type:backend, type:api, slice:S0
Componentslifecycle-module
FR referencesFR-TENANT-001, FR-TENANT-004, FR-TENANT-011, FR-TENANT-013
Legacy FR refsFR-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/tenants is called, then tenant is persisted with status=pending; tenant.tenant.created.v1 emitted.
  • Given a suspended tenant, when POST /api/v1/admin/tenants/:id/reactivate is called, then status becomes active; tenant.tenant.reactivated.v1 emitted.
  • Given a valid subscription patch, when PATCH /api/v1/admin/tenants/:id/subscription is called, then tier and dates are updated; tenant.subscription.updated.v1 emitted exactly once.
  • Given a duplicate slug, when create is attempted, then 409 TENANT_SLUG_DUPLICATE returned.

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.v1 schema test green.
  • Coverage ≥ 80%.

TENANT-US-002 — Tenant activate, suspend, terminate, and expiry

FieldValue
Issue typeStory
SummaryOrchestrated activation, suspension with IAM invalidation, and termination
Epic linkTENANT-EPIC-01
StatusTo Do
PriorityMust
Story points8
Labelsservice:tenant, type:backend, type:api, slice:S0
Componentslifecycle-module, activation-saga
FR referencesFR-TENANT-002, FR-TENANT-003, FR-TENANT-005, FR-TENANT-006, FR-TENANT-012
Legacy FR refsFR-TEN-002, FR-TEN-003, FR-TEN-005, FR-TEN-006, FR-TEN-012
DependenciesTENANT-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 pending tenant, when POST /api/v1/admin/tenants/:id/activate succeeds, then status=active, root_node_id set, tenant.tenant.activated.v1 emitted.
  • Given an active tenant, when POST /api/v1/admin/tenants/:id/suspend is called, then status=suspended; tenant.tenant.suspended.v1 emitted; identity-service invalidates sessions within token TTL.
  • Given subscription end_date <= today, when cron runs, then tenant.subscription.expired.v1 emitted 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 PENDING on 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.v1 and tenant.tenant.suspended.v1 schema tests green.

TENANT-US-003 — Tenant profile and config KV management

FieldValue
Issue typeStory
SummaryTenant admin updates profile and governed config key-value pairs
Epic linkTENANT-EPIC-02
StatusTo Do
PriorityMust
Story points3
Labelsservice:tenant, type:backend, type:api, slice:S0
Componentsconfig-module
FR referencesFR-TENANT-007, FR-TENANT-008
Legacy FR refsFR-TEN-007, FR-TEN-008
DependenciesTENANT-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/:id is called, then profile fields update; tenant.tenant.updated.v1 emitted.
  • Given a config key outside the allow-list, when PUT /api/v1/tenants/:id/config/:key is called, then 400 TENANT_CONFIG_KEY_UNKNOWN returned.
  • Given a valid allowed key, when value is set or changed, then tenant.config.changed.v1 emitted including operation and key.

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.v1 schema test green.

TENANT-US-004 — Country-to-profile default and tenant isolation

FieldValue
Issue typeStory
SummaryCountry determines default hierarchy profile; cross-tenant access blocked
Epic linkTENANT-EPIC-02
StatusTo Do
PriorityMust
Story points3
Labelsservice:tenant, type:backend, slice:S0
Componentsconfig-module
FR referencesFR-TENANT-009
Legacy FR refsFR-TEN-009
DependenciesTENANT-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=AF and no explicit profile, when activation runs, then hierarchy_profile_id=AFG_MOPH is used.
  • Given country without direct mapping, when profile resolves, then PRIVATE_HOSPITAL fallback is applied.
  • Given Tenant Admin requesting data for a different tenant ID, when endpoint is called, then 403 TENANT_CROSS_TENANT returned.

Technical notes:

  • Profile map: AF → AFG_MOPH, AE → UAE_DOH, * → PRIVATE_HOSPITAL.
  • RLS policy enforced; mandatory tenant-isolation.spec.ts test.

Definition of Done:

  • Tenant-isolation integration test passes.
  • Profile resolution unit test covers all mapping cases.

TENANT-US-005 — Activation idempotency token

FieldValue
Issue typeStory
SummaryIdempotency-Key on activation prevents duplicate downstream side effects
Epic linkTENANT-EPIC-03
StatusTo Do
PriorityMust
Story points3
Labelsservice:tenant, type:backend, slice:S0
Componentsactivation-saga
FR referencesFR-TENANT-ENH-001
Legacy FR refsFR-TEN-ENH-001
DependenciesTENANT-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-Key and 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_MISMATCH returned.
  • Given first-time token, when activation succeeds, then token record stored with tenant ID and 24h expiry.

Technical notes:

  • Idempotency key stored in idempotency_keys table (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

FieldValue
Issue typeStory
SummaryVerify IAM session invalidation is measurable and auditable after suspension
Epic linkTENANT-EPIC-03
StatusTo Do
PriorityMust
Story points5
Labelsservice:tenant, type:backend, type:ops, slice:S0
Componentslifecycle-module
FR referencesFR-TENANT-003
Legacy FR refsFR-TEN-003, FR-TEN-016
DependenciesTENANT-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.v1 event 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.v1 consumed 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

FieldValue
Issue typeStory
SummaryTenant admin creates, updates, and archives org hierarchy nodes
Epic linkTENANT-EPIC-04
StatusTo Do
PriorityMust
Story points5
Labelsservice:tenant, type:backend, type:api, slice:S1
Componentshierarchy-module
FR referencesFR-TENANT-HIER-001..004
Legacy FR refs
DependenciesTENANT-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/nodes is called, then node created; tenant.hierarchy_node.created.v1 emitted.
  • Given a parent node from a different tenant, when create is attempted, then 422 TENANT_NODE_CROSS_TENANT returned.
  • Given an active node with children, when POST /api/v1/tenants/:id/nodes/:nodeId/archive is called, then node archived; children inherit archived flag; tenant.hierarchy_node.archived.v1 emitted.

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.v1 schema test green.

TENANT-US-008 — Ancestor chain query for license resolver

FieldValue
Issue typeStory
SummaryInternal ancestor chain endpoint for identity licensing resolver
Epic linkTENANT-EPIC-04
StatusTo Do
PriorityMust
Story points3
Labelsservice:tenant, type:backend, type:api, slice:S1
Componentshierarchy-module
FR referencesFR-TENANT-HIER-005
Legacy FR refs
DependenciesTENANT-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

FieldValue
Issue typeStory
SummaryJIT user profile creation on identity event and tenant admin invite flow
Epic linkTENANT-EPIC-05
StatusTo Do
PriorityMust
Story points5
Labelsservice:tenant, type:backend, type:api, slice:S1
Componentsprofile-module
FR referencesFR-TENANT-USR-001..003
Legacy FR refsFR-IAM-USR-001
DependenciesTENANT-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.v1 event arrives for a user expected in the tenant, when inbox consumer processes it, then UserProfile is created and linked to userId; tenant.user_profile.created.v1 emitted.
  • Given a Tenant Admin POST to /api/v1/tenants/:id/users, when called, then profile created; tenant.user.invited.v1 emitted; 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 via UNIQUE(tenant_id, user_id).
  • tenant.user.invited.v1 consumed by communication-service.

Definition of Done:

  • Inbox idempotency integration test passes.
  • tenant.user_profile.created.v1 schema test green.

TENANT-US-010 — Org membership assignment and removal

FieldValue
Issue typeStory
SummaryAssign and remove users from hierarchy nodes with status lifecycle
Epic linkTENANT-EPIC-05
StatusTo Do
PriorityMust
Story points5
Labelsservice:tenant, type:backend, type:api, slice:S1
Componentsmembership-module
FR referencesFR-TENANT-USR-004..006
Legacy FR refs
DependenciesTENANT-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/memberships is called with nodeId, then membership created; tenant.org_membership.created.v1 emitted.
  • Given a membership, when DELETE /api/v1/tenants/:id/users/:userId/memberships/:nodeId is called, then status=removed; tenant.org_membership.removed.v1 emitted.
  • Given identity.user.deactivated.v1 event, 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.v1 triggers cascade remove.

Definition of Done:

  • GDPR erasure cascade integration test passes.
  • tenant.org_membership.removed.v1 schema test green.

TENANT-US-011 — Built-in role seeding and custom role management

FieldValue
Issue typeStory
SummarySeed built-in roles and allow tenant admin to create custom roles
Epic linkTENANT-EPIC-06
StatusTo Do
PriorityMust
Story points3
Labelsservice:tenant, type:backend, type:api, slice:S0
Componentsrbac-module
FR referencesFR-TENANT-ACC-001..003
Legacy FR refsFR-ACPOL-001..003
DependenciesTENANT-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/roles is called with a custom role definition, then role persisted; permissions validated against platform vocabulary.
  • Given a built-in role, when delete is attempted, then 422 returned (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

FieldValue
Issue typeStory
SummaryAssign and revoke role for user at a specific hierarchy node
Epic linkTENANT-EPIC-06
StatusTo Do
PriorityMust
Story points3
Labelsservice:tenant, type:backend, type:api, slice:S0
Componentsrbac-module
FR referencesFR-TENANT-ACC-004..006
Legacy FR refsFR-ACPOL-004..006
DependenciesTENANT-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/roles is called with roleId and nodeId, then role assignment created; tenant.role_assignment.created.v1 emitted.
  • Given a user without membership at target node, when role assignment is attempted, then 422 TENANT_MEMBERSHIP_REQUIRED returned.
  • Given an existing role assignment, when DELETE /api/v1/tenants/:id/users/:userId/roles/:roleId is called, then assignment removed; tenant.role_assignment.removed.v1 emitted.

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.v1 schema test green.
  • access-context cache invalidation test passes (cross-service contract).

TENANT-US-013 — RBAC evaluate() decision endpoint

FieldValue
Issue typeStory
SummaryPOST /access/evaluate returns allow/deny decision for subject + resource + action
Epic linkTENANT-EPIC-06
StatusTo Do
PriorityMust
Story points5
Labelsservice:tenant, type:backend, type:api, slice:S0
Componentsrbac-module
FR referencesFR-TENANT-ACC-007..009
Legacy FR refsFR-ACPOL-007..009
DependenciesTENANT-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/evaluate is called with resource=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/evaluate for 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

FieldValue
Issue typeStory
SummaryCoverage ≥ 80%, OTel instrumentation, activation SLO, and event publish alerting
Epic linkTENANT-EPIC-07
StatusTo Do
PriorityMust
Story points5
Labelsservice:tenant, type:ops, slice:S0
Componentscross-cutting
FR referencesFR-TENANT-NFR-001..003
Legacy FR refsNFR-TEN-001..003
DependenciesAll 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/telemetry initialized in main.ts before NestFactory.
  • 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

FieldValue
Issue typeStory
SummaryPer-tenant rate limits on lifecycle endpoints to prevent control-plane abuse
Epic linkTENANT-EPIC-07
StatusTo Do
PriorityShould
Story points3
Labelsservice:tenant, type:ops, slice:S4
Componentscross-cutting
FR referencesFR-TENANT-NFR-004
Legacy FR refsNFR-TEN-ENH-001
DependenciesTENANT-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 429 returned with Retry-After header.
  • 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.