Skip to main content

ADR 0001: Core architecture & tech stack (Ghasi Melmastoon)

Status

Accepted — 2026-04-22.

Context

Ghasi Melmastoon must ship four user-facing surfaces (consumer meta web, consumer mobile, tenant-branded booking web/mobile, AI-first offline-first backoffice desktop) plus 22 backend services, in a market that demands cost-discipline, low-bandwidth tolerance, RTL/LTR parity, cash-heavy payments, and pluggable lock hardware. The team is small, hires from a JS/TS-heavy talent pool in our target geographies, and must be productive immediately with AI coding tools (Cursor, Copilot, Claude). We need one tech-stack decision that simultaneously protects velocity, operability, and architectural cleanliness for the next 5 years.

This ADR locks in the foundational technology choices on which every other ADR and per-service spec depends. Subsequent ADRs (multi-tenancy model, Electron offline-first, lock-integration abstraction) extend this baseline.

Design forces:

  • One language across the estate to maximize cross-team mobility, code review, shared libraries (@ghasi/ui-melmastoon, @ghasi/domain-money, @ghasi/sync-client), and AI-tool effectiveness.
  • Clean Architecture / DDD to keep the domain layer pure and replaceable, and to scale the codebase past 22 services without service-fragmentation chaos.
  • Event-driven to enable offline sync, audit, replay, and cross-service decoupling.
  • GCP-first because Vertex AI is co-located, GCP cost is competitive at our scale, and our target region has acceptable GCP latency.
  • Three BFFs because anonymous-consumer, in-funnel-guest, and authenticated-staff surfaces have incompatible auth, cache, and shape requirements.
  • Electron for desktop because the staff-facing line-of-business app needs offline-first SQLite, ONNX edge inference, OS keychain integration, signed installers, vendor SDKs (locks, peripherals) shipped as Node bindings, and a hiring profile that aligns with the rest of the team.

Decision

We adopt the following stack and architectural pattern across the entire platform.

1. Language

TypeScript everywhere. Backend (NestJS), frontend web (Next.js), frontend mobile (React Native), frontend desktop (Electron renderer + main), shared packages, infrastructure tooling (CDKTF / Pulumi), CI scripts. No polyglot. Domain layers are pure TS with zero framework imports.

2. Backend

  • NestJS microservices, one per bounded context (22 services).
  • Clean / Hexagonal Architecture in every service: presentation → application → domain → infrastructure with strict inward-only dependency rule. Domain is framework-free.
  • Drizzle (or pg directly for hot paths) for Postgres access; no ORM in the domain layer, repositories live in infrastructure and return domain entities.
  • Zod for DTO validation at the presentation/application boundary; never inside the domain.

3. Frontend (web)

  • Next.js (App Router) + TailwindCSS + React Query + Zustand for the consumer meta layer and the tenant booking experience.
  • PWA shell for browse caching on consumer surfaces (not full offline — only browse cache + last-results cache).
  • Per-tenant theming applied at runtime via theme-config-service (CSS variables + token bundle, no per-tenant build).

4. Frontend (mobile)

  • React Native (single consumer app, multi-tenant aware). Shares @ghasi/ui-melmastoon design tokens and i18n bundle with web.
  • Native modules only for: payments SDKs, push notifications, deep links, geo, secure-store.

5. Frontend (desktop backoffice)

  • Electron (Node 20 main process + Chromium renderer) + Vite + React + better-sqlite3 local store + ONNX Runtime Node for offline AI inference + electron-builder + electron-updater + keytar for OS keychain.
  • Strict process model: main process owns OS, lock SDKs, keychain, AI inference, sync worker; renderer is React-only with nodeIntegration: false, contextIsolation: true; preload exposes a narrow typed window.melmastoon via contextBridge.
  • Detail and rationale in ADR-0003.

6. Cloud

  • Google Cloud Platform. Cloud Run for compute, Cloud SQL (Postgres) for OLTP, Pub/Sub for messaging, Cloud Storage for media + invoices + theme assets, Memorystore (Redis) for cache + rate-limit, Firestore for sync cursors + outbox marks + device-pair state, Vertex AI for cloud LLM/embeddings/vision/TTS, Secret Manager for secrets, KMS for envelope encryption, Cloud Logging/Monitoring/Trace for OTel.
  • Kong as the north-south gateway (TLS, JWT validation, rate limit, header propagation).
  • pgvector as a Postgres extension; no separate vector DB.

7. Architecture pattern

  • Event-driven microservices on Pub/Sub. Subject pattern {service}.{aggregate}.{event}.v{N}. Transactional outbox per service. At-least-once delivery; idempotent consumers.
  • Saga orchestration for multi-service flows (notably booking → inventory → payment → key issuance), with declared compensations.
  • No synchronous cross-service chain longer than 2 hops.
  • Three BFFsbff-consumer-service, bff-tenant-booking-service, bff-backoffice-service — because the surfaces have incompatible auth, cache, and shape requirements (full justification in 02 Enterprise Architecture §5).
  • Hybrid multi-tenancy: shared schema + tenant_id + Postgres RLS for most services; schema-per-tenant for billing-service and payment-gateway-service (detail in ADR-0002).
  • Single AI gateway (ai-orchestrator-service) is the only egress to Vertex AI or any external model provider; edge inference on Electron via ONNX Runtime Node.

Alternatives Considered

