Skip to main content

tenant-service — EVENT_SCHEMAS

Cross-cutting event conventions live in 04 Event-Driven Architecture. This file lists service-specific event subjects, payload schemas, retention, ordering keys, and consumer expectations. Naming follows melmastoon.<service>.<aggregate>.<verb-past>.v<n> per NAMING.md.

All events use the standard envelope; only the data field varies per event. The transport is GCP Pub/Sub. Topics are created at deploy time by Terraform.


1. Envelope (recap)

{
"specVersion": "1.0",
"id": "evt_01H8YN7Q2P7GZ4F8Y5CK4MV3DT",
"source": "tenant-service",
"type": "melmastoon.tenant.created.v1",
"subject": "tnt_01H7…",
"tenantId": "tnt_01H7…",
"time": "2026-04-22T08:00:00Z",
"traceparent": "00-…-…-01",
"causationId": "evt_…",
"correlationId": "req_…",
"actor": { "type": "user", "id": "usr_01H…" },
"schema": "https://schemas.melmastoon.ghasi.io/tenant/created/v1.json",
"data": { /* per-event */ }
}

tenantId is mandatory on every published event (used as Pub/Sub orderingKey to keep per-tenant in-order delivery).


2. Topic Configuration

TopicOrderingRetentionDead-letterNotes
melmastoon.tenant.created.v1by tenant_id90 d…dlq.v1 after 5 attemptsTier-1 — every service subscribes
melmastoon.tenant.suspended.v1 / …reactivated.v1by tenant_id90 dyesDrives downstream write-blocks
melmastoon.tenant.deleted.v1by tenant_id365 dyesCascade saga; retained for compliance
melmastoon.tenant.config_updated.v1by tenant_id30 dyesHigh volume; cache-invalidation trigger
melmastoon.tenant.membership.*.v1by tenant_id30 dyesiam-service revokes sessions on removed / suspended
melmastoon.tenant.invitation.*.v1by tenant_id30 dyesnotification-service consumes sent
melmastoon.tenant.organization_unit.*.v1by tenant_id30 dyesproperty-service + search-aggregation consume
melmastoon.tenant.feature_flag.toggled.v1by tenant_id30 dyesall services with feature-gated paths consume
melmastoon.tenant.guest.erasure_requested.v1by tenant_id365 d (compliance)yesDSAR fan-out
melmastoon.tenant.billing_contact_updated.v1by tenant_id30 dyesbilling-service consumes

DLQ topics route to audit-service and PagerDuty.


3. Published Events — Payload Schemas

3.1 melmastoon.tenant.created.v1

{
"tenantId": "tnt_01H…",
"slug": "asia-hotel",
"legalName": "Asia Hotel Co. Ltd.",
"country": "AF",
"residencyRegion": "asia-south1",
"status": "pending",
"ownerUserId": "usr_01H…",
"rootOrganizationUnitId": "org_01H…",
"createdAt": "2026-04-22T08:00:00Z"
}

3.2 melmastoon.tenant.suspended.v1

{
"tenantId": "tnt_01H…",
"previousStatus": "active",
"reason": "billing.subscription_cancelled",
"by": "billing",
"suspendedAt": "2026-04-22T08:00:00Z",
"writesBlocked": true
}

3.3 melmastoon.tenant.reactivated.v1

{
"tenantId": "tnt_01H…",
"previousStatus": "suspended",
"by": "platform",
"note": "manual_reinstatement",
"reactivatedAt": "2026-04-22T08:00:00Z"
}

3.4 melmastoon.tenant.deleted.v1

{
"tenantId": "tnt_01H…",
"reason": "operator_request",
"sagaId": "sag_01H…",
"expectedAcksFrom": [
"billing-service", "payment-gateway-service",
"reservation-service", "property-service",
"housekeeping-service", "lock-integration-service",
"theme-config-service", "notification-service",
"iam-service", "search-aggregation-service"
],
"ackDeadline": "2026-04-29T08:00:00Z",
"closedAt": "2026-04-22T08:00:00Z"
}

Consumers must ack via melmastoon.tenant.deletion_acked.v1 (a synthetic event consumed by the saga). Failure to ack raises an SLO alert and pages the platform on-call.

3.5 melmastoon.tenant.config_updated.v1

