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:
- Tenant lifecycle — provision, activate, suspend, reactivate, close.
- 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.
- Organization structure —
chain → region → propertytree (ltree), max depth 5, supports chain restructuring under saga. - Staff membership —
User × Tenantrelationship, status, optional per-property scope. - RBAC — tenant-scoped role catalog (system + custom) plus
RoleAssignments with optional property scope. - Invitation flow — single-use invite tokens, 14-day TTL, email delivery via
notification-service. - Per-tenant feature flags — overrides on the platform-wide flag set.
- Billing contact — finance contact, tax IDs, invoicing address;
billing-serviceconsumes 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-serviceandpayment-gateway-service. - Property physical attributes (rooms, room types, amenities, photos) → owned by
property-service.tenant-serviceonly owns the org-tree node identity, not the property's domain content.
2. Bounded Context Card
| Attribute | Value |
|---|---|
| Name | Tenant & Org |
| Class | Supporting (Core for chain customers; the operator hierarchy is a competitive lever there) |
| Strategic pattern | Conformist toward iam-service (consumes user identity); Open Host toward all other services (publishes well-versioned tenant events) |
| Aggregate roots | Tenant, TenantConfig, OrganizationUnit, Membership, Role, RoleAssignment, Invitation, BillingContact, FeatureFlagOverride |
| Ubiquitous language | Operator, Property, Chain, Region, Membership, Role, Owner, GM, Front Desk, Housekeeping, Invitation, Org Unit |
| Persistence | Cloud SQL Postgres 16, schema tenant, with ltree extension for org tree, RLS on every tenant-scoped table |
| Hot read store | Memorystore (Redis Standard tier), TTL 60 s for tenant config + membership, 600 s for role catalog |
| Eventing | GCP Pub/Sub topics under melmastoon.tenant.*; transactional outbox to ensure exactly-once-from-source semantics |
| Sync surface | Read-only desktop projection; conflict policy server_authoritative for tenant config; role/membership writes require online |
| AI use | Light. Consumes AIClient for invite-abuse classification + bulk-removal anomaly review. Always advisory. |
| Tenancy model | tenants 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
| Direction | Counterpart | Endpoint | Purpose | Failure mode |
|---|---|---|---|---|
| Inbound | every service | GET /api/v1/tenants/{id}/config (read-through cache) | Resolve tenant config; called on cold-cache requests downstream | Returns stale-from-cache up to TTL_GRACE; never a hard fail |
| Inbound | gateway | POST /api/v1/authz/check | ABAC decision endpoint (cacheable PDP) | Fail-closed on timeout > 50 ms |
| Inbound | bff-backoffice-service | GET /api/v1/me/tenants, GET /api/v1/me/memberships | Bootstrapping the desktop after login | Can degrade to last sync snapshot |
| Outbound | iam-service | POST /api/v1/users (invite resolves to a pre-registration) | Create or look up the user record paired with an invitation | Saga-style; retries with idempotency key |
| Outbound | notification-service | event-driven only (no sync call) | Send invitation email | Decoupled |
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.v1melmastoon.tenant.suspended.v1/melmastoon.tenant.reactivated.v1melmastoon.tenant.deleted.v1melmastoon.tenant.config_updated.v1melmastoon.tenant.membership.created.v1/…role_changed.v1/…removed.v1melmastoon.tenant.invitation.sent.v1/…accepted.v1/…expired.v1melmastoon.tenant.guest.erasure_requested.v1melmastoon.tenant.feature_flag.toggled.v1melmastoon.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 toremoved.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
| Layer | Tech | Notes |
|---|---|---|
| Runtime | Node.js 20, NestJS 10 (TypeScript strict) | Per platform standard |
| Domain | Pure TypeScript, zero framework deps | @melmastoon/tenant-domain |
| Database | Cloud SQL Postgres 16, schema tenant | ltree, pgcrypto, pg_trgm, pgaudit |
| Cache | Memorystore (Redis Standard tier) | TLS-only |
| Eventing | GCP Pub/Sub (push subs to Cloud Run, ordered keys per tenant) | Outbox + inbox |
| Secrets | Secret Manager (CMEK) | Per-service service account |
| Container | Distroless Node 20, multi-arch | Built by Cloud Build, signed via Binary Authorization |
| Deploy | Cloud Run, regional + multi-region failover for the read API | See DEPLOYMENT_TOPOLOGY |
| Observability | OpenTelemetry → Cloud Trace, Cloud Monitoring, Cloud Logging | Loki/Tempo mirror in dev |
7. Architectural Decisions
| ADR | Decision | Reason |
|---|---|---|
| ADR-0001 | Clean / Hexagonal architecture; pure domain in TS, ports + adapters in NestJS | Keeps domain testable; swappable infra |
| ADR-0002 | Shared schema + tenant_id + RLS for child tables; platform-scope for tenants itself | Operational simplicity; PCI carve-outs go to other services |
| ADR-0003 | Electron desktop with offline-first SQLite via sync-service | Tenant projection is read-only on desktop; no role mutations offline |
| Local | ltree for org tree | Native ancestor queries; well-supported in Postgres |
| Local | System roles immutable per tenant; identity by code | Predictable RBAC across tenants; safe to evolve via migration |
| Local | Invitation token = crypto.randomBytes(32); only SHA-256 hash stored | Token cannot be reconstructed from DB |
| Local | RoleAssignment.propertyScope[] lives on the assignment, not the role | Same role can be scoped differently per assignee |
8. Readiness Targets (NFR)
| Class | Metric | Target |
|---|---|---|
| Availability | Monthly | 99.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 |
| Throughput | Membership resolution | ≥ 5 000 rps per Cloud Run instance |
| Event delivery lag | Outbox → Pub/Sub p95 | ≤ 2 s |
| RPO / RTO | Cloud SQL HA + PITR | 1 min / 5 min |
| Tenant isolation | Two-tenant simulator | 100 % green per CI run |
| Security | OWASP ASVS L2 | achieved 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.*andMELMASTOON.MEMBERSHIP.*. - System role
codestrings (tenant.owner,tenant.gm, …).
10. What Lives Elsewhere
| Concern | Owner | Why not here |
|---|---|---|
| Password hashes, MFA, refresh tokens | iam-service | Identity store is shared across tenants; tenant-service has no business with credentials |
| Property rooms, photos, amenities | property-service | Hotel domain inventory; org-unit only carries identity |
| Theme tokens, logos, content blocks | theme-config-service | Visual presentation; consumed by booking surfaces |
| Subscription, invoices, payment methods | billing-service, payment-gateway-service | PCI scope minimization (schema-per-tenant carve-out) |
| Cross-tenant search projection | search-aggregation-service | Only legitimate cross-tenant store on the platform |
| Audit log | audit-service (BigQuery sink) | We emit audit events, but the long-term store is centralized |
See the Microservices Catalog for the full map.