Skip to main content

SERVICE_OVERVIEW — theme-config-service

Bundle index: SERVICE_OVERVIEW · DOMAIN_MODEL · APPLICATION_LOGIC · API_CONTRACTS · EVENT_SCHEMAS · DATA_MODEL · SYNC_CONTRACT · AI_INTEGRATION · SECURITY_MODEL · OBSERVABILITY · TESTING_STRATEGY · DEPLOYMENT_TOPOLOGY · FAILURE_MODES · LOCAL_DEV_SETUP · SERVICE_READINESS · SERVICE_RISK_REGISTER · MIGRATION_PLAN

Strategic anchors: 02 Enterprise Architecture · 04 Event-Driven Architecture · 05 API Design · 06 Data Models · 07 Security/Compliance/Tenancy · 08 AI Architecture · ADR-0002 Multi-tenancy · ADR-0003 Electron Offline-First

1. Purpose

theme-config-service is the single source of truth for what each tenant looks and feels like on every consumer surface of Ghasi Melmastoon — the multi-tenant hotel SaaS whose backoffice is an Electron offline-first desktop and whose cloud is GCP. A single shared codebase serves the consumer meta layer, the tenant-branded booking site (web + mobile), and the meta-search hotel-detail card; this service is the runtime mechanism that makes that single codebase look uniquely each tenant's via versioned design tokens, layout presets, content blocks, navigation menus, per-locale copy, booking-flow toggles, and email theme overrides.

The service exists for five reasons that no other service can satisfy:

  1. One token vocabulary. Every consumer surface and every email theme refers to the same semantic tokens (color.primary, typography.body.size.md, spacing.4, …). Owning that vocabulary in one place — and the resolution rules that turn semantic tokens into platform-specific CSS, mobile theme objects, and email-safe inline styles — is what lets us evolve the design system without coordinating N services.
  2. One publish lifecycle. A tenant must be able to draft a brand refresh, preview it on a shared link, sign off, publish atomically, and roll back if something looks wrong — without taking down the booking flow. We own the draft → preview_ready → published → archived state machine and the atomic publication pointer.
  3. One CDN cache contract. The published theme bundle is served at edge from Cloud CDN; only this service knows when to invalidate which tag and which version to bump on the bundle URL.
  4. One i18n + RTL boundary. Pashto, Dari, Arabic, Urdu need RTL; tokens must derive logical-property variants; missing translations need a fallback chain. Centralising this prevents every consumer surface from re-implementing the same fallback logic incorrectly.
  5. One audit trail for brand changes. "Who published this? When? With what tokens? Did publishing degrade contrast?" — questions the platform must answer for security review, GDPR, and tenant disputes — are anchored in our publication ledger.

2. Bounded context

Context name: Theming & Configuration Domain class: Supporting (a force multiplier on every consumer surface; differentiator is not in shipping a token system but in the per-tenant lifecycle, RTL safety, and zero-deploy customisation) Ubiquitous language: Theme, ThemeVersion, ThemePublication, DesignTokenSet, SemanticColor, TypographyPair, SpacingScale, RadiusScale, ShadowScale, MotionToken, LayoutPreset, ContentBlock, NavigationConfig, MenuItem, BookingFlowConfig, BookingFlowStep, FieldRequirement, EmailTheme, LocalePack, FallbackChain, PreviewToken, AssetRef (URL into file-storage-service), AIProvenance (reference only — owned by ai-orchestrator-service).

What is in:

  • The Theme aggregate, its versioned history, and the ThemePublication pointer that names the active version.
  • The semantic token vocabulary and resolution into deliverable artefacts (CSS variables map, JSON token bundle, MJML-friendly inline-style map for email).
  • The LayoutPreset registry (platform-global), and the per-ThemeVersion selection of presets per page (home / listing / detail / booking / post-stay).
  • ContentBlock CRUD with per-locale rich-text bodies, allow-listed Markdown / sanitised HTML.
  • NavigationConfig for header / footer / mobile-drawer.
  • LocalePack (per-tenant enabled locales, default locale, fallback chain) and per-locale formatting hints (dateFormatPattern, currencyDisplay, phoneFormat).
  • BookingFlowConfig (which steps + which optional fields).
  • EmailTheme (override block consumed by notification-service for transactional email rendering).
  • PreviewToken minting and verification.
  • The publish workflow, atomic flip, rollback, OCC, CDN invalidation, audit projection.
  • Synchronous read APIs for the BFFs.

