Skip to main content

EVENT_SCHEMAS — theme-config-service

Sibling: APPLICATION_LOGIC · API_CONTRACTS · DATA_MODEL

Platform anchors: docs/04-event-driven-architecture.md · docs/standards/NAMING.md

This document defines every event the service publishes and consumes, the canonical envelope, the topic / subscription wiring on Pub/Sub, and the schema-evolution policy.


1. Envelope

All events follow the platform envelope from docs/04-event-driven-architecture.md §4:

interface EventEnvelope<TPayload = unknown> {
schema: 'melmastoon.event.v1';
eventId: string; // ULID, globally unique (idempotency key for consumers)
eventType: string; // e.g. 'melmastoon.theme.published.v1'
occurredAt: string; // RFC3339 UTC
producer: 'theme-config-service';
producerVersion: string; // semver of the service at emit time
tenantId: string; // ULID
correlationId: string; // request_id of the originating command
causationId?: string; // eventId of the upstream event when chained
actor?: { kind: 'user'|'system'|'service'; id: string; ip?: string; userAgent?: string };
partitionKey: string; // 'tenant:<tenantId>' for tenant-scoped events
retentionClass: 'short'|'standard'|'long'; // matches topic config
payload: TPayload; // see per-event tables below
metadata?: Record<string, unknown>;
}

JSON-Schema definitions live in services/theme-config-service/contracts/events/ and are published to the platform schema registry on release.


2. Topics & subscriptions

TopicRetentionProducersConsumers
melmastoon.theme.eventsstandard (7 days)theme-config-servicebff-tenant-booking-service, bff-consumer-service, bff-backoffice-service, notification-service, analytics-service, audit-service
melmastoon.theme.versionslong (30 days)theme-config-serviceaudit-service, analytics-service
melmastoon.theme.cdnshort (24h)theme-config-serviceedge-warmer-service
melmastoon.tenant.eventsstandardtenant-servicetheme-config-service (subscriber)
melmastoon.media.eventsstandardfile-storage-servicetheme-config-service (subscriber)

Subscriptions owned by theme-config-service:

SubscriptionTopicFilterAck deadline
theme-config-svc--tenant-lifecyclemelmastoon.tenant.eventsattributes.eventType IN ("melmastoon.tenant.created.v1","melmastoon.tenant.deleted.v1","melmastoon.tenant.config_updated.v1")60s
theme-config-svc--media-lifecyclemelmastoon.media.eventsattributes.eventType = "melmastoon.media.deleted.v1"60s
theme-config-svc--property-lifecycle (Phase 2)melmastoon.property.eventsattributes.eventType = "melmastoon.property.created.v1"60s

All subscriptions have a dead-letter topic …--dlq with max 5 delivery attempts.


3. Published events

3.1 Catalogue

EventTopicRetentionTriggerSchema version
melmastoon.theme.draft_created.v1theme.eventsstandardCreateThemeVersionUseCase okv1
melmastoon.theme.draft_updated.v1theme.eventsstandardPatchThemeVersionUseCase okv1
melmastoon.theme.preview_generated.v1theme.eventsstandardMintPreviewTokenUseCase okv1
melmastoon.theme.preview_revoked.v1theme.eventsstandardpreview token revokedv1
melmastoon.theme.published.v1theme.events + theme.versionslongPublishThemeVersionUseCase okv1
melmastoon.theme.publish_rejected.v1theme.eventsstandardpublish failed validationv1
melmastoon.theme.rolled_back.v1theme.events + theme.versionslongRollbackThemeUseCase okv1
melmastoon.theme.version_archived.v1theme.versionslongpublish or rollback supersedes a versionv1
melmastoon.theme.tokens_changed.v1theme.eventsstandardpublished tokens differ from previous publicationv1
melmastoon.theme.layout_preset_changed.v1theme.eventsstandardlayout selection changed in a draftv1
melmastoon.theme.content_block.created.v1theme.eventsstandardcontent block CRUDv1
melmastoon.theme.content_block.updated.v1theme.eventsstandardcontent block CRUDv1
melmastoon.theme.content_block.deleted.v1theme.eventsstandardcontent block CRUDv1
melmastoon.theme.navigation_updated.v1theme.eventsstandardnav PATCH okv1
melmastoon.theme.locale_added.v1theme.eventsstandardAddLocaleUseCase okv1
melmastoon.theme.locale_removed.v1theme.eventsstandardRemoveLocaleUseCase okv1
melmastoon.theme.booking_flow_config_updated.v1theme.eventsstandardUpdateBookingFlowConfigUseCase okv1
melmastoon.theme.email_theme_updated.v1theme.eventsstandardUpdateEmailThemeUseCase ok (also re-emitted on publish)v1
melmastoon.theme.cdn_cache_invalidated.v1theme.cdnshortpost-publish CDN purge issuedv1
melmastoon.theme.broken_asset_detected.v1theme.eventsstandardscanner finds a referenced asset URL unreachablev1

