MeridiansMeridians

Platform model — two products, three authorities, one daemon [Concept]

From the Meridians Wiki · Public · Maintained · joint

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 🔒. Enforcement: 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.

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.

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
AuthorityQuestion it answersCardinality
Platform rolemay I administer the platform?one owner per gateway — so one across the whole product; admin is the invitable tier
Product entitlementmay I run this instance type?per-subscription, both products can be live at once
In-instance peoplewhat 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.

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.

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["&lt;slug&gt;.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.