What is out:

  • Raw assets (logos, hero images, videos, fonts) — owned by file-storage-service. We reference them by URL.
  • Real translation of copy — ai-orchestrator-service produces drafts; we persist the approved strings.
  • Tenant identity / domain provisioningtenant-service owns the per-tenant verified hosts; we project the values needed for routing.
  • OAuth / authentication of consumers — anonymous public reads of the published bundle are the norm; staff edits go through iam-service-issued JWTs.
  • CDN itself — Cloud CDN is platform infra; we hold the adapter that issues invalidation requests.
  • Email renderingnotification-service renders the email; we just hand it the email-theme block.

3. Aggregates owned

AggregateCardinalityPurposeIdentity prefix
Themeroot, 1 per (tenantId, propertyId?)Logical theme; carries the active publication pointer + version historythm_
ThemeVersionchild of Theme, 1..NImmutable snapshot of tokens + layout selections + content + navigation + booking-flow + email overrides; states draft → preview_ready → published → archivedthv_
ThemePublicationchild of Theme, exactly 1 active per ThemeThe version currently served to consumers; rollback creates a new publication pointing at an older versionthp_
LayoutPresetplatform-global (no tenantId)Canonical layout preset registered by Frontend Platform; tenants select one per pagelpr_
ContentBlockchild of ThemeVersion, 1..NPage-attached block (about/policy/faq/etc.) with per-locale bodycnb_
NavigationConfigchild of ThemeVersion, 1 per surfaceOrdered tree of menu items with per-locale labelsnvc_
BookingFlowConfigchild of ThemeVersion, 1Toggles + ordered step list for the booking flowbfc_
EmailThemechild of ThemeVersion, 1Override block consumed by notification-serviceemt_
LocalePackchild of ThemeVersion, 1..N (one per locale)Keyed copy bundle (cta.book_now, errors.required_field, …)(composite, no prefix)
PreviewTokenroot, short-lived (TTL 24h, configurable)Signed token granting anonymous read of one unpublished ThemeVersion on the preview hostpvt_

Theme.tenantId is mandatory; Theme.propertyId is null for tenant-default themes (Phase 0) and non-null for per-property overrides (Phase 2 chain branding). Resolution is "property-override wins; otherwise fall back to tenant-default; otherwise fall back to platform scaffold".

