RevenueCat Integration
RevenueCat handles mobile subscription management for iOS and Android apps, abstracting the complexity of App Store and Google Play billing.
Overview
BookWish uses RevenueCat for:
- Mobile IAP: In-app purchases for Premium subscriptions
- Subscription Status: Real-time subscription validation
- Webhook Events: Server-side subscription lifecycle management
- Cross-Platform: Unified API for iOS and Android
- Receipt Validation: Automatic receipt validation with Apple/Google
Implementation
Location: /backend/src/integrations/revenuecat.ts
Configuration
Required environment variables:
REVENUECAT_WEBHOOK_SECRET=rcwh_...
Note: The SDK initialization happens on the mobile apps (iOS/Android), not the backend. The backend only handles webhooks.
Features
1. Webhook Event Handling
RevenueCat sends webhook events for subscription lifecycle changes.
Supported Event Types
enum RevenueCatEventType {
INITIAL_PURCHASE = 'INITIAL_PURCHASE', // First subscription purchase
RENEWAL = 'RENEWAL', // Subscription renewed
CANCELLATION = 'CANCELLATION', // User cancelled (still active until expiration)
UNCANCELLATION = 'UNCANCELLATION', // User re-enabled subscription
EXPIRATION = 'EXPIRATION', // Subscription expired
BILLING_ISSUE = 'BILLING_ISSUE', // Payment failed
PRODUCT_CHANGE = 'PRODUCT_CHANGE', // Upgrade/downgrade
TRANSFER = 'TRANSFER' // Transferred between accounts
}
Handle Webhook
import { handleWebhook, verifyWebhookSignature } from '../integrations/revenuecat';
// In webhook endpoint
const signature = request.headers['x-revenuecat-signature'];
const payload = request.rawBody.toString();
// Verify signature
const isValid = verifyWebhookSignature(payload, signature);
if (!isValid) {
return res.status(401).send('Invalid signature');
}
// Process event
const webhookPayload = JSON.parse(payload);
await handleWebhook(webhookPayload);
res.status(200).send('OK');
2. Event Processing
The integration automatically handles each event type:
Initial Purchase
When a user first subscribes:
- User tier upgraded to
premium - Subscription record created in database
- Stores RevenueCat transaction ID
// Automatically handled by handleWebhook()
// User: tier = 'free' → 'premium'
// Subscription: status = 'active'
Renewal
When subscription auto-renews:
- Subscription period updated
- Status confirmed as
active
Cancellation
When user cancels (but subscription still active until expiration):
cancelledAttimestamp recorded- Status remains
activeuntil expiration - Notification sent to user
Expiration
When subscription expires:
- User tier downgraded to
free - Subscription status set to
expired
Billing Issue
When payment fails:
- Notification sent to user to update payment method
- Subscription remains active during grace period
Product Change
When user upgrades/downgrades:
- Plan updated to new product ID
- Period dates updated
3. Webhook Signature Verification
RevenueCat signs webhooks with HMAC-SHA256.
Verify Signature
import { verifyWebhookSignature } from '../integrations/revenuecat';
const isValid = verifyWebhookSignature(
request.rawBody.toString(),
request.headers['x-revenuecat-signature']
);
Uses timing-safe comparison to prevent timing attacks.
Webhook Payload Structure
interface RevenueCatWebhookPayload {
event: {
type: RevenueCatEventType;
app_user_id: string; // BookWish user ID
product_id: string; // e.g., 'premium_monthly'
period_type: 'NORMAL' | 'TRIAL' | 'INTRO';
purchased_at_ms: number; // Purchase timestamp
expiration_at_ms?: number; // Expiration timestamp
presented_offering_id?: string; // Offering identifier
transaction_id: string; // Platform transaction ID
original_transaction_id: string; // Original transaction ID
is_trial_conversion?: boolean; // Trial → paid conversion
store: 'APP_STORE' | 'PLAY_STORE'; // iOS or Android
};
}
Mobile SDK Setup
The mobile apps must configure the RevenueCat SDK.
iOS (Swift)
import RevenueCat
// In AppDelegate or App init
Purchases.configure(
withAPIKey: "appl_..." // RevenueCat API key for iOS
)
// Set user ID
Purchases.shared.logIn(userId) { (purchaserInfo, created, error) in
// Handle login
}
Android (Kotlin)
import com.revenuecat.purchases.Purchases
// In Application onCreate
Purchases.configure(
PurchasesConfiguration.Builder(this, "goog_...") // RevenueCat API key for Android
.build()
)
// Set user ID
Purchases.sharedInstance.logIn(userId) { purchaserInfo, error ->
// Handle login
}
User Identification
Critical: Use BookWish user ID as app_user_id:
// iOS
Purchases.shared.logIn(bookwishUserId)
// Android
Purchases.sharedInstance.logIn(bookwishUserId)
This ensures webhook events map to correct users in BookWish database.
Subscription Flow
Purchase Flow
- User taps "Upgrade to Premium" in mobile app
- App calls RevenueCat SDK:
Purchases.shared.purchase(package: package) - iOS/Android native IAP flow presented
- User completes purchase
- RevenueCat validates receipt with Apple/Google
- RevenueCat sends
INITIAL_PURCHASEwebhook to BookWish backend - BookWish upgrades user tier to
premium - App receives updated entitlements
Renewal Flow
- Apple/Google auto-charges user on renewal date
- RevenueCat receives receipt from platform
- RevenueCat sends
RENEWALwebhook to BookWish - BookWish updates subscription period
- User continues with Premium features
Cancellation Flow
- User cancels in iOS Settings or Google Play
- RevenueCat detects cancellation
- RevenueCat sends
CANCELLATIONwebhook - BookWish records cancellation date
- User keeps Premium until expiration date
- On expiration, RevenueCat sends
EXPIRATIONwebhook - BookWish downgrades user to
freetier
Database Schema
Subscription Table
CREATE TABLE subscriptions (
id UUID PRIMARY KEY,
user_id UUID NOT NULL REFERENCES users(id),
plan VARCHAR NOT NULL, -- e.g., 'premium'
status VARCHAR NOT NULL, -- 'active', 'expired', 'cancelled'
revenuecat_id VARCHAR, -- Original transaction ID
current_period_start TIMESTAMP,
current_period_end TIMESTAMP,
cancelled_at TIMESTAMP, -- When user cancelled
created_at TIMESTAMP DEFAULT NOW(),
updated_at TIMESTAMP DEFAULT NOW()
);
Error Handling
Webhook Processing Errors
The handleWebhook() function handles errors gracefully:
try {
await handleWebhook(payload);
} catch (error) {
logger.error('revenuecat.webhook_processing_failed', { error });
// Don't return error to RevenueCat - prevents retries for invalid data
// Log and investigate manually
}
Signature Verification Failures
If signature verification fails:
- Return
401 Unauthorized - Log the attempt
- Do not process the webhook
Missing Configuration
If REVENUECAT_WEBHOOK_SECRET is not set:
- Logs warning
- In development: allows webhooks without verification
- In production: should reject webhooks
Testing
Test Subscriptions
Both iOS and Android support test subscriptions:
iOS Sandbox
- Use sandbox Apple ID
- Subscriptions renew every 5 minutes (monthly) or 1 hour (yearly)
- Can cancel in iOS Settings → Apple ID → Subscriptions
Google Play Test Users
- Add test users in Google Play Console
- Use test credit cards
- Subscriptions renew on accelerated schedule
Test Webhook Locally
# Use ngrok to expose local server
ngrok http 3000
# Configure webhook URL in RevenueCat Dashboard
https://your-ngrok-url.ngrok.io/webhooks/revenuecat
Send Test Events
RevenueCat Dashboard allows sending test webhook events:
- Go to Project Settings → Integrations → Webhooks
- Click "Send Test Event"
- Select event type
- Verify backend processes correctly
Best Practices
- Use User IDs: Always set
app_user_idto BookWish user ID - Verify Signatures: Never skip webhook signature verification in production
- Handle Idempotency: Webhooks may be sent multiple times
- Sync Entitlements: Check entitlements on app launch
- Log Events: Log all webhook events for debugging
- Grace Periods: Respect billing grace periods before downgrading
- Test Thoroughly: Test all subscription states on both platforms
Limitations
- Mobile Only: RevenueCat is for mobile IAP only (web uses Stripe)
- Platform Fees: Apple takes 30% (15% after year 1), Google takes 15%
- Delayed Webhooks: Webhooks may arrive with slight delay
- Trial Periods: Trial handling depends on platform configuration
- Refunds: Platform refunds don't always trigger webhooks immediately
Notification Integration
The integration creates in-app notifications for users:
// Example: Cancellation notification
await notificationService.createNotification({
userId,
type: 'premium_subscription_expiring',
title: 'Subscription Cancelled',
body: 'Your Premium subscription will remain active until...'
});
Notification types:
premium_subscription_expiring: Cancelled but still activepayment_issue: Billing problem
Monitoring
Important metrics to monitor:
- Webhook delivery success rate
- Event processing latency
- Signature verification failures
- Subscription churn rate
- Trial conversion rate