Monetizing Flutter Apps with RevenueCat: Subscriptions Done Right
Flutter Development
9 min read
September 15, 2025

Monetizing Flutter Apps with RevenueCat: Subscriptions Done Right

Implement in-app subscriptions without the headache. Learn how RevenueCat handles the hard parts of IAP while you focus on building a great paywall experience.

Muhammad Nabi Rahmani

Muhammad Nabi Rahmani

Flutter Developer passionate about creating beautiful mobile experiences

Monetizing Flutter Apps with RevenueCat: Subscriptions Done Right

In-app purchases are one of the most painful parts of mobile development. Apple and Google have completely different APIs, receipt validation is a nightmare, and subscription state management is full of edge cases. RevenueCat makes all of that disappear.

Why RevenueCat Instead of Raw IAP

If you've ever tried implementing in-app purchases directly, you know the pain:

  • Different APIs for iOS and Android
  • Server-side receipt validation
  • Handling subscription renewals, cancellations, and expirations
  • Grace periods and billing retry logic
  • Sandbox testing that never works quite right

RevenueCat wraps all of this into a clean SDK. You call Purchases.purchasePackage(package) and it handles everything else. Entitlements are cached locally, so premium checks work offline too.

Architecture: Where Everything Lives

Following Clean Architecture, here's how monetization fits:

lib/src/feature/premium/
  domain/
    subscription_status.dart     # Entities and enums
  data/
    premium_repository.dart      # RevenueCat SDK wrapper
  application/
    premium_service.dart         # Business logic
  presentation/
    premium_controller.dart      # State management
    paywall/
      paywall_screen.dart        # Purchase UI

The Domain: What Subscription States Exist

enum SubscriptionStatus { active, paused, expired, lifetime }

class SubscriptionState {
  final bool isPremium;
  final SubscriptionStatus status;
  final DateTime? expirationDate;
  final String? productId;

  static const free = SubscriptionState(
    isPremium: false,
    status: SubscriptionStatus.expired,
  );
}

The Repository: A Thin SDK Wrapper

class PremiumRepository {
  Future<CustomerInfo> getCustomerInfo() async {
    return Purchases.getCustomerInfo();
  }

  Future<Offerings> getOfferings() async {
    return Purchases.getOfferings();
  }

  Future<CustomerInfo> purchase(Package package) async {
    return Purchases.purchasePackage(package);
  }

  Future<CustomerInfo> restore() async {
    return Purchases.restorePurchases();
  }
}

This is intentionally thin. The repository just wraps RevenueCat SDK calls. No business logic, no validation.

The Service: Entitlement Logic

class PremiumService {
  final PremiumRepository _repo;

  Future<bool> isPremium() async {
    final info = await _repo.getCustomerInfo();
    return info.entitlements.active.containsKey('premium');
  }

  Future<SubscriptionState> getSubscriptionState() async {
    final info = await _repo.getCustomerInfo();
    final entitlement = info.entitlements.active['premium'];

    if (entitlement == null) return SubscriptionState.free;

    return SubscriptionState(
      isPremium: true,
      status: entitlement.willRenew
          ? SubscriptionStatus.active
          : SubscriptionStatus.expired,
      expirationDate: entitlement.expirationDate,
      productId: entitlement.productIdentifier,
    );
  }
}

The Premium Gate Widget

Gate premium features with a simple widget:

class PremiumGate extends ConsumerWidget {
  final Widget child;
  final Widget fallback;

  const PremiumGate({required this.child, required this.fallback});

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final isPremium = ref.watch(premiumControllerProvider).valueOrNull ?? false;
    return isPremium ? child : fallback;
  }
}

// Usage:
PremiumGate(
  child: JournalEditor(),     // Premium users see the editor
  fallback: UpgradePrompt(),  // Free users see upgrade prompt
)

Backend Sync with Supabase Webhooks

For cross-device subscription tracking, set up RevenueCat webhooks that write to Supabase:

CREATE TYPE sub_status AS ENUM ('ACTIVE', 'PAUSED', 'EXPIRED', 'LIFETIME');

CREATE TABLE public.subscriptions (
  id uuid DEFAULT gen_random_uuid() PRIMARY KEY,
  user_id uuid NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
  status sub_status NOT NULL,
  period_end_date timestamptz,
  sku_id text NOT NULL,
  last_update_date timestamptz NOT NULL DEFAULT now()
);

ALTER TABLE public.subscriptions ENABLE ROW LEVEL SECURITY;
CREATE POLICY "Users can view own subscription"
  ON public.subscriptions FOR SELECT USING (auth.uid() = user_id);

The Edge Function

Deno.serve(async (req) => {
  // Verify RevenueCat webhook token
  const authToken = req.headers.get("Authorization")?.split(" ")[1];
  if (authToken !== Deno.env.get("RC_TOKEN")) {
    return new Response("Unauthorized", { status: 401 });
  }

  const body = await req.json();
  const event = body.event;

  // Map RevenueCat events to subscription status
  const status = mapEventToStatus(event.type);
  if (!status) return new Response("ignored", { status: 200 });

  // Upsert subscription record
  await supabase.from('subscriptions').upsert({
    user_id: event.app_user_id,
    status: status,
    period_end_date: event.expiration_at_ms
      ? new Date(event.expiration_at_ms) : null,
    sku_id: event.product_id,
  });

  return new Response("saved", { status: 200 });
});

RevenueCat Event Mapping

EventStatus
INITIAL_PURCHASEACTIVE
RENEWALACTIVE
UNCANCELLATIONACTIVE
CANCELLATIONEXPIRED
EXPIRATIONEXPIRED
SUBSCRIPTION_PAUSEDPAUSED
NON_RENEWING_PURCHASELIFETIME
BILLING_ISSUEIgnore
TRANSFERIgnore

Two Strategies: Choose Your Path

Strategy A: Anonymous (No Backend)

Purchases are tied to the Apple ID or Google Play account. RevenueCat SDK caches entitlements locally. No user accounts needed. Perfect for simple apps.

Strategy B: User-Synced (With Supabase)

Purchases sync to your database via webhooks. Users have accounts, subscription data is centralized, and you get a dashboard for admin purposes. Better for apps with existing authentication.

The Rules

  1. Never store subscription state locally - RevenueCat is the source of truth
  2. Never hardcode prices - Fetch from Offering objects (prices vary by country)
  3. Always show "Restore Purchases" - App Store requires it
  4. Don't force login for anonymous purchases - Let users buy first, create accounts later
  5. Use --no-verify-jwt when deploying webhook Edge Functions
  6. Keep the paywall screen under 150 lines - Extract pricing cards into separate widgets

What To Build First

  1. Set up RevenueCat account and configure products in App Store Connect / Play Console
  2. Create the premium/ feature folder with all four layers
  3. Implement the isPremium check and PremiumGate widget
  4. Build a simple paywall screen
  5. Add restore purchases functionality
  6. (Optional) Set up Supabase webhooks for backend sync

RevenueCat turns what used to be months of IAP plumbing into a weekend project. Your time is better spent making your premium features worth paying for than wrestling with receipt validation.

Share:

Keep Reading

More articles you might enjoy

© 2026 Mohammad Nabi RahmaniBuilt with Next.js & Tailwind