Data Flow
This page traces the major data flows through the platform from store upload to downstream consumption.
Ingestion pipeline (hot landing path)
The core pipeline processes POST /v1/sync/batch requests from the Flutter POS app.
Request shape
Pipeline steps (BatchApplyService.ApplyAsync())
Database: ingestion schema
Key guardrails
deletewrites a tombstone (op='delete',payload=NULL), never a hard delete- NUL-byte rejection:
BatchValidatorrejects JSON payloads with\u0000(PostgreSQL jsonb can't hold U+0000) — a single bad row would otherwise reject the entire batch ingest_approle (NOLOGIN): The request path doesSET ROLE ingest_appfor RLS. This role is never used as a connection username
DataHub read substrate
DataHub (src/SalonCloud.DataHub.Infrastructure) is a migration-only project — it has no runtime service. It creates PostgreSQL views in the data_hub schema, all with security_invoker = true.
How it works
Downstream services read DataHub views — never ingestion.ingest_rows directly. The views are live queries, always current, with no replication lag or materialization.
View categories
Fail-safe casts
A critical invariant: a cast raising in a view's target list takes down the query for every store. DataHub provides fail-safe functions that return NULL instead of raising:
All are IMMUTABLE STRICT PARALLEL SAFE. Every typed view uses these for payload casts. Consuming code must IsDBNull-guard nullable columns.
PII roles
Current consumers:
- Booking — reads via
datahub_maint(cross-tenant; adds its ownWHERE store_id = ...) - Accounts — reads
store_settingsviadatahub_maint - No per-store RLS consumer exists yet; future store-facing query APIs should use
datahub_app
Capability mirror
Ingestion and Booking maintain local mirrors of store state from Accounts via a pull-based pattern.
Key property: pull, not push. A downed consumer catches up naturally on restart by reading from its cursor. The cursor is a keyset over a monotonic sequence, so store state changes mid-scan are still seen.
Accounts can nudge consumers for lower latency via POST /internal/capabilities/nudge.
Booking flow
Public consumer endpoints (/v1/book/)
Booking reconciliation
Booking runs a BookingReconcileHostedService (every 30s) that reads data_hub.appointments for appointments with cloud_booking_id set, reconciles them against local booking state, and sends notifications for changes.
Write-back to store
When Booking creates/modifies an appointment, it writes an outbox record. The store app polls GET /v1/booking/outbox and acks via POST /v1/booking/outbox/ack.
Store lifecycle
Push and messaging
- Push delivers in-app nudges to store devices (new booking alert, etc.)
- Messaging handles email (OTP codes, booking confirmations) and SMS (OTP, status webhooks). In development, both use logging transports instead of real providers
More on deployment and operations in Operations.