3.2 Payload contracts

3.2.1 melmastoon.theme.draft_created.v1

{
"themeId": "thm_01J...",
"themeVersionId": "thv_01J...",
"ordinal": 8,
"source": "clone_active",
"sourceVersionId": "thv_01J...",
"createdAt": "2026-04-23T10:00:00.000Z",
"createdBy": "usr_01J..."
}

3.2.2 melmastoon.theme.draft_updated.v1

{
"themeId": "thm_01J...",
"themeVersionId": "thv_01J...",
"patchSummary": {
"fieldsChanged": ["tokens.color.primary","tokens.color.primaryHover","layoutSelections.home.variantKey"],
"fieldsCount": 3
},
"updatedAt": "2026-04-23T10:14:55.211Z",
"updatedBy": "usr_01J..."
}

Payload deliberately excludes the new field values to keep the bus light; consumers needing the full version do a callback to GET /theme-versions/{id}.

3.2.3 melmastoon.theme.preview_generated.v1

{
"themeId": "thm_01J...",
"themeVersionId": "thv_01J...",
"previewTokenId": "pvt_01J...",
"expiresAt": "2026-04-24T10:00:00.000Z",
"createdBy": "usr_01J..."
}

The token secret is never put on the bus.

3.2.4 melmastoon.theme.published.v1

The most important event in the service.

{
"themeId": "thm_01J...",
"themeVersionId": "thv_01J...",
"previousThemeVersionId": "thv_01J...",
"publicationId": "thp_01J...",
"ordinal": 8,
"bundleUrl": "https://cdn.melmastoon.app/themes/thm_01J.../published.json",
"bundleSha256": "9f2c8e...",
"bundleSizeGzippedBytes": 27341,
"tenantConfigSnapshot": {
"defaultLocale": "ps-AF",
"enabledLocales": ["ps-AF","fa-AF","en-US"],
"currency": "AFN"
},
"publishedAt": "2026-04-23T10:14:55.211Z",
"publishedBy": "usr_01J...",
"releaseNotes": "Spring promo, new hero, AR locale launch",
"diffSummary": {
"tokens": { "changed": 7, "added": 0, "removed": 0 },
"blocks": { "changed": 2, "added": 1, "removed": 0 },
"navs": { "changed": 1, "added": 0, "removed": 0 },
"locales": { "changed": 0, "added": 1, "removed": 0 },
"bookingFlow": { "changed": false },
"emailTheme": { "changed": true }
},
"aiProvenance": {
"anyAiContent": true,
"aggregates": ["LocalePack:ar-SA","ContentBlock:cnb_01J..."],
"approvers": ["usr_01J..."]
}
}

Consumers:

  • BFFs: invalidate Memorystore key theme:<themeId>:published and warm with the new bundleUrl.
  • notification-service: invalidate theme:<themeId>:email if diffSummary.emailTheme.changed.
  • analytics-service: record publication for branding-change attribution.
  • audit-service: persist immutable audit log entry.

