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 GitHubRaw Markdown source