4. Responsibilities (numbered)

  1. Default-theme provisioning. On melmastoon.tenant.created.v1, scaffold a Theme with one published ThemeVersion derived from the melmastoon-default platform scaffold, in the tenant's primary locale. The booking flow must be live the instant the tenant exists.
  2. Draft authoring. Create new ThemeVersions by cloning the active version (default) or a blank scaffold. Apply patches to tokens, layout selections, content blocks, navigation, booking-flow, email theme, and locale packs. OCC enforced via If-Match: <version>.
  3. Token validation. On every save (and again at publish), validate (a) token schema (no missing semantic colors), (b) WCAG AA contrast for every foreground/background pair the design system uses, (c) absence of physical layout properties in custom CSS variables. Block save on validation failure; emit theme.token_validation_failed.v1 (audit only, not consumer-facing).
  4. Preview minting. Mint a PreviewToken for any non-archived ThemeVersion; build the bundle once and cache; share a https://preview.melmastoon.app/<previewToken> URL the tenant can send to stakeholders. Tokens expire and rotate; revocation is immediate.
  5. Atomic publish. On publish, run final validation, build the deliverable bundle (JSON tokens + CSS variables map + email-theme JSON), upload to GCS at gs://melmastoon-themes/<tenantId>/<themeId>/<version>/bundle.json (immutable), atomically flip the ThemePublication pointer in a single Postgres transaction with an outbox event, swap the Memorystore key, and queue a Cloud CDN invalidation. Emit theme.published.v1, theme.tokens_changed.v1 (with token diff), theme.cdn_cache_invalidated.v1.
  6. Rollback. Publish a previous version (no destructive history). Same atomic flip; emits theme.rolled_back.v1 carrying both the new and previous version ids.
  7. Per-locale management. Add / remove locales from the enabled set with safeguards (cannot remove defaultLocale; cannot remove a locale referenced by an active PreviewToken); emit theme.locale_added.v1 / theme.locale_removed.v1.
  8. Fallback resolution. When the published bundle is requested with ?locale=ps-AF and the requested key is not translated, walk the fallback chain (ps-AF → fa-AF → ar-SA → en) declared on the LocalePack. If the chain ends without a match, return the default value baked into the platform scaffold and emit a metric (theme.locale_fallback_missed).
  9. RTL token derivation. Resolve direction = auto per locale: rtl for ar, ps, fa, ur, he; ltr otherwise. Derive logical-property tokens (spacing.start, spacing.end) from physical scales and emit them in the bundle. Reject any custom CSS variable key that uses physical left / right outside an explicit rtl-mirror: false opt-out.
  10. Asset reference integrity. Persist asset references as MediaRef URLs into file-storage-service. On consumed melmastoon.media.deleted.v1, scan published versions referencing the URL; mark as broken, emit theme.broken_asset_detected.v1, alert the tenant admin in backoffice.
  11. Email theme co-resolution. Expose a per-tenant read endpoint that notification-service reads at template-render time: returns the active email theme block (logo URL, header gradient, primary color, footer markup, social-link nav). Cached in Memorystore with the same invalidation tag as the booking bundle.
  12. Booking-flow configuration. Expose bookingFlow block (which steps / which optional fields per step) consumed by bff-tenant-booking-service; toggles like captureGuestPassport, requestAirportTransport, allowGiftBooking, requireGuestSignature, showCancellationPolicySummary. Validate against the canonical step + field registry on save.
  13. Concurrent edit conflict (OCC). Every mutating endpoint requires If-Match: <version>. Mismatch → 412 Precondition Failed with the conflicting fields surfaced in errors[] so the editor UI can three-way-merge.
  14. CDN cache invalidation. Issue Cloud CDN purge by tag theme:<themeId>; tag is set on every served bundle response so the purge invalidates only the affected tenant. Emit theme.cdn_cache_invalidated.v1 with the bundle URL and Cache-Tag value.
  15. Audit projection. Every publish, rollback, locale change, token change, and broken-asset detection is appended to the audit projection consumed by audit-service.

5. Upstream / downstream context map

┌─────────────────────────────────┐
│ tenant-service │ per-tenant settings,
│ │ default locale,
│ │ currency display,
│ │ enabled domains
└────────────────┬────────────────┘
│ tenant.created.v1
│ tenant.config_updated.v1
│ tenant.deleted.v1

┌──────────────────┐ ┌──────────────────────────┐
│ file-storage-svc │ media.uploaded.v1 / media.deleted.v1 │ ai-orchestrator-service │
│ │ ─ asset URL refs ─ │ ai_drafted_content_ready │
└────────┬─────────┘ │ (palette / translations) │
│ └─────────────┬────────────┘
│ │
┌────────▼─────────────────────────────────────────────────────────────────▼────────────┐
│ theme-config-service │
│ ┌────────────────┐ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ Authoring API │→ │ OCC + token │→ │ Validate + │→ │ Build │ → GCS bundle │
│ │ (drafts) │ │ guard │ │ contrast │ │ bundle │ (immutable) │
│ │ │ │ │ │ (WCAG AA) │ │ (json+css) │ │
│ └────────────────┘ └──────────────┘ └──────────────┘ └──────────────┘ │
│ │ │ │
│ ▼ ▼ │
│ ┌─────────────────┐ ┌─────────────────────┐ │
│ │ Preview-token │ │ CDN invalidator │ │
│ │ minter │ │ (Cloud CDN tag) │ │
│ └─────────────────┘ └─────────────────────┘ │
└────────────────────────────────────────────────────────────────────────────────────────┘

