Skip to main content

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):

  • cancelledAt timestamp recorded
  • Status remains active until 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

  1. User taps "Upgrade to Premium" in mobile app
  2. App calls RevenueCat SDK: Purchases.shared.purchase(package: package)
  3. iOS/Android native IAP flow presented
  4. User completes purchase
  5. RevenueCat validates receipt with Apple/Google
  6. RevenueCat sends INITIAL_PURCHASE webhook to BookWish backend
  7. BookWish upgrades user tier to premium
  8. App receives updated entitlements

Renewal Flow

  1. Apple/Google auto-charges user on renewal date
  2. RevenueCat receives receipt from platform
  3. RevenueCat sends RENEWAL webhook to BookWish
  4. BookWish updates subscription period
  5. User continues with Premium features

Cancellation Flow

  1. User cancels in iOS Settings or Google Play
  2. RevenueCat detects cancellation
  3. RevenueCat sends CANCELLATION webhook
  4. BookWish records cancellation date
  5. User keeps Premium until expiration date
  6. On expiration, RevenueCat sends EXPIRATION webhook
  7. BookWish downgrades user to free tier

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:

  1. Go to Project Settings → Integrations → Webhooks
  2. Click "Send Test Event"
  3. Select event type
  4. Verify backend processes correctly

Best Practices

  1. Use User IDs: Always set app_user_id to BookWish user ID
  2. Verify Signatures: Never skip webhook signature verification in production
  3. Handle Idempotency: Webhooks may be sent multiple times
  4. Sync Entitlements: Check entitlements on app launch
  5. Log Events: Log all webhook events for debugging
  6. Grace Periods: Respect billing grace periods before downgrading
  7. 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 active
  • payment_issue: Billing problem

Monitoring

Important metrics to monitor:

  • Webhook delivery success rate
  • Event processing latency
  • Signature verification failures
  • Subscription churn rate
  • Trial conversion rate

Additional Resources