entitlementInvalidationWatcher function

  1. @Riverpod(keepAlive: true)
Future<void> entitlementInvalidationWatcher(
  1. Ref<Object?> ref
)

Background watcher that subscribes to Supabase Realtime on the admin_grants and subscriptions tables and invalidates the cached currentEntitlementsProvider on any change. RLS still filters which rows the client receives — only grants/subs targeting the current user (or their org, as an admin) will trigger an event.

The watcher is lifecycle-managed: it subscribes on the first read, tears down on provider dispose, and rebuilds when auth state changes so a sign-in/sign-out cleanly swaps the user context.

Wire into the app by reading the provider once at startup (e.g. from main.dart's _initServicesAsync). Reading it is enough — the side effect is the subscription.

Implementation

@Riverpod(keepAlive: true)
Future<void> entitlementInvalidationWatcher(Ref ref) async {
  ref.watch(authStateProvider);

  final client = Supabase.instance.client;
  final user = client.auth.currentUser;
  if (user == null) return;

  late final RealtimeChannel channel;

  void invalidate() {
    try {
      ref.invalidate(currentEntitlementsProvider);
    } catch (e) {
      dev.log(
        'Failed to invalidate entitlement cache: $e',
        name: 'EntitlementInvalidationWatcher',
      );
    }
  }

  channel = client
      .channel('public:entitlement-invalidation:${user.id}')
      .onPostgresChanges(
        event: PostgresChangeEvent.all,
        schema: 'public',
        table: 'admin_grants',
        callback: (_) => invalidate(),
      )
      .onPostgresChanges(
        event: PostgresChangeEvent.all,
        schema: 'public',
        table: 'subscriptions',
        callback: (_) => invalidate(),
      );

  channel.subscribe();

  ref.onDispose(() {
    unawaited(client.removeChannel(channel));
  });
}