SalonCloud — Quickstart
SalonCloud is a monorepo of independently-deployable .NET 10 microservices that form the cloud backend for a salon POS platform. It receives incremental uploads from a Flutter store app, lands them in PostgreSQL (AWS RDS), and exposes typed, PII-governed read models to downstream services (booking, messaging, push, admin).
What this platform does
- Receives store data — the Flutter POS app uploads row-level changes (appointments, tickets, customers, payments, staff, services, settings) via
POST /v1/sync/batch - Lands idempotently — last-write-wins (LWW) upsert on
(changed_at, seq)with deduplication, clock-skew clamping, and suppression detection - Publishes read models —
data_hubschema providessecurity_invokerPostgreSQL views with typed, null-safe projections and PII-tiered access roles - Powers booking — consumer-facing booking API (availability, OTP, holds, bookings, self-service management)
- Drives operations — operator admin console (store management, RBAC, diagnostics, tasks) via
AdminBFF andEnginehealth/rule engine - Routes notifications — push nudges and OTP/email/SMS messaging
Prerequisites
- .NET 10 SDK (pinned in
global.json) - Docker — required for integration tests (Testcontainers spins up PostgreSQL) and for
docker compose - Node.js — only needed for CDK synth tests (JSII)
Quick start
The API auto-applies EF migrations on startup (RunMigrationsOnStartup, default on).
Service inventory
Each service has a companion *.Infrastructure project for its data layer (migrations, repositories, bridges). Infrastructure projects exist for: Ingestion, Accounts, Booking, Admin, Engine, Push, Messaging, Observability, and DataHub.
Project layout
Key design principles
- One service = one bounded context = one PostgreSQL schema. Never cross schemas directly. The sole exception:
data_hubis the blessed shared read substrate viasecurity_invokerviews. - Pre-production — no live customers, no production data. Freely squash migrations, rename tables, wipe DB. No backward-compatibility shims.
- Pull-based capability sync — services poll Accounts for store state rather than receiving pushed events. Downed services catch up naturally.
- Row-level LWW idempotency — upsert guarded by
(changed_at, seq), not batch id. The C# deduplication ordering must match the SQL guard exactly.
Critical invariants
These are the silent-breakage risks — read before modifying core pipeline code:
store_idisuuid,device_idistext(not guaranteed UUID)changed_atis unix milliseconds from the sync envelope; payloadDateTimeis unix seconds — different scales- LWW dedupe must match SQL guard ordering — divergence = silent data loss
- Landing must never silently drop a row —
RETURNINGkeys, suppression detection, WARN logs - A cast in
data_hubviews must never raise — always usetry_*functions (try_bigint,try_int,try_numeric,try_timestamp,try_jsonb) - Consumers must
IsDBNull-guard nullable columns AND traceWHERE/JOINpredicates — three-valued logic can silently drop rows before C# sees them deletewrites a tombstone (op='delete',payload=NULL), never a hard delete
Where to go next
- Architecture — service catalog, cross-service patterns, design decisions
- Data Flow — ingestion pipeline, DataHub, capability mirror, booking flow
- Operations — Docker, CI/CD, AWS CDK, configuration
- Testing — test framework, integration patterns, test categories