Access gate [Flow]
From the Meridians Wiki · Public · Maintained · joint
AccessProvider (lib/core/reducer/access-context) wraps every route and decides what renders before the page. The decision ladder:
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).isLocalRequestis 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.