critical priority low complexity backend pending backend specialist Tier 1

Acceptance Criteria

`NoAccessConfigRepository` class exists and exposes `Future<NoAccessConfig> getConfig()` method
On first call, `getConfig()` fetches config from Supabase remote config table/function
Subsequent calls within 5 minutes return the cached `NoAccessConfig` without a network call
After 5-minute TTL expires, the next call triggers a fresh Supabase fetch
On any network error, timeout, or malformed response, `getConfig()` returns a `NoAccessConfig` built from `kFallbackAdminPortalUrl` and `kDefaultBlockedRoles`
Repository has no dependency on Flutter widgets — it is a plain Dart class
Unit tests (separate task) can mock the Supabase client and verify fallback behavior
`flutter analyze` reports zero warnings or errors on the file

Technical Requirements

frameworks
Flutter
Dart
Supabase Flutter SDK
apis
Supabase remote config table or `rpc` function returning admin portal URL and blocked roles
data models
NoAccessConfig
kFallbackAdminPortalUrl
kDefaultBlockedRoles
performance requirements
Cache hit path must complete synchronously (no async gap after first fetch)
Network fetch must have an explicit timeout of ≤5 seconds to prevent blocking the app startup on slow connections
security requirements
Repository must not log or expose the admin portal URL in debug output
Supabase query must be row-level-security compliant — anonymous/authenticated read only, no write access
Fallback must be silent (no error UI) — the repository logs the error internally but returns valid fallback data

Execution Context

Execution Tier
Tier 1

Tier 1 - 540 tasks

Can start after Tier 0 completes

Implementation Notes

Implement the TTL cache using a private `_cachedConfig` field and a `_cacheExpiry` `DateTime?` field. Check `DateTime.now().isBefore(_cacheExpiry!)` on each `getConfig()` call. Inject a `SupabaseClient` (or an abstract `ISupabaseClient`) through the constructor for testability — avoid accessing `Supabase.instance.client` directly inside the class. The Supabase remote config source could be a dedicated `app_config` table with a single row, or a Postgres function — coordinate with the backend team on the exact table/RPC name.

Wrap the entire fetch in a `try-catch` catching both `PostgrestException` and `TimeoutException`. Use `.timeout(const Duration(seconds: 5))` on the Supabase future.

Testing Requirements

Unit tests using `flutter_test` and `mocktail`. Test scenarios: (1) successful Supabase response maps to correct `NoAccessConfig` fields; (2) second call within TTL returns cached value without invoking Supabase client; (3) Supabase throws `SocketException` → fallback constants returned; (4) Supabase returns malformed JSON → fallback constants returned; (5) after TTL expires (advance fake clock), next call re-fetches from Supabase. Use `fake_async` or manual `DateTime` injection to control the TTL clock in tests.

Epic Risks (2)
high impact medium prob technical

Supabase remote config may be unavailable at app startup (network error, cold start), causing the repository to return no blocked-role list. If the fallback is empty, blocked users could access the app.

Mitigation & Contingency

Mitigation: Define a local constants fallback list of blocked roles compiled into the app binary. Remote config enriches or overrides this list when available.

Contingency: If remote config repeatedly fails in production, pin the blocked-role list to local constants only and disable remote override until the Supabase config endpoint is stabilised.

medium impact low prob dependency

url_launcher package behaviour differs between iOS and Android (e.g. canLaunchUrl returning false on some Android configurations), leading to silent failures when the admin portal link is tapped.

Mitigation & Contingency

Mitigation: Run canLaunchUrl check before every launch attempt and surface a descriptive inline error message (e.g. 'Could not open link — visit admin.example.org manually') when the check fails.

Contingency: If canLaunchUrl is consistently unreliable on a target platform, replace the tap-to-open pattern with a copyable text field showing the URL as a fallback.