Skip to main content

tenant-service — SERVICE_OVERVIEW

Bounded context: Tenant & Org (Supporting → Core for chain customers) Companion bundle: DOMAIN_MODEL · APPLICATION_LOGIC · API_CONTRACTS · EVENT_SCHEMAS · DATA_MODEL · SECURITY_MODEL Platform context: 02 Enterprise Architecture · 05 API Design · 06 Data Models · 07 Security & Tenancy


1. Mission

tenant-service is the operator-of-record service for the Ghasi Melmastoon platform. It owns the answer to "who is this hotel business and which of its people may act on behalf of which property?" Every other service consumes its events, reads its tenant configuration, and depends on its membership decisions for ABAC scoping.

Concretely it is the source of truth for:

  1. Tenant lifecycle — provision, activate, suspend, reactivate, close.
  2. Tenant configuration — currencies, locales (with RTL/LTR), tax model, time zone, default cancellation policy, default check-in/out times, breakfast inclusion default, smoking and child policy, business hours.
  3. Organization structurechain → region → property tree (ltree), max depth 5, supports chain restructuring under saga.
  4. Staff membershipUser × Tenant relationship, status, optional per-property scope.
  5. RBAC — tenant-scoped role catalog (system + custom) plus RoleAssignments with optional property scope.
  6. Invitation flow — single-use invite tokens, 14-day TTL, email delivery via notification-service.
  7. Per-tenant feature flags — overrides on the platform-wide flag set.
  8. Billing contact — finance contact, tax IDs, invoicing address; billing-service consumes via event.

It explicitly does not own:

  • Credentials, password hashes, MFA factors, sessions, refresh tokens, device pairings → owned by iam-service.
  • Theme colors, logos, content blocks → owned by theme-config-service.
  • Plan catalogs, prices, subscriptions, invoices, payment methods → owned by billing-service and payment-gateway-service.
  • Property physical attributes (rooms, room types, amenities, photos) → owned by property-service. tenant-service only owns the org-tree node identity, not the property's domain content.

2. Bounded Context Card

AttributeValue
NameTenant & Org
ClassSupporting (Core for chain customers; the operator hierarchy is a competitive lever there)
Strategic patternConformist toward iam-service (consumes user identity); Open Host toward all other services (publishes well-versioned tenant events)
Aggregate rootsTenant, TenantConfig, OrganizationUnit, Membership, Role, RoleAssignment, Invitation, BillingContact, FeatureFlagOverride
Ubiquitous languageOperator, Property, Chain, Region, Membership, Role, Owner, GM, Front Desk, Housekeeping, Invitation, Org Unit
PersistenceCloud SQL Postgres 16, schema tenant, with ltree extension for org tree, RLS on every tenant-scoped table
Hot read storeMemorystore (Redis Standard tier), TTL 60 s for tenant config + membership, 600 s for role catalog
EventingGCP Pub/Sub topics under melmastoon.tenant.*; transactional outbox to ensure exactly-once-from-source semantics
Sync surfaceRead-only desktop projection; conflict policy server_authoritative for tenant config; role/membership writes require online
AI useLight. Consumes AIClient for invite-abuse classification + bulk-removal anomaly review. Always advisory.
Tenancy modeltenants table itself is platform-scoped (no tenant_id, only platform.super_admin reads); every other table is tenant_id-scoped with RLS enforced

3. Position in the System

┌────────────────────────┐
(creates user) │ iam-service │ (publishes user events)
───────────────► │ passwords / sessions │ ──────────────────────────┐
└────────────────────────┘ │

┌──────────────────────────────────────────────────────────────────────┐
│ tenant-service │
│ Tenant · TenantConfig · OrgUnit · Membership · Role · Invite │
│ (this doc) │
└──────────────────────────────────────────────────────────────────────┘
│ │ │ │
│ tenant.created │ config_updated │ membership.* │ org_unit.created
▼ ▼ ▼ ▼
┌──────────────┐ ┌──────────────┐ ┌──────────────┐ ┌──────────────────┐
│ billing-svc │ │ pricing-svc │ │ all services │ │ property-service │
│ theme-config │ │ reservation │ │ (caches) │ │ search-aggregation│
│ notification │ │ theme-config │ │ │ │ │
└──────────────┘ └──────────────┘ └──────────────┘ └──────────────────┘

tenant-service sits in the second ring of the architecture: it depends only on iam-service (user identity events) and billing-service (subscription state events). Every other downstream service depends on it. This makes it a strict tier-1 service in the boot order of the platform.


4. Synchronous Interactions

DirectionCounterpartEndpointPurposeFailure mode
Inboundevery serviceGET /api/v1/tenants/{id}/config (read-through cache)Resolve tenant config; called on cold-cache requests downstreamReturns stale-from-cache up to TTL_GRACE; never a hard fail
InboundgatewayPOST /api/v1/authz/checkABAC decision endpoint (cacheable PDP)Fail-closed on timeout > 50 ms
Inboundbff-backoffice-serviceGET /api/v1/me/tenants, GET /api/v1/me/membershipsBootstrapping the desktop after loginCan degrade to last sync snapshot
Outboundiam-servicePOST /api/v1/users (invite resolves to a pre-registration)Create or look up the user record paired with an invitationSaga-style; retries with idempotency key
Outboundnotification-serviceevent-driven only (no sync call)Send invitation emailDecoupled