┌──────────────────────────────────┼─────────────────────────────────────┐
│ │ │
▼ ▼ ▼
┌──────────────────────┐ ┌─────────────────────────────┐ ┌────────────────────────┐
│ bff-consumer-service │ │ bff-tenant-booking-service │ │ notification-service │
│ (meta detail page) │ │ (full booking funnel) │ │ (email theme reads) │
└──────────────────────┘ └─────────────────────────────┘ └────────────────────────┘

6. Theme publishing lifecycle — ASCII state diagram

┌──────────────────────────────┐
│ │
│ (no version exists) │
│ │
└──────────────┬───────────────┘
│ POST /api/v1/themes/:id/versions
│ { source: 'clone_active' | 'blank' }

┌──────────────────────────────────────────────────────────────────────────┐
│ │
│ ┌──────────┐ │
│ │ draft │ ◀──── PATCH /api/v1/theme-versions/:id (OCC, repeatable) │
│ └────┬─────┘ │
│ │ │
│ │ POST /api/v1/theme-versions/:id/preview │
│ ▼ │
│ ┌──────────────────┐ preview token issued (TTL 24h) │
│ │ preview_ready │ ──▶ shareable URL: preview.melmastoon.app/<token> │
│ └────┬─────────────┘ │
│ │ │
│ │ POST /api/v1/theme-versions/:id/publish │
│ │ 1. final validation (tokens + contrast + RTL parity + assets) │
│ │ 2. build bundle (json + css vars + email block) │
│ │ 3. upload to GCS (immutable, content-addressed) │
│ │ 4. atomic flip ThemePublication → this version (Postgres tx) │
│ │ 5. swap Memorystore key (theme:<themeId>:published) │
│ │ 6. emit theme.published.v1 + theme.tokens_changed.v1 │
│ │ 7. enqueue CDN invalidation (Cache-Tag theme:<themeId>) │
│ ▼ │
│ ┌──────────────────┐ │
│ │ published │ ◀── (the *previously* published version goes here) │
│ │ (active for tnt) │ once a *new* version supersedes it │
│ └────┬─────────────┘ │
│ │ │
│ │ POST /api/v1/themes/:id/rollback { toVersion: thv_… } │
│ │ → publishes a prior version; this current published version │
│ │ transitions to `archived` with `archivedReason='rolled_back'`│
│ ▼ │
│ ┌──────────────────┐ │
│ │ archived │ — no consumer reads this version directly anymore │
│ │ (immutable) │ except via /api/v1/theme-versions/:id (audit) │
│ └──────────────────┘ │
│ │
└──────────────────────────────────────────────────────────────────────────┘

Compensation paths:
- Validation fails on publish → version stays in preview_ready; emit
theme.publish_rejected.v1 with the failed checks; never half-publish.
- GCS upload succeeds but Postgres flip fails → bundle remains in GCS
(orphan, swept by retention); no events emitted; no consumer impact.
- Postgres flip succeeds but Memorystore swap fails → next read repopulates
the cache from GCS; emit cache_swap_failed metric (warn).
- CDN invalidation fails → up to 60s of stale edge bundles is acceptable;
retry queue with exponential backoff; alert if failures persist > 5 min.

