MeridiansMeridians

Access gate [Flow]

Source path: knowledge-base/diagrams/flows/access-gate.md

# Access gate `[Flow]`

`AccessProvider` (`lib/core/reducer/access-context`) wraps every route and decides what renders **before** the page. The decision ladder:

```mermaid
flowchart TB
    Enter([any route]) --> Fresh{fresh master?}
    Fresh -->|yes| Claim["claim the instance<br/>via Supabase (no local PIN)"]
    Fresh -->|no| Public{public route?}
    Public -->|yes| Page["render the page"]
    Public -->|no| Signed{signed in?}
    Signed -->|no| Portal["UserPortal<br/>member sign-in · guest entry"]
    Signed -->|yes| Role{role allows route?}
    Role -->|no| Deny["NoAccess panel"]
    Role -->|yes| Page

    Claim -.->|"AFTER sign-in,<br/>non-blocking"| Ent{gateway entitled?}
    Ent -->|denied| Locked["locked portal"]
    Ent -->|"entitled OR unreachable"| Page
```

- **Role is decided by origin** (`getSyncRole`): localhost / Electron / canonical web = **master** (owns its encrypted IndexedDB); a tunnel origin = **client** (joined a host). `isLocalRequest` is the server-side authority check.
- **Auth is local-first.** Sign-in resolves entirely on the instance (`beginLogin → login → signInMember`); the Director's credential lives on Supabase. The **entitlement check is a separate, non-blocking layer** run *after* the session exists — an unreachable gateway **keeps** access. Never put a gateway call in the sign-in path.
- Portal + onboarding render inside the shared `GatewayShell`, so signing in flows straight into the home console.
- `<ScenarioHost/>` mounts at the app root, **outside** the gate, so the master keeps hosting every live game regardless of route. Inert on clients.

Public routes: `/`, `/join`, `/white-paper`, `/faq`, `/scenario`. Full journey: [PRODUCT_BEHAVIOURS.md](../../knowledge/product/PRODUCT_BEHAVIOURS.md#from-first-load-to-the-studio--auth-is-local-first).
Open on GitHub

Raw Markdown source