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
| Topic | Ordering | Retention | Dead-letter | Notes |
|---|---|---|---|---|
melmastoon.tenant.created.v1 | by tenant_id | 90 d | …dlq.v1 after 5 attempts | Tier-1 — every service subscribes |
melmastoon.tenant.suspended.v1 / …reactivated.v1 | by tenant_id | 90 d | yes | Drives downstream write-blocks |
melmastoon.tenant.deleted.v1 | by tenant_id | 365 d | yes | Cascade saga; retained for compliance |
melmastoon.tenant.config_updated.v1 | by tenant_id | 30 d | yes | High volume; cache-invalidation trigger |
melmastoon.tenant.membership.*.v1 | by tenant_id | 30 d | yes | iam-service revokes sessions on removed / suspended |
melmastoon.tenant.invitation.*.v1 | by tenant_id | 30 d | yes | notification-service consumes sent |
melmastoon.tenant.organization_unit.*.v1 | by tenant_id | 30 d | yes | property-service + search-aggregation consume |
melmastoon.tenant.feature_flag.toggled.v1 | by tenant_id | 30 d | yes | all services with feature-gated paths consume |
melmastoon.tenant.guest.erasure_requested.v1 | by tenant_id | 365 d (compliance) | yes | DSAR fan-out |
melmastoon.tenant.billing_contact_updated.v1 | by tenant_id | 30 d | yes | billing-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.v1and.v2topics 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>.jsonand committed toevent-schemas/in this repo. CI fails any breaking change without a major bump. - Consumers MUST ignore unknown fields (forward-compat).
6. Consumer Expectations
| Consumer | Subscribes to | Effect |
|---|---|---|
| All services | tenant.created.v1 | Provision per-tenant resources (RLS row, default settings) |
| All services | tenant.suspended.v1 / tenant.reactivated.v1 | Toggle write-block at gateway |
| All services | tenant.config_updated.v1 | Refresh local TenantConfig cache; recompute defaults |
| iam-service | tenant.membership.suspended.v1 / …removed.v1 | Revoke active sessions for that user in that tenant |
| notification-service | tenant.invitation.sent.v1 | Render and send the email (locale-aware) |
| billing-service | tenant.billing_contact_updated.v1 | Update invoice header |
| property-service | tenant.organization_unit.created.v1 (kind=property) | Sanity-check that propertyId resolves |
| search-aggregation-service | tenant.organization_unit.* | Update org-tree projection |
| every service that holds guest data | tenant.guest.erasure_requested.v1 | Perform local erasure + emit ack |
| analytics-service | every tenant.* | Sink to BigQuery |
| audit-service | every tenant.* | Append to immutable audit chain |