Platform model — two products, three authorities, one daemon [Concept]
Source path: knowledge-base/diagrams/concepts/platform-model.md
# Platform model — two products, three authorities, one daemon `[Concept]`
The control plane around a running instance. Four ideas that are easy to conflate — kept apart here. Shipped code: `src/app/(gateway)`, `src/features/gateway`, `src/lib/server/gateway`, `supabase/migrations/`, `electron/`. Design doc: [GATEWAY.md](../../knowledge/operations/GATEWAY.md). Enforcement: [PERMISSIONS.md](../../knowledge/governance/PERMISSIONS.md).
## Two products, one account
A director can hold **License AND/OR Hosted**. Products are per-subscription rows, never a column — so "both at once" is a first-class case, not an edge.
```mermaid
flowchart LR
D["**director**<br/>one per person<br/>· one Stripe customer"]
D -->|"0..1"| L["subscription: **license**<br/>BYOK · self-hosted"]
D -->|"0..1"| H["subscription: **hosted**<br/>VM · provider usage included"]
L -->|"hasLicense"| App(["Electron app<br/>runs on your machine"])
H -->|"hasHosted"| VM(["Fly VM<br/>always-on host + its own domain"])
D -.->|"in platform_admins?"| Admin(["platform operator"])
```
## Three authorities — don't conflate them
"Director" names three related-but-separate things, and each answers a different question. Supabase authenticates the **person**; it says nothing about the other two.
```mermaid
flowchart LR
subgraph Platform["Platform account · Supabase"]
PR["**platform role**<br/>owner › admin › director › guest<br/>= WHAT you are on the platform"]
PA["directors + subscriptions<br/>= which PRODUCT you own"]
end
subgraph Instance["In-instance people · access-context"]
GM["instanceMembers<br/>Guest→Viewer→Contributor→Manager→Director"]
end
PA ==>|"the account owner IS<br/>the instance Director"| GM
PR -.->|"gates: the cross-Director<br/>admin plane + owner SQL"| CP(["control plane"])
PA -.->|"gates: can you run<br/>this instance type"| RT(["a running instance"])
GM -.->|"gates: what you may<br/>DO inside it"| RT
```
| Authority | Question it answers | Cardinality |
|---|---|---|
| **Platform role** | may I administer the *platform*? | **one owner per gateway** — so one across the whole product; `admin` is the invitable tier |
| **Product entitlement** | may I *run* this instance type? | per-subscription, both products can be live at once |
| **In-instance people** | what may I *do* inside this instance? | **one Director per instance** — it is one person's record |
Platform role = *operator authority*; account = *ownership + billing*; in-instance people = *collaboration roles*. The gateway never touches `instanceMembers`; the instance never touches Supabase billing.
## Knowing the platform role
Because the role lives only on the gateway, an instance has to *ask* — and every headless caller must get the same answer. So one resolver writes the verdict through onto the daemon's copy of the Director session, and everything reads it from there.
```mermaid
flowchart LR
SI["Director signs in<br/>(browser · Supabase)"] -->|"session push"| DS[("directorSession.json<br/>local-only, never replicated")]
DS -->|"resolve once"| GW{{"GET /api/account/session<br/>role · admin · owner · entitled"}}
GW -->|"cached with checkedAt"| DS
DS --> MCP(["MCP scope gate<br/>director / admin / owner"])
DS --> CR(["control-plane reads<br/>owner/admin only"])
DS --> Diag(["session diagnostics<br/>never a token"])
```
The cache is **advisory and fresh-only** (5-minute TTL): a role that outlived a revocation is never re-readable, it survives token rotation (rotating a token is not a role change), and it never outlives the session it belongs to. It buys latency and good error messages — **never** authorization. The remote route re-checks every call.
## One daemon, two hosts
The server-of-record (**E1**) is one host-agnostic Node daemon — record + engine + scheduler + loops. License forks it locally; Hosted boots the *same* code on a VM. Only *where it runs* and *how the browser reaches it* differ.
```mermaid
flowchart LR
E1["**E1** server-of-record<br/>host-agnostic daemon"]
E1 -->|"Electron forks it<br/>(License)"| Local["localhost:41999<br/>record → userData"]
E1 -->|"a Fly VM boots it<br/>(Hosted)"| Fly["<slug>.meridians.global<br/>record → /data volume"]
Local --> LC(["local renderer<br/>always-connected"])
Fly --> RC(["remote browser<br/>reconnect + sync"])
```
> **Shipped:** the managed Fly VM runtime, entitlement model, gateway app, control-plane schema, and Electron host. The hosted runtime lives in `src/lib/server/program/hosted-runtime.ts` + `src/lib/server/gateway/`; provisioning/fleet in the gateway console. Sign-in mechanics: [flows/hosted-signin](../flows/hosted-signin.md).
Open on GitHubRaw Markdown source