Summary
Client-side enforcement of the closed-beta gate. An authenticated account whose
profiles.access_status is not approved is held on a lock screen and cannot
reach the app until an admin approves it or the user redeems an invite code.
This is the mobile half of the backend feature feat-beta-access-gate.
Files Changed
lib/features/access/notifiers/access_gate_notifier.dart—AccessGateNotifieraccessGateProvider. Reads ownaccess_status, exposesredeem(code)via theredeem_invite_codeRPC, and subscribes to Realtime on the own profile row.
lib/features/access/screens/access_pending_screen.dart— lock screen (pending / rejected states, invite-code field, manual + 20s periodic refresh, logout).lib/core/router/app_router.dart—/access/pendingroute,RouterNotifierlistens toaccessGateProvider, redirect gates non-approved accounts.lib/l10n/app_{en,it,es}.arb—access*keys (see localization).
Key Decisions
- Dedicated provider, not the
Usermodel.access_statusis read by a standaloneaccessGateProviderrather than threaded through the largeUsermodel and its serialization — minimal blast radius. - Fail closed. If the status read errors, the gate treats the account as
pending(locked, recoverable via refresh) rather than letting it through. - Realtime + polling. Live unlock uses a Postgres-changes subscription on the
caller’s own
profilesrow; a 20s periodic refresh on the pending screen is the fallback ifprofilesis not in the realtime publication. unknown/loadingcount as not-approved in the redirect, so an unapproved user never flashes the app; the pending screen shows a spinner while resolving, so approved users only see a brief loader before/home.
Architecture Notes
auth becomes authenticated
→ AccessGateNotifier.load() (reads profiles.access_status for auth.uid())
→ RouterNotifier listens to accessGateProvider
→ GoRouter.redirect: status != approved → /access/pending
status == approved → /home
AccessPendingScreen
→ invite code → accessGateProvider.redeem() → redeem_invite_code RPC
→ ok → status=approved → redirect to /home
→ Realtime UPDATE on own profile (admin approval) → status flips live
→ periodic 20s load() fallback
Realtime prerequisite
Live unlock needs
public.profilesin thesupabase_realtimepublication. If not enabled, approval still works via the manual/periodic refresh. Enabling it must keep RLS so a user only receives their own row — do not broadcast all profile changes.
Invite deep link
The approval/invite email links to
NEXT_PUBLIC_APP_INVITE_URLwith?code=. Wiring that universal/app link to openAccessPendingScreenwith the code prefilled is a remaining task (see feat-beta-access-gate P1).
Related
- feat-beta-access-gate — backend gate, invite_codes, RPC, admin flow
- database-schema —
profiles.access_status,redeem_invite_code() - localization — the
access*l10n keys