RevenueCat Integration: From Zero to Subscriptions in Hours
RevenueCat Integration: From Zero to Subscriptions in Hours
I’ve shipped 3 apps with RevenueCat. Each time, the integration gets faster.
Muse Otter took 4 hours from zero to working subscriptions on iOS and Android.
Here’s how.
Why RevenueCat
Before RevenueCat, I spent weeks on StoreKit 1, then StoreKit 2, then Google Billing Library. Different APIs. Different edge cases. Different headaches.
RevenueCat gives you:
| What | Why It Matters |
|---|---|
| One SDK | Same code for iOS, Android, and Web |
| Receipt validation | No server-side code needed |
| Subscription state | Handles renewals, cancellations, grace periods |
| Webhooks | Real-time sync to your backend |
| Analytics | Revenue, churn, LTV out of the box |
The math: 2 weeks of native implementation vs 4 hours with RevenueCat. Easy choice.
The Architecture
Here’s how Muse Otter handles subscriptions:
┌─────────────┐ Purchase ┌─────────────┐
│ Flutter App │──────────────►│ RevenueCat │
│ │ │ │
│ purchases_ │ Entitle- │ (handles │
│ flutter │◄──────────────│ stores) │
└──────┬──────┘ ments └──────┬──────┘
│ │
│ Read from │ Webhook
│ Firestore │ events
▼ ▼
┌─────────────────────────────────────────────┐
│ FIRESTORE │
│ │
│ users/{userId} │
│ └── subscriptionStatus: free | pro | lifetime │
│ │
│ ══ Single source of truth ══ │
└─────────────────────────────────────────────┘
Key insight: The app reads subscription status from Firestore, not RevenueCat. The webhook keeps Firestore in sync.
Step 1: RevenueCat Setup (30 min)
Dashboard Configuration
- Create app in RevenueCat dashboard
- Add products:
monthly_10— $9.99/monthyearly_80— $79.99/year
- Create entitlement:
Muse Otter Pro - Create offering:
default - Attach products to offering
App Store / Play Store
Set up products in App Store Connect and Google Play Console. Match the IDs exactly.
Step 2: Flutter Integration (1 hour)
Install SDK
# pubspec.yaml
dependencies:
purchases_flutter: ^9.10.0
Initialize
Future<void> initRevenueCat() async {
await Purchases.setLogLevel(LogLevel.debug);
PurchasesConfiguration config;
if (Platform.isIOS) {
config = PurchasesConfiguration('appl_your_api_key');
} else {
config = PurchasesConfiguration('goog_your_api_key');
}
await Purchases.configure(config);
}
Login User
Future<void> loginUser(String firebaseUid) async {
await Purchases.logIn(firebaseUid);
}
Important: Use your Firebase UID as the RevenueCat user ID. This links everything together.
Fetch Offerings
Future<Offerings?> getOfferings() async {
try {
return await Purchases.getOfferings();
} catch (e) {
logger.e('Failed to fetch offerings: $e');
return null;
}
}
Make Purchase
Future<bool> purchasePackage(Package package) async {
try {
await Purchases.purchasePackage(package);
return true;
} on PurchasesErrorCode catch (e) {
if (e != PurchasesErrorCode.purchaseCancelledError) {
logger.e('Purchase failed: $e');
}
return false;
}
}
Step 3: Firebase Webhook (2 hours)
This is where Firestore becomes the source of truth.
Cloud Function
import { onRequest } from 'firebase-functions/v2/https';
import { getFirestore } from 'firebase-admin/firestore';
export const revenueCatWebhook = onRequest(
{ region: 'europe-west1', secrets: ['REVENUECAT_WEBHOOK_SECRET'] },
async (req, res) => {
// Verify authorization
const authHeader = req.headers.authorization;
if (authHeader !== process.env.REVENUECAT_WEBHOOK_SECRET) {
res.status(401).send('Unauthorized');
return;
}
const event = req.body;
const userId = event.app_user_id;
const eventType = event.type;
// Map event to subscription status
let newStatus: string | null = null;
switch (eventType) {
case 'INITIAL_PURCHASE':
case 'RENEWAL':
case 'UNCANCELLATION':
newStatus = 'pro';
break;
case 'EXPIRATION':
newStatus = 'free';
break;
// CANCELLATION: no change (user keeps access until expiry)
// BILLING_ISSUE: no change (grace period)
}
if (newStatus) {
const userRef = getFirestore().doc(`users/${userId}`);
const userDoc = await userRef.get();
// Don't override protected statuses
const currentStatus = userDoc.data()?.subscriptionStatus;
if (currentStatus === 'donation' || currentStatus === 'lifetime') {
res.status(200).send('Protected status - no change');
return;
}
await userRef.update({
subscriptionStatus: newStatus,
subscriptionUpdatedAt: new Date(),
});
}
res.status(200).send('OK');
}
);
Configure Webhook in RevenueCat
- Go to Project Settings → Integrations → Webhooks
- Add your Cloud Function URL
- Set authorization header
- Select events: INITIAL_PURCHASE, RENEWAL, EXPIRATION, CANCELLATION, UNCANCELLATION
Step 4: App Reads from Firestore (30 min)
Riverpod Provider
@riverpod
Stream<bool> isPro(IsProRef ref) {
final user = ref.watch(currentUserProvider);
if (user == null) return Stream.value(false);
return FirebaseFirestore.instance
.doc('users/${user.uid}')
.snapshots()
.map((doc) {
final status = doc.data()?['subscriptionStatus'] ?? 'free';
return status != 'free';
});
}
Gate Pro Features
class ProGate extends ConsumerWidget {
final Widget child;
final Widget fallback;
@override
Widget build(BuildContext context, WidgetRef ref) {
final isPro = ref.watch(isProProvider);
return isPro.when(
data: (pro) => pro ? child : fallback,
loading: () => fallback,
error: (_, __) => fallback,
);
}
}
The Patterns That Work
1. Firestore as Source of Truth
Don’t check RevenueCat SDK every time. Write status to Firestore once, read from there always.
Why: Faster, works offline, one source of truth.
2. Protected Statuses
Some users get free access (testers, contest judges, supporters). Protect them:
if (currentStatus === 'donation' || currentStatus === 'lifetime') {
// Don't change anything
return;
}
3. Login with Firebase UID
Always use the same user ID in RevenueCat and Firebase. Makes webhooks trivial.
4. Soft Paywall First
Don’t block immediately. Show value, then ask for money:
Free: 20 messages/day
Pro: Unlimited
Users hit the limit organically. Then the paywall feels fair.
Results
Muse Otter subscription system:
- 4 hours to integrate
- 100% event coverage via webhooks
- Works offline (reads from Firestore cache)
- Protected statuses for manual grants
- A/B testable via Firebase Remote Config
Try It
RevenueCat’s free tier handles up to $2.5k/month. That’s enough to validate your product.
Don’t build subscription infrastructure. Just ship.
The best payment system is the one you don’t have to maintain.