7. Key invariants enforced in the domain layer

  1. No cross-tenant references. Every aggregate carries a TenantId value object; the constructor refuses missing or mismatched values. LayoutPreset is the only platform-global aggregate (tenantId = null) and is constructed via a separate PlatformLayoutPreset.create() factory. (MELMASTOON.GENERAL.CROSS_TENANT_REFERENCE)
  2. Exactly one published version per Theme. The ThemePublication table has a unique index on (themeId) where status = 'active'. Atomic flips run inside a single Postgres transaction. (MELMASTOON.THEME.PUBLISH_CONFLICT)
  3. A ThemeVersion is immutable once published. Any patch attempt against a published or archived version returns 409 MELMASTOON.THEME.VERSION_IMMUTABLE. To change anything, create a new draft.
  4. enabledLocales must include defaultLocale. Removing the default locale is rejected with 422 MELMASTOON.THEME.DEFAULT_LOCALE_REQUIRED.
  5. Fallback chain must terminate at defaultLocale or en. Cycles or open-ended chains are rejected at draft save. (MELMASTOON.THEME.FALLBACK_CHAIN_INVALID)
  6. Layout-preset selection must reference a registered preset. Unknown preset id → MELMASTOON.THEME.LAYOUT_PRESET_UNKNOWN.
  7. Token contrast invariant at publish. Every (foreground, background) semantic pair must meet WCAG AA contrast (4.5:1 for body, 3:1 for large text). Failures block publish with MELMASTOON.THEME.TOKEN_INVALID and a list of violating pairs.
  8. RTL parity invariant. The tokens bundle resolves direction auto per locale; the resolved bundle must contain logical-property tokens for every spacing/padding key. Missing logical variants → MELMASTOON.THEME.RTL_VARIANT_MISSING at publish.
  9. Asset references must resolve. At publish, every MediaRef URL is HEAD-checked against file-storage-service; broken refs reject with MELMASTOON.THEME.ASSET_BROKEN (override allowed by ?allowBrokenAssets=true for staged migrations only).
  10. Asset size budget. Logo ≤ 1 MiB; hero ≤ 8 MiB; theme video ≤ 32 MiB. Enforced via the file-storage-service contract, but mirrored here at draft save. (MELMASTOON.THEME.ASSET_TOO_LARGE)
  11. OCC version checked on every save. (MELMASTOON.GENERAL.PRECONDITION_FAILED)
  12. PreviewToken is single-version, single-use-per-pair, expiring. Default TTL 24h; max TTL 7 days; revocation is immediate (cache-key purge).
  13. AI-drafted palette / translations require HITL before applying. A draft mutation that uses AI-generated tokens requires aiProvenance + an approvedBy actor before save. (MELMASTOON.AI.HITL_REQUIRED)
  14. Booking-flow steps reference the canonical registry. Unknown step id → MELMASTOON.THEME.BOOKING_STEP_UNKNOWN.

8. Hot read paths

ReadFrequencyCaching strategy
Active published bundle for (tenantId, propertyId?)per consumer page view (very high)Cloud CDN (TTL 5 min) → Memorystore key theme:<themeId>:published (TTL 1h, invalidated on publish) → GCS bundle object
Active published bundle for (host, path) (BFF resolution)per BFF requestMemorystore key theme:by-host:<host> (TTL 5 min)
Email theme block for (tenantId, propertyId?)per email render in notification-serviceMemorystore key theme:<themeId>:email (TTL 10 min, invalidated on publish)
Layout-preset registryper BFF cold startMemorystore key theme:layout-presets (TTL 1h; rare updates)
Draft bundle for the authoring UIper editor keystroke (debounced)No cache; direct Postgres read with tenant_id RLS context
Preview bundle for previewTokenper preview-link visitMemorystore key theme:preview:<token> (TTL = token TTL)
Locale fallback table for (tenantId)per renderMemorystore key theme:<themeId>:locales (TTL 10 min)

9. Cost & scale envelope