3.2.5 melmastoon.theme.publish_rejected.v1

{
"themeId": "thm_01J...",
"themeVersionId": "thv_01J...",
"attemptedAt": "2026-04-23T10:14:55.211Z",
"attemptedBy": "usr_01J...",
"reasons": [
{ "code": "MELMASTOON.THEME.RTL_VARIANT_MISSING", "field": "tokens.spacing.start", "message": "Logical 'start' missing for ar-SA." },
{ "code": "MELMASTOON.THEME.ASSET_BROKEN", "field": "blocks[cnb_01J...].body.en-US.value.items[0].ref.url", "message": "404 from origin." }
]
}

3.2.6 melmastoon.theme.rolled_back.v1

{
"themeId": "thm_01J...",
"fromThemeVersionId": "thv_01J...",
"toThemeVersionId": "thv_01J...",
"publicationId": "thp_01J...",
"reason": "Investor preview launched broken hero image",
"rolledBackAt": "2026-04-23T10:30:00.000Z",
"rolledBackBy": "usr_01J..."
}

3.2.7 melmastoon.theme.version_archived.v1

{
"themeId": "thm_01J...",
"themeVersionId": "thv_01J...",
"archivedReason": "superseded",
"archivedAt": "2026-04-23T10:14:55.211Z"
}

archivedReason ∈ ('superseded','rolled_back','manual_archive').

3.2.8 melmastoon.theme.tokens_changed.v1

Emitted as a sibling of published.v1 whenever the token diff is non-empty. Carries the canonical token diff so that notification-service can pre-render new email assets and the edge-warmer can priority-warm critical surfaces.

{
"themeId": "thm_01J...",
"fromThemeVersionId": "thv_01J...",
"toThemeVersionId": "thv_01J...",
"diff": {
"color.primary": { "from": "#0F4C81", "to": "#0E437A" },
"color.primaryHover": { "from": "#0C3F6C", "to": "#0B3866" },
"typography.fontFamilyDisplay": { "from": "\"Iran Sans\", system-ui, sans-serif", "to": "\"Vazirmatn\", system-ui, sans-serif" }
},
"publishedAt": "2026-04-23T10:14:55.211Z"
}

3.2.9 melmastoon.theme.layout_preset_changed.v1

{
"themeId": "thm_01J...",
"themeVersionId": "thv_01J...",
"surface": "home",
"from": { "presetKey": "hero-with-search", "variantKey": "video-hero" },
"to": { "presetKey": "hero-with-search", "variantKey": "static-hero" },
"changedAt": "2026-04-23T10:14:55.211Z"
}

3.2.10 melmastoon.theme.content_block.{created,updated,deleted}.v1

Common shape:

{
"themeId": "thm_01J...",
"themeVersionId": "thv_01J...",
"blockId": "cnb_01J...",
"kind": "gallery",
"surface": "home",
"ordinal": 30,
"occurredAt": "2026-04-23T10:14:55.211Z",
"actor": "usr_01J..."
}

updated.v1 additionally carries fieldsChanged: string[]. deleted.v1 is a tombstone (no body).

3.2.11 melmastoon.theme.navigation_updated.v1

{
"themeId": "thm_01J...",
"themeVersionId": "thv_01J...",
"navId": "nvc_01J...",
"surface": "header",
"itemCount": 6,
"occurredAt": "2026-04-23T10:14:55.211Z"
}

3.2.12 melmastoon.theme.locale_added.v1 / …locale_removed.v1

{
"themeId": "thm_01J...",
"locale": "ar-SA",
"fallbackChainAfter": ["ps-AF","en-US","ar-SA"],
"occurredAt": "2026-04-23T10:14:55.211Z",
"actor": "usr_01J..."
}

3.2.13 melmastoon.theme.booking_flow_config_updated.v1