All synchronous calls follow the platform's API design: Idempotency-Key on writes, If-Match on PATCH, ULID-prefixed IDs, Problem+JSON errors with MELMASTOON.<DOMAIN>.<CODE>.


5. Asynchronous Interactions

Pub/Sub topics published (versioned, retained ≥ 90 days for tenant lifecycle, ≥ 30 days for membership):

  • melmastoon.tenant.created.v1
  • melmastoon.tenant.suspended.v1 / melmastoon.tenant.reactivated.v1
  • melmastoon.tenant.deleted.v1
  • melmastoon.tenant.config_updated.v1
  • melmastoon.tenant.membership.created.v1 / …role_changed.v1 / …removed.v1
  • melmastoon.tenant.invitation.sent.v1 / …accepted.v1 / …expired.v1
  • melmastoon.tenant.guest.erasure_requested.v1
  • melmastoon.tenant.feature_flag.toggled.v1
  • melmastoon.tenant.organization_unit.created.v1

Subscribed:

  • melmastoon.iam.user.registered.v1 → match against open invitations, materialize membership.
  • melmastoon.iam.user.deleted.v1 → background sweep flips memberships to removed.
  • melmastoon.billing.subscription.cancelled.v1 → after 14-day grace, auto-suspend.
  • melmastoon.billing.subscription.reactivated.v1 → lift suspension.

All published events go through the transactional outbox (single transaction with the domain mutation) and are delivered by an outbox poller to Pub/Sub. Consumers are idempotent via (eventId, consumerName) inbox table.


6. Technology Stack

LayerTechNotes
RuntimeNode.js 20, NestJS 10 (TypeScript strict)Per platform standard
DomainPure TypeScript, zero framework deps@melmastoon/tenant-domain
DatabaseCloud SQL Postgres 16, schema tenantltree, pgcrypto, pg_trgm, pgaudit
CacheMemorystore (Redis Standard tier)TLS-only
EventingGCP Pub/Sub (push subs to Cloud Run, ordered keys per tenant)Outbox + inbox
SecretsSecret Manager (CMEK)Per-service service account
ContainerDistroless Node 20, multi-archBuilt by Cloud Build, signed via Binary Authorization
DeployCloud Run, regional + multi-region failover for the read APISee DEPLOYMENT_TOPOLOGY
ObservabilityOpenTelemetry → Cloud Trace, Cloud Monitoring, Cloud LoggingLoki/Tempo mirror in dev

7. Architectural Decisions

ADRDecisionReason
ADR-0001Clean / Hexagonal architecture; pure domain in TS, ports + adapters in NestJSKeeps domain testable; swappable infra
ADR-0002Shared schema + tenant_id + RLS for child tables; platform-scope for tenants itselfOperational simplicity; PCI carve-outs go to other services
ADR-0003Electron desktop with offline-first SQLite via sync-serviceTenant projection is read-only on desktop; no role mutations offline
Localltree for org treeNative ancestor queries; well-supported in Postgres
LocalSystem roles immutable per tenant; identity by codePredictable RBAC across tenants; safe to evolve via migration
LocalInvitation token = crypto.randomBytes(32); only SHA-256 hash storedToken cannot be reconstructed from DB
LocalRoleAssignment.propertyScope[] lives on the assignment, not the roleSame role can be scoped differently per assignee

8. Readiness Targets (NFR)

ClassMetricTarget
AvailabilityMonthly99.95 % (every other service depends on us being up)
Latency (read, cached)tenant.resolve p95≤ 5 ms
Latency (read, db)tenant.config p95≤ 25 ms
Latency (write)PATCH /tenants/{id}/config p95≤ 200 ms
ThroughputMembership resolution≥ 5 000 rps per Cloud Run instance
Event delivery lagOutbox → Pub/Sub p95≤ 2 s
RPO / RTOCloud SQL HA + PITR1 min / 5 min
Tenant isolationTwo-tenant simulator100 % green per CI run
SecurityOWASP ASVS L2achieved for v1.0; ASVS L3 by v1.5

9. Freeze Points (immutable contracts)

The following are explicitly versioned and may only evolve through additive change after v1.0:

  • Event subjects + payload schemas in EVENT_SCHEMAS.
  • REST resource paths in API_CONTRACTS.
  • ID prefixes (tnt_, mbr_, org_, rol_, inv_, flg_).
  • Error code namespace MELMASTOON.TENANT.* and MELMASTOON.MEMBERSHIP.*.
  • System role code strings (tenant.owner, tenant.gm, …).

10. What Lives Elsewhere

ConcernOwnerWhy not here
Password hashes, MFA, refresh tokensiam-serviceIdentity store is shared across tenants; tenant-service has no business with credentials
Property rooms, photos, amenitiesproperty-serviceHotel domain inventory; org-unit only carries identity
Theme tokens, logos, content blockstheme-config-serviceVisual presentation; consumed by booking surfaces
Subscription, invoices, payment methodsbilling-service, payment-gateway-servicePCI scope minimization (schema-per-tenant carve-out)
Cross-tenant search projectionsearch-aggregation-serviceOnly legitimate cross-tenant store on the platform
Audit logaudit-service (BigQuery sink)We emit audit events, but the long-term store is centralized

See the Microservices Catalog for the full map.