DimensionTarget
Tenants per region (Phase 0)200
Themes per tenant1 (Phase 0); up to 1 + N properties (Phase 2 chains)
Versions per theme retained on hot tierlast 50; older archived to GCS coldline
Published-bundle p99 size≤ 40 KB gzipped (excluding asset bytes)
Published-bundle reads per second (steady-state)< 5 / s — CDN absorbs the rest
Authoring API p99< 250 ms
Publish workflow end-to-end p99< 8 s (includes GCS upload + CDN invalidation enqueue)
Cloud Run min replicas (API)3
Cloud Run min replicas (publish-worker)2
Cloud Run min replicas (CDN-invalidation worker)1 (autoscale)
Cloud SQL Postgres CPUshared with platform-services pool on the regional HA instance
GCS bucketmelmastoon-themes-<region> (Standard for last 90 days, Nearline 90+, Coldline 365+)

10. Decision log (anchors)

  • Why a separate service rather than a folder in tenant-service — the publishing lifecycle (draft / preview / publish / rollback / cache invalidate) and the design-token vocabulary are heavy enough that mixing them with tenant CRUD would obscure both. Lifecycle, OCC, contrast checks, RTL derivation, and CDN integration are non-trivial concerns that benefit from isolation.
  • Why semantic tokens, not raw colour hex everywhere — the consumer apps depend on a stable token vocabulary; tenants change the values, never the names. This lets us evolve the design system (add accent, deprecate mutedAlt) without coordinating with N tenants.
  • Why CSS variables + JSON, not a CSS file per tenant — the consumer apps are a single shared bundle; per-tenant CSS files would balloon the build matrix and bypass our compression / CDN economy. CSS variables let one stylesheet adapt at runtime; JSON lets React Native and email renderers consume the same source.
  • Why immutable versions and atomic publish — design rollbacks must be safe and instant; the only way to guarantee that without "did I miss a field?" risk is to ban in-place edits on published versions.
  • Why publish through GCS + CDN rather than directly from Postgres — edge latency. A Pashto guest in Kandahar should see their hotel's brand at 50 ms, not 500 ms. Postgres is canonical; GCS + Memorystore + CDN is for serving.
  • Why we read AI provenance but never call models — every AI call must flow through ai-orchestrator-service for routing, moderation, budget, HITL, and AIProvenance per 02 §11. Suggested palettes and draft translations arrive as content with provenance attached; we persist provenance and require HITL approval before activation.
  • Why per-tenant CDN tags rather than per-bundle URL versioning alone — bundle URLs are content-hashed (immutable), but consumer apps cache the bundle URL itself in Memorystore for 1 h; the tag-based purge lets us invalidate that without waiting for TTL.

11. What this service depends on (libraries, ports, infrastructure)

  • NestJS for presentation + DI composition root (out of the domain layer).
  • Drizzle ORM for Postgres access in the infrastructure layer.
  • @google-cloud/pubsub for outbox publishing and consumed-event subscription.
  • @google-cloud/storage for storing immutable bundle objects.
  • @google-cloud/compute (Cloud CDN admin client) for cache-tag invalidation.
  • Memorystore (Redis) for published-bundle hot cache, preview-token cache, layout-preset registry cache.
  • color2k for contrast calculation; @formatjs/intl-locale for locale resolution; postcss-logical for verifying logical-property usage; dompurify + jsdom for sanitising rich-text in content blocks.
  • Ports the application layer depends on (interfaces only):
    • ThemeRepository
    • ThemeVersionRepository
    • ThemePublicationRepository
    • ContentBlockRepository
    • NavigationConfigRepository
    • BookingFlowConfigRepository
    • EmailThemeRepository
    • LocalePackRepository
    • LayoutPresetRegistryRepository
    • PreviewTokenRepository
    • BundleStoragePort (GCS adapter)
    • CdnInvalidationPort (Cloud CDN adapter)
    • EventPublisher (outbox-backed)
    • AssetIntegrityClient (calls file-storage-service HEAD)
    • AIClient (calls ai-orchestrator-service for HITL palette / translation drafts)
    • TenantConfigClient (reads per-tenant default locale, currency display from tenant-service)
    • Clock, IdGenerator, Hasher, ContrastChecker

The domain layer depends on nothing outside @ghasi/domain-primitives and the standard library. CI fails the build on any framework or I/O import inside src/domain/.

12. References