docs(wiki): capture cloud-service SaaS requirements (postponed)

Multi-tenant SaaS layered on the offline model: link-up monitoring of the
signed ledger, device status, financials; one admin → many sites; per-site
secret custody; recurring fee. Records the four tensions, the confirmed
secrets boundary (sync creds + device-password escrow + app identity, NOT
the signing key), and the two in-discussion corrections that stand (NetBird
already solves booth isolation; remote barrier-open is pulseOpen-and-signed,
driven by the unmanned future). status: open, postponed.

Claude-Session: https://claude.ai/code/session_01Xcm6ikLgGoCxxHrxtjkk5V
This commit is contained in:
2026-07-13 14:51:01 +02:00
parent bb365b5d6e
commit ba7538aeb5
3 changed files with 203 additions and 1 deletions
+181
View File
@@ -0,0 +1,181 @@
---
type: decision
tags: [parking, cloud, saas, multi-tenant, monitoring, netbird, threat-model, offline-first]
sources: []
updated: 2026-07-13
status: open
---
# Cloud service — multi-tenant SaaS for fleet monitoring & control
> **Status: postponed (2026-07-13).** Captured as context, not a commitment. This records an
> early requirements/architecture discussion so it isn't re-derived from scratch later. No app
> code, no schema. Two of the initial requirements were **corrected in-discussion** (see
> "Corrections" below) — read those before treating any first-pass answer as settled.
## The idea
The offline backup model we ship today is the right **tradeoff for offline sites** and stays.
On top of it, the user wants an **online, multi-tenant SaaS** — the "**cloud service**" — that
subscribing park sites connect to for **real-time (link-up) monitoring**: the signed ledger,
device status, financial reports, and whatever else a site reports. One **admin owns more than
one site** (a portfolio). The cloud also **custodies per-site secrets**. Business model: recurring
per-site monthly/yearly fee — a revenue line the offline appliance alone can't produce.
This is the customer-facing evolution of the off-site control plane that [[fleet-deployment-komodo]]
already stood up (**Komodo Core**, **NetBird** mesh, **Gitea** registry). Much of the transport and
Tier-0 reasoning there carries over directly; this page is about turning that internal ops plane into
a **multi-tenant product**.
## The four hard tensions (what makes a naïve SaaS wrong here)
The booth's two governing forces ([[offline-first]], [[threat-model]]) plus the signed ledger
([[append-only-event-chain]]) make the "obvious" SaaS shape wrong. Four tensions dominate:
1. **Offline-first vs. real-time monitoring.** The cloud must **never be in the critical path** of
entry/exit/payment/barrier ([[offline-first]]). It is a **read-mostly mirror + control-plane**, fed
by the booth when the link is up, tolerant of hours/days offline, and unable to block booth
operation by being down. "Real-time" = *near*-real-time when up, **gracefully stale** when not —
and the UI must show staleness **honestly** (last-seen everywhere), never paint a dark site green.
2. **The signed ledger must stay *verifiable* in the cloud, not merely displayed.** If subscribers
see "their ledger" in the cloud, the cloud copy must be **re-verified server-side** — re-check the
hash chain + signatures on ingest, flag gaps/breaks/forks loudly. The [[threat-model|operator-as-
adversary]] extends upward: an operator may want the cloud *not* to see certain events, so the sync
must be **gap-evident** (sequence continuity). This is both the anti-tamper mechanism **and** a
headline feature — *"we can prove your revenue record wasn't altered, even by your own night
shift."* See [[reconciliation]] (this is reconciliation, productised).
3. **Secrets for every site — the scariest requirement.** A central secret store for hundreds of
sites is a single juicy target. The custody boundary must be deliberate — see "Secrets boundary".
4. **Multi-tenancy under operator-as-adversary — now at two levels.** One admin, many sites ⇒ a new
**portfolio-owner** role *above* the existing per-site roles ([[local-jwt-auth]] admin/operator/
cashier/readonly). Row-level tenant isolation must be **airtight** — a bug now leaks *another
company's* revenue, not just an intra-site escalation. Every row carries `tenant_id` + `site_id`,
non-optional in the query path (not a filter someone can forget). **Cloud identity is separate from
booth-local auth** — the booth keeps its offline JWT/bcrypt login untouched; a site never
authenticates its *users* against the cloud (that would break [[offline-first]]).
## Secrets boundary (settled-in-principle 2026-07-13)
User confirmed the cloud custodies **three** classes — and **not** the crown jewel:
| Class | Cloud custodies? | Notes |
| --- | --- | --- |
| Sync/connection creds + ledger **public** (verify) key | ✅ yes | Per-site uplink credential + the public half to *verify* signatures. Smallest blast radius. |
| **Device/controller passwords** (Dingtian `relay_pw`, camera creds, push tokens) | ✅ yes, as **escrow** | Solves the real pain: lost `relay_pw` after a DB reset ([[dingtian-relay]]). See escrow rules below. |
| App/admin identity (portfolio login) | ✅ yes | Cloud-side identity for portfolio admins. Separate from booth-local auth. |
| Ledger **signing** key / [[atecc608\|ATECC608]] private key, LUKS/TPM material | ❌ **never** | Centralising the signer **kills the anti-fraud model** ([[append-only-event-chain]], [[hardware-signer-options]]). The user did **not** pick this. |
**How device-password escrow must work (so it earns its keep instead of becoming the breach):**
- **Envelope encryption, per-tenant DEK**, DEKs wrapped by a KMS master key; a DB dump is ciphertext,
every decrypt is KMS-audited.
- The cloud is an **escrow, not an operational credential store**. Its job is "**give the booth back
its `relay_pw`** after a wipe," *not* "the cloud logs into the Dingtian." Decryption happens **at the
booth** (booth fetches its own wrapped blob, unwraps locally); ideally the cloud never holds
plaintext device secrets in memory. This keeps the [[dingtian-http-api-unauthenticated|unauthenticated-
CGI]] exposure host-local.
- **The booth threat model applies upward:** writes to escrow are append/version ops the operator
can't silently rewrite; reads are logged where the operator can't scrub them.
- Sellable as: *"your device credentials survive any wipe, encrypted so even we can't read them in
bulk."*
## Corrections made in-discussion (2026-07-13) — read these
The first pass argued *against* the user's two boldest choices ("cloud reaches into the booth";
implicitly, "no remote barrier open"). **The user corrected both, and the corrections stand.**
### Correction 1 — NetBird already solves the isolation objection
Initial worry: a cloud tunnel *into* the booth is a new inbound attack surface on every site. **But
park-buzi is already monitored remotely over a NetBird private mesh** (WireGuard) — the same
mesh [[fleet-deployment-komodo]] uses. The booth **dials out** to join the overlay; **nothing is
exposed** on the booth PC. So "cloud reaches booth" is the booth-dialed reverse-channel pattern
**already in production**, not a new hole. The objection is **withdrawn.** What it *shifts* rather than
removes:
- Trust moves to the **overlay's identity/ACL layer**: "cloud can reach the booth" now means "any
peer the mesh authorizes can reach the booth host." **Mesh ACLs must enforce the same tenant
isolation as the app layer** — site A's admin never gets a route to site B's booth. Multi-tenant
isolation in a different hat.
- **The access-controller VLAN still holds:** the mesh terminates at the **host**, not the controller
segment. A cloud peer talks to the booth API; the **booth** talks to the Dingtian/UHPPOTE
([[network-isolation]], [[access-direction-is-per-relay]]). The cloud never gets an L3 route to the
UDP relay.
- **NetBird's control plane joins the trust base** (self-hosted = another service to harden; their
SaaS = a third party who can authorize peers). A conscious call, not an architecture change.
### Correction 2 — remote barrier-open is *compatible* with barrier-not-a-door, and the unmanned future *requires* it
Initial worry: the cloud must never open a barrier. **The user's driver is the [[autonomous-direction|
unmanned-site]] future** — no operator on-site; if the exit reader or payment dies, *someone* must open
the barrier remotely rather than trap people ("we can't take hostages because a stupid device is not
responsive"). This is **right**, and it does **not** violate [[barrier-not-a-door]]:
- That rule was **never** "no remote open." It forbids driving the barrier as a **timed auto-close**
("open for N ms"); physical safety (loop-detector, anti-crush reversal) lives in the **barrier
firmware**. A remote human pressing "open" is an **intent expression** — exactly `pulseOpen`. It's
the [[fail-state-safety|exit-fails-open]] value, triggered by a remote human instead of a power-loss.
- Constrain the **how**, not the whether (this is the command where [[threat-model|operator-as-
adversary]] bites hardest — a remote "let this car out free" is the classic fraud):
- **Every remote open is a first-class signed ledger event** ([[append-only-event-chain]]): appended,
hash-chained, signed, with **actor** (which cloud identity), **reason code**, and **site/relay**.
Control power and audit come as a **pair** — the same discipline [[setup-relay-test]] and
[[booth-exit-flow|audited re-open]] already apply locally.
- A **distinct, high-privilege capability**, not bundled into "monitoring" — a readonly portfolio
viewer can't open barriers.
- **The booth stays the enforcer:** cloud sends *intent*; the booth validates (for-me? authorized
peer? signed?) and issues `pulseOpen` to its own relay. Cloud never touches the relay.
- **Cloud can't be the *sole* egress path.** A fully unattended site needs a **local fail-open on
host-loss** + physical override too — offline-first means the cloud is a *convenience* remote-open
path, not the *only* one, or you've recreated "device down = hostages" one layer up.
> **Emergent tenet:** an unattended site is a **higher** safety bar than an attended one, not a lower
> one. Every local failure mode (barrier stuck, payment dead, network down) needs an answer that
> **doesn't require the cloud**; the cloud makes resolution *nicer*, not *possible*. Fold into
> [[autonomous-direction]] and [[fail-state-safety]] when this is picked up.
## What looks straightforward (agreed quickly)
- **Transport:** the existing **NetBird overlay** (booth-dialed, nothing exposed) — not a bespoke
channel. Reuses [[fleet-deployment-komodo]].
- **Sync:** **booth-push, verify-on-ingest** — booth streams ledger + device telemetry
([[device-events]]) + snapshot metadata + financial data outbound; cloud **re-verifies the chain +
signatures** and flags gaps.
- **Staleness first-class in the UI:** every site tile shows last-seen; a dark site is visibly stale.
- **DB:** almost certainly **PostgreSQL** — already the named deferred sync target ([[drizzle-orm]],
[[technology-stack]]); the Drizzle schemas are meant to port to it.
## The genuinely open questions (postponed — pick up here)
1. **What does "real-time" mean to the buyer?** Live-ish (seconds, streaming uplink → **heavier
booth**) vs. every-few-minutes rollups (cheap, still sells "monitoring"). This gap is **most of the
engineering cost** and drives how heavy the booth-side uplink must be.
2. **Financial reports computed where?** Cloud **re-derives** revenue from the verified ledger →
independently trustworthy (*"we don't take the booth's word for it"*) but the cloud must implement
the [[tariff]] pricing logic. Vs. booth sends **pre-computed rollups** (cheaper, but trusts the
booth's math). Lean: **cloud re-derives** — the whole point of [[threat-model|operator-adversary]]
is not to trust the site's self-report ([[reporting-analytics]] is already "projections over the
signed log").
3. **Hosting + licensing.** The booth stack is deliberately all-MIT/Apache/BSD ([[technology-stack]]);
a SaaS the user **hosts** has more freedom (like the [[fleet-deployment-komodo|Komodo GPL]] /
[[vision-service|AGPL]] self-host exceptions) — but anything that ever ships **on-premise** re-binds
the constraint.
4. **Custodianship is leverage *and* liability.** Holding other companies' financial records + device
secrets is what makes the subscription **sticky** — and what pulls in **backups, retention policy,
breach disclosure, data-residency**. A deliberate "yes, we want to be the custodian" call, with the
obligations that implies. (Cloud/Core is a **Tier-0 asset** for the whole fleet — the same bar
[[fleet-deployment-komodo]] already sets for Core.)
## Relates
- [[fleet-deployment-komodo]] — the off-site control plane (Komodo Core + NetBird) this productises;
Core-as-Tier-0 reasoning carries over.
- [[autonomous-direction]] — the unmanned future that *drives* remote barrier-open (Correction 2).
- [[reconciliation]] — the cloud *is* reconciliation, productised (verify-on-ingest, gap-evidence).
- [[append-only-event-chain]] / [[hardware-signer-options]] — why the **signing** key stays on the
booth even as everything else centralises.
- [[threat-model]] / [[offline-first]] — the two forces every tension above traces back to.
- [[network-isolation]] / [[access-direction-is-per-relay]] — why the mesh terminates at the host.
+2 -1
View File
@@ -7,7 +7,7 @@ updated: 2026-07-02
# Index
Content catalog for the wiki. Start at [[overview]]. Maintained on every ingest.
Counts: 4 sources · 19 entities · 47 concepts · 7 decision records.
Counts: 4 sources · 19 entities · 47 concepts · 8 decision records.
## Overview & navigation
- [[overview]] — the top-level synthesis and entry point.
@@ -127,6 +127,7 @@ Counts: 4 sources · 19 entities · 47 concepts · 7 decision records.
- [[open-questions]] — 9 open items (procurement + JWT key + FX + pay-station money corners); ESP32 device auth deferred.
- [[access-controller-button-flow]] — ✅ RESOLVED: Dingtian decoupled inputs enable ticket-first entry (was a UHPPOTE/ZKTeco blocker).
- [[autonomous-direction]] — roadmap: toward fully unmanned (no booth); reshapes threat model + fail-state.
- [[cloud-service-saas]] — 📌 POSTPONED: multi-tenant SaaS for fleet monitoring/control; productises the NetBird/Komodo control plane. Four tensions (offline-first vs real-time, verifiable-ledger-in-cloud, secrets custody, two-level tenancy); signing key stays on the booth; NetBird already solves isolation; remote barrier-open is `pulseOpen`+signed (the unmanned driver).
- [[dingtian-vs-mqtt]] — transport choice: direct HTTP/UDP now, MQTT parked until multi-lane scale.
- [[session-model]] — business layer start: session = projection; transient-first; pay-on-foot. New event types.
- [[vision-service]] — build a host-side ANPR + vehicle-verification service; replaces edge-LPR; scoped AGPL exception.
+20
View File
@@ -2540,3 +2540,23 @@ to no reset-db category, silently surviving even `--all`. Added `--diagnostics`
tariff_drafts under `--config`, and a drift guard that refuses to run when any table is
uncategorized ([[local-dev-workflow]], [[appliance-provisioning]] §7d). 8 new tests
(3 button-light backoff, 5 coalescing); guard + both new wipes verified on a scratch DB.
## [2026-07-13] decision | Cloud service — multi-tenant SaaS (postponed, context captured)
From a design conversation, not a source. The user floated an online, multi-tenant SaaS (the
"cloud service") on TOP of the offline backup model (which stays, as the offline-site tradeoff):
subscribing park sites get real-time (link-up) monitoring of the signed ledger, device status,
and financial reports; one admin owns many sites; the cloud custodies per-site secrets; recurring
per-site fee = a revenue line. Recorded as [[cloud-service-saas]] (status: open, POSTPONED per the
user) so it isn't re-derived later. It productises the off-site control plane already stood up in
[[fleet-deployment-komodo]] (Komodo Core + NetBird). Captured: the four hard tensions (offline-first
vs real-time; the ledger must be VERIFIABLE not just displayed in the cloud; central secret custody;
two-level tenancy under operator-as-adversary), the secrets boundary the user confirmed (sync creds
+ device-password ESCROW + app identity — but NOT the signing/ATECC608 key, which stays on the
booth), and TWO in-discussion corrections that stand: (1) NetBird already solves the "cloud reaches
booth" isolation objection — park-buzi is monitored that way today, booth-dialed, nothing exposed;
(2) remote barrier-open is COMPATIBLE with [[barrier-not-a-door]] (it's `pulseOpen`/intent, never
timed-close) and is DRIVEN by the [[autonomous-direction]] unmanned future — gated as a distinct
privilege + a signed ledger event with actor+reason, with the booth as enforcer and a local
fail-open that can't depend on the cloud. Four open questions parked (real-time definition, where
reports are computed, hosting/licensing, custodianship-as-liability). Cross-linked; index count
7→8 decisions.