AlternativeWhy rejected
Polyglot stack (Go for hot services, Python for AI, TS for frontend)Inflates onboarding cost; fragments shared libraries; hurts AI-tool effectiveness across the estate; small team cannot afford context-switching tax. The 5-10% perf upside on hot services does not justify the org cost.
Tauri desktop instead of ElectronLock vendors (TTLock, Salto, Assa Abloy) ship Node bindings, not Rust crates. better-sqlite3 and ONNX Runtime Node are first-class Node ecosystem. electron-builder + electron-updater give one-click signed installers across Windows/macOS/Linux that hotel IT can deploy with no extra toolchain. Hiring profile in target markets favors JS/TS over Rust. The Tauri binary-size win is real but does not move the needle for a staff-installed line-of-business app. Detail in ADR-0003.
Single BFF for all surfacesThe three surfaces have incompatible auth (anonymous vs. tenant-scoped vs. authenticated-staff), incompatible cache policy (cross-tenant aggressive vs. tenant-namespaced vs. user-scoped), and incompatible response shapes (denormalized listings vs. funnel state vs. operational dashboard). Collapsing them either over-fetches or under-protects.
AWS instead of GCPVertex AI proximity matters for our AI-first thesis; GCP cost is competitive at our scale; Cloud Run's scale-to-zero is a better fit for our long-tail services than Fargate. AWS is not strictly worse but offers no advantage that justifies switching.
Firebase-only backendOperational complexity at our service count + multi-tenancy + RLS requirements; Firestore alone cannot do the relational + RLS work that 22 services need. Firestore stays in scope for sync cursors only.
Monolith with modulesDefeats the offline-first sync surface (a monolith cannot expose per-aggregate sync semantics cleanly), defeats per-service deploy cadence, and locks the team into one runtime decision for 5 years. The 22-service split mirrors team ownership and bounded contexts.
GraphQL on the wireREST + BFF gives us better cache headers, simpler edge tooling at Kong, and a smaller dependency surface. GraphQL is not banned for internal exploration but is not the contract.
Custom ORM in domainDefeats Clean Architecture; couples domain to schema. We use repositories in infrastructure and pure entities in domain.

Consequences

Positive

  • One language across the team; one shared library set; one AI-tool prompt context.
  • Clean Architecture lets us swap NestJS, Postgres, Pub/Sub, or Vertex AI without touching the domain — each is one ADR away from being replaceable.
  • Three BFFs let each surface scale, cache, and authenticate independently.
  • Event-driven backbone gives us audit, replay, offline sync, and decoupling for free.
  • Electron desktop has the mature signed-installer + auto-update story that hotel IT departments expect, plus access to the Node ecosystem of lock-vendor SDKs.
  • GCP + Vertex AI + Cloud Run scale-to-zero is cost-aligned with our target market.

Negative

  • 22 services is a lot of operational surface for a small team; we mitigate via the per-service template (docs/standards/SERVICE_TEMPLATE.md), shared CI, and shared infrastructure modules.
  • Electron binaries are larger than Tauri; we accept this — desktop install size is not the constraint, network reliability is.
  • Hybrid tenancy (shared + schema-per-tenant for finance services) means two RLS strategies to operate; documented in ADR-0002.
  • TypeScript-only foreclose certain perf optimizations; we accept this and re-evaluate per service if a hot path proves it cannot meet SLO in TS.
  • Vendor-rotation cost for AI is real; we mitigate by funneling all AI through ai-orchestrator-service so rotation is a one-place change.

Risk register

RiskLikelihoodImpactMitigation
TS hot-path performance shortfall on a specific serviceLowMediumPer-service SLO; if breached, escalate to ADR for that single service rather than estate-wide language change
Cloud Run cold-start hurting hot-path UXMediumMediummin-instances on hot services (reservation-service, bff-tenant-booking-service, iam-service); scale-to-zero only on long-tail
Vendor-rotation pain on AI providerMediumMediumAll AI through ai-orchestrator-service; rotation is a one-place adapter swap
Vendor-rotation pain on lock vendorMediumHighLockPort abstraction (see ADR-0004)
GCP region outageLowHighDaily Cloud SQL backups; quarterly DR drill; per-tenant residency pin
Electron security regressionMediumHighRenderer hardening enforced in CI (see ADR-0003)
Pub/Sub at-least-once → duplicate eventsHighLowConsumer-side idempotency keyed by eventId + content hash; required, not optional

Operational expectations

  • Per-service SLO is documented in each service's SERVICE_READINESS.md. Default targets: 99.9% availability, p95 < 250ms for hot reads, p95 < 800ms for hot writes, sync push p95 < 1.5s for batches ≤ 100 mutations.
  • Per-service runbook is required for production sign-off; lives in services/<service-name>/RUNBOOK.md.
  • OTel everywhere — every service emits traces, metrics, and logs; traceparent propagated through HTTP, Pub/Sub headers, and SSE.
  • Schema registry — every Pub/Sub subject has a JSON Schema in packages/event-schemas/; CI enforces presence and backward compatibility.

Compliance

  • Every service must follow the layered structure under services/<service-name>/src/{presentation,application,domain,infrastructure}.
  • Every service must emit OpenAPI to services/<service-name>/openapi.json; CI enforces presence.
  • Every service must declare its conflict policy per replicable aggregate; CI enforces presence in SYNC_CONTRACT.md.
  • Every backend mutation endpoint must accept Idempotency-Key.
  • No service may import a vendor AI SDK except ai-orchestrator-service; CI enforces.
  • No service may import a lock vendor SDK except lock-integration-service; CI enforces.
  • The Electron desktop app must ship with nodeIntegration: false, contextIsolation: true, signed installer, signed auto-update; CI enforces via electron-builder config lint.
  • All cross-context references in code must use the TenantId value object; CI enforces via type lint.

References