{
"themeId": "thm_01J...",
"themeVersionId": "thv_01J...",
"bookingFlowId": "bfc_01J...",
"togglesChanged": ["capturePassportNumber","requestAirportTransfer"],
"stepsChanged": false,
"occurredAt": "2026-04-23T10:14:55.211Z"
}

3.2.14 melmastoon.theme.email_theme_updated.v1

{
"themeId": "thm_01J...",
"themeVersionId": "thv_01J...",
"emailThemeId": "emt_01J...",
"tokensChanged": true,
"logoChanged": false,
"footerChanged": false,
"publishedToActive": true,
"occurredAt": "2026-04-23T10:14:55.211Z"
}

3.2.15 melmastoon.theme.cdn_cache_invalidated.v1

{
"themeId": "thm_01J...",
"cacheTag": "theme:thm_01J...",
"invalidationId": "ci_01J...",
"reason": "publish",
"queuedAt": "2026-04-23T10:14:55.300Z",
"estimatedPropagationSeconds": 45
}

3.2.16 melmastoon.theme.broken_asset_detected.v1

{
"themeId": "thm_01J...",
"themeVersionId": "thv_01J...",
"assetUrl": "https://storage.googleapis.com/melmastoon-tenant-prod/...",
"referencedAt": ["blocks[cnb_01J...].body.en-US.value.items[0].ref.url"],
"detectedAt": "2026-04-23T10:14:55.211Z",
"detectionSource": "media_deleted_event"
}

detectionSource ∈ ('media_deleted_event','daily_scanner','publish_validation').


4. Consumed events

4.1 melmastoon.tenant.created.v1

Payload contract owned by tenant-service; we read:

{ tenantId, primaryLocale, currency, hostHints?: string[], plan }

Handler: ProvisionDefaultThemeUseCase — see APPLICATION_LOGIC §2.1.

Idempotency: by eventId in consumed_events; additionally guarded by the unique(tenant_id, property_id) where deleted_at is null index on themes.

4.2 melmastoon.tenant.deleted.v1

Handler: PurgeTenantThemesUseCase — soft-deletes all themes + cancels in-flight previews; schedules hard purge in 30 days.

4.3 melmastoon.tenant.config_updated.v1

Payload of interest: currency, defaultLocale, formattingOptions. Handler: RefreshTenantFormattingUseCase — only re-emits tokens_changed.v1 if the formatting fields actually differ from the active publication's snapshot (no churn).

4.4 melmastoon.media.deleted.v1

Payload of interest: assetUrl. Handler scans active published versions for references; emits theme.broken_asset_detected.v1 per reference site.

4.5 (Phase 2) melmastoon.property.created.v1

When chain_branding flag is on for the tenant, provisions a property-scoped Theme inheriting from the tenant theme.


5. Schema evolution

  • Additive only within a major version. Adding fields → no version bump; consumers MUST ignore unknown fields.
  • Removing or renaming a field → new event type with .v2 suffix; the old type is published in parallel for ≥ 2 weeks; deprecation banner in docs/04-event-driven-architecture.md.
  • Semantic changes (meaning of an existing field changes) → .v2, never repurpose .v1.
  • Schemas are validated in CI against fixtures in services/theme-config-service/contracts/events/__fixtures__/. Every PR that adds an event MUST include a fixture and a consumer-side compatibility assertion.

6. Outbox & at-least-once delivery

Per docs/04-event-driven-architecture.md §10:

  • Every event is written to the outbox table within the same Postgres transaction as the aggregate save.
  • The outbox-publisher worker drains rows in created_at order, publishes to Pub/Sub, and stamps published_at.
  • Pub/Sub message attributes mirror envelope fields (eventType, tenantId, correlationId, partitionKey, retentionClass) so subscribers can filter without parsing the payload.
  • Consumers MUST be idempotent on eventId. The theme-config-service consumes through the platform inbox pattern: every consumed event is recorded in consumed_events(event_id PRIMARY KEY) before its handler runs; duplicates short-circuit.

7. References