{
"tenantId": "tnt_01H…",
"version": 13,
"previousVersion": 12,
"changedFields": ["currencies", "defaultCheckIn"],
"updatedAt": "2026-04-22T08:00:00Z",
"snapshot": {
"currencies": ["AFN", "USD", "EUR"],
"locales": [{ "value": "fa-AF", "isRtl": true }, { "value": "en-US", "isRtl": false }],
"timeZone": "Asia/Kabul",
"taxModel": { "inclusive": false, "defaultRateBasisPoints": 1500 },
"defaultCheckIn": "15:00",
"defaultCheckOut": "12:00",
"breakfastIncludedDefault": true,
"smokingPolicy": "designated",
"childPolicy": { "minAge": 0, "cribsAvailable": true },
"cancellationDefault": { "windowHours": 48, "chargeOnLateCancelMicro": "0", "noShowChargeMicro": "1000000" }
}
}

snapshot is included so that downstream consumers do not need a sync REST call to refresh their cache. Marked as PII-safe (no guest data).

3.6 melmastoon.tenant.membership.created.v1

{
"membershipId": "mbr_01H…",
"tenantId": "tnt_01H…",
"userId": "usr_01H…",
"displayName": "Sara Ahmadi",
"status": "active",
"propertyScope": ["org_01H…"],
"rolesGranted": [{ "roleId": "rol_01H…", "code": "tenant.front_desk" }],
"invitationId": "inv_01H…",
"createdAt": "2026-04-22T10:13:00Z"
}

3.7 melmastoon.tenant.membership.role_changed.v1

{
"membershipId": "mbr_01H…",
"tenantId": "tnt_01H…",
"userId": "usr_01H…",
"added": [{ "assignmentId": "rla_01H…", "roleId": "rol_01H…", "code": "tenant.gm", "propertyScope": [] }],
"removed": [{ "assignmentId": "rla_01H…", "roleId": "rol_01H…", "code": "tenant.front_desk" }],
"by": "usr_01H…",
"changedAt": "2026-04-22T10:14:00Z",
"version": 5
}

3.8 melmastoon.tenant.membership.removed.v1

{
"membershipId": "mbr_01H…",
"tenantId": "tnt_01H…",
"userId": "usr_01H…",
"reason": "policy.disciplinary",
"by": "usr_01H…",
"removedAt": "2026-04-22T10:15:00Z",
"revokeSessions": true
}

revokeSessions = true instructs iam-service to invalidate active sessions for (tenantId, userId).

3.9 melmastoon.tenant.invitation.sent.v1

{
"invitationId": "inv_01H…",
"tenantId": "tnt_01H…",
"email": "newhire@asiahotel.af",
"rolesProposed": [{ "roleId": "rol_01H…", "code": "tenant.front_desk" }],
"propertyScope": ["org_01H…"],
"invitedBy": "usr_01H…",
"expiresAt": "2026-05-06T08:00:00Z",
"locale": "fa-AF",
"redemptionUrl": "https://app.melmastoon.ghasi.io/invite/{token}",
"tokenDelivery": "email_only_payload_redacted_in_logs",
"attempt": 1
}

The raw token never appears in the persisted event payload; notification-service receives it via the synchronous notification API call (or via a separate, ephemeral side-channel topic with 60-second retention) and stitches it into the email. The published event is sanitized for audit safety.

3.10 melmastoon.tenant.invitation.accepted.v1

{
"invitationId": "inv_01H…",
"tenantId": "tnt_01H…",
"email": "newhire@asiahotel.af",
"userId": "usr_01H…",
"membershipId": "mbr_01H…",
"acceptedAt": "2026-04-22T10:13:00Z"
}

3.11 melmastoon.tenant.invitation.expired.v1

{
"invitationId": "inv_01H…",
"tenantId": "tnt_01H…",
"email": "newhire@asiahotel.af",
"expiredAt": "2026-05-06T08:00:00Z"
}

3.12 melmastoon.tenant.guest.erasure_requested.v1

{
"requestId": "ers_01H…",
"tenantId": "tnt_01H…",
"guestId": "gst_01H…",
"reason": "gdpr_request",
"requestedBy": "usr_01H…",
"requestedAt": "2026-04-22T08:00:00Z",
"expectedAcksFrom": ["reservation-service", "billing-service", "notification-service", "lock-integration-service"]
}

3.13 melmastoon.tenant.feature_flag.toggled.v1

{
"tenantId": "tnt_01H…",
"flagKey": "aiEnabled",
"previous": { "enabled": false, "rolloutBasisPoints": 0 },
"current": { "enabled": true, "rolloutBasisPoints": 5000 },
"by": "usr_01H…",
"changedAt": "2026-04-22T08:00:00Z"
}

3.14 melmastoon.tenant.organization_unit.created.v1

{
"organizationUnitId": "org_01H…",
"tenantId": "tnt_01H…",
"kind": "property",
"parentId": "org_01H…",
"path": "chain_root.kabul.asia_hotel",
"name": "Hotel Asia Kabul",
"propertyId": "ppt_01H…",
"createdAt": "2026-04-22T08:00:00Z"
}

Companion subjects: …organization_unit.moved.v1, …organization_unit.archived.v1.

3.15 melmastoon.tenant.billing_contact_updated.v1

{
"tenantId": "tnt_01H…",
"billingContactId": "bcn_01H…",
"fullName": "Sara Ahmadi",
"email": "sara@asiahotel.af",
"phone": "+93770000000",
"address": { "country": "AF", "city": "Kabul", "line1": "Shar-e-Naw" },
"taxId": null,
"version": 2,
"updatedAt": "2026-04-22T08:00:00Z"
}

4. Consumed Events

4.1 melmastoon.iam.user.registered.v1

{
"userId": "usr_01H…",
"email": "newhire@asiahotel.af",
"registeredAt": "2026-04-22T10:00:00Z",
"via": "magic_link"
}

Effect: scan invitations where email = data.email AND status = 'pending'. For each match, attempt acceptance via the AcceptInvitation use case (idempotent).

4.2 melmastoon.iam.user.deleted.v1

{ "userId": "usr_01H…", "deletedAt": "2026-04-22T08:00:00Z", "reason": "account_closure" }

Effect: flip every membership for that user across all tenants to removed; emit membership.removed.v1 per tenant.

4.3 melmastoon.billing.subscription.cancelled.v1

{
"tenantId": "tnt_01H…",
"subscriptionId": "sub_…",
"cancelledAt": "2026-04-22T08:00:00Z",
"gracePeriodEndsAt": "2026-05-06T08:00:00Z",
"reason": "involuntary_payment_failure"
}

Effect: schedule SuspendTenant for gracePeriodEndsAt if no subscription.reactivated arrives by then.

4.4 melmastoon.billing.subscription.reactivated.v1

{ "tenantId": "tnt_01H…", "subscriptionId": "sub_…", "reactivatedAt": "2026-04-22T08:00:00Z" }

Effect: cancel any pending suspend job; if tenant is currently suspended with by = 'billing', invoke ReactivateTenant immediately.

4.5 melmastoon.tenant.deletion_acked.v1 (synthetic, consumed by saga)

{
"sagaId": "sag_01H…",
"tenantId": "tnt_01H…",
"service": "billing-service",
"ackedAt": "2026-04-22T08:00:00Z",
"outcome": "ok"
}

5. Schema Evolution Rules

  • Additive only within a major version. New optional fields allowed; never remove or rename existing fields.
  • Bump major (.v2) when removing or renaming a field, changing semantics of a value, or changing required fields. Both .v1 and .v2 topics run side-by-side for at least 90 days with the producer dual-publishing.
  • All schemas are JSON Schema, published at https://schemas.melmastoon.ghasi.io/tenant/<event>/v<n>.json and committed to event-schemas/ in this repo. CI fails any breaking change without a major bump.
  • Consumers MUST ignore unknown fields (forward-compat).

6. Consumer Expectations

ConsumerSubscribes toEffect
All servicestenant.created.v1Provision per-tenant resources (RLS row, default settings)
All servicestenant.suspended.v1 / tenant.reactivated.v1Toggle write-block at gateway
All servicestenant.config_updated.v1Refresh local TenantConfig cache; recompute defaults
iam-servicetenant.membership.suspended.v1 / …removed.v1Revoke active sessions for that user in that tenant
notification-servicetenant.invitation.sent.v1Render and send the email (locale-aware)
billing-servicetenant.billing_contact_updated.v1Update invoice header
property-servicetenant.organization_unit.created.v1 (kind=property)Sanity-check that propertyId resolves
search-aggregation-servicetenant.organization_unit.*Update org-tree projection
every service that holds guest datatenant.guest.erasure_requested.v1Perform local erasure + emit ack
analytics-serviceevery tenant.*Sink to BigQuery
audit-serviceevery tenant.*Append to immutable audit chain