Skip to content

ramonclaudio/convex-revenuecat

Repository files navigation

Convex       RevenueCat

Convex RevenueCat

npm version Build Status License

A Convex component that syncs RevenueCat subscription state via webhooks.
Query entitlements directly from your Convex database.

What This Component Does

This component receives RevenueCat webhooks and maintains subscription state in your Convex database. Use it to:

  • Check if users have active entitlements (e.g., "premium" access)
  • Query subscription status with Convex's real-time reactivity
graph LR
    A[RevenueCat] -->|webhooks| B[Component]
    B -->|writes| C[(Convex DB)]
    C -->|queries| D[Your App]
Loading

This is not a replacement for the RevenueCat SDK. Use their SDK in your client app for purchases. This component handles the server-side state that webhooks provide.

Tip

Webhook timing: After a purchase completes in the SDK, there's a delay before RevenueCat sends the webhook (usually seconds, occasionally longer). During this window, hasEntitlement() returns false. Once the webhook arrives, Convex's real-time sync updates your UI. No polling needed.

Features

  • Webhook Processing: Idempotent handling of all 18 RevenueCat webhook events
  • Convex Integration: Data stored in Convex tables with real-time reactivity
  • Correct Edge Cases: Cancellation keeps access until expiration, pause doesn't revoke, etc.
  • Rate Limiting: Built-in protection against webhook abuse (100 req/min per app)
  • Subscriber Attributes: Stores customer attributes from webhooks
  • Experiment Tracking: Tracks A/B test enrollments
  • TypeScript: Typed API methods (webhook payloads stored as-is)

Prerequisites

Installation

npm install convex-revenuecat

Quick Start

1. Configure the Component

// convex/convex.config.ts
import { defineApp } from "convex/server";
import revenuecat from "convex-revenuecat/convex.config";

const app = defineApp();
app.use(revenuecat);

export default app;

2. Mount the Webhook Handler

// convex/http.ts
import { httpRouter } from "convex/server";
import { RevenueCat } from "convex-revenuecat";
import { components } from "./_generated/api";

const http = httpRouter();

const revenuecat = new RevenueCat(components.revenuecat, {
  REVENUECAT_WEBHOOK_AUTH: process.env.REVENUECAT_WEBHOOK_AUTH,
});

http.route({
  path: "/webhooks/revenuecat",
  method: "POST",
  handler: revenuecat.httpHandler(),
});

export default http;

3. Set Up Environment Variables

Generate a secure random string for webhook authorization:

openssl rand -base64 32

Add it to your Convex deployment:

npx convex env set REVENUECAT_WEBHOOK_AUTH "your-generated-secret"

For local development, add to your .env.local file instead.

4. Configure RevenueCat Webhooks

  1. Open the RevenueCat Dashboard
  2. Select your project
  3. Go to Project SettingsIntegrationsWebhooks
  4. Click + New
  5. Configure the webhook:
Field Value
Name Convex (or any identifier)
Webhook URL https://<your-deployment>.convex.site/webhooks/revenuecat
Authorization header The secret you generated in step 3
  1. Click Save

Find your Convex deployment URL in the Convex Dashboard under your project's SettingsURL & Deploy Key.

5. Test the Webhook

  1. In RevenueCat, go to your webhook configuration
  2. Click Send Test Event
  3. Verify the event was received:
npx convex logs

You should see a log entry showing the TEST event was processed.

If the test fails, check Troubleshooting below.

Usage

Check Entitlements

import { query } from "./_generated/server";
import { components } from "./_generated/api";
import { RevenueCat } from "convex-revenuecat";
import { v } from "convex/values";

const revenuecat = new RevenueCat(components.revenuecat);

export const checkPremium = query({
  args: { appUserId: v.string() },
  returns: v.boolean(),
  handler: async (ctx, args) => {
    return await revenuecat.hasEntitlement(ctx, {
      appUserId: args.appUserId,
      entitlementId: "premium",
    });
  },
});

Get Active Subscriptions

export const getSubscriptions = query({
  args: { appUserId: v.string() },
  handler: async (ctx, args) => {
    return await revenuecat.getActiveSubscriptions(ctx, {
      appUserId: args.appUserId,
    });
  },
});

Centralizing Access

Create a module to avoid instantiating RevenueCat in every file:

// convex/revenuecat.ts
import { RevenueCat } from "convex-revenuecat";
import { components } from "./_generated/api";

export const revenuecat = new RevenueCat(components.revenuecat, {
  REVENUECAT_WEBHOOK_AUTH: process.env.REVENUECAT_WEBHOOK_AUTH,
});

Important

ID matching is critical:

  • The app_user_id you pass to Purchases.logIn() must match what you query with hasEntitlement(). Use a consistent identifier (e.g., your Convex user ID).
  • The entitlementId parameter (e.g., "premium") must match exactly what you configured in the RevenueCat dashboard.

API Reference

Query Behavior

  • Missing users: Queries return empty arrays or null (never throw). Use this for loading states.
  • Billing issues: During grace periods, hasEntitlement() returns true and getActiveSubscriptions() includes the subscription.
  • Lifetime purchases: Subscriptions without expirationAtMs are always considered active.

Constructor

const revenuecat = new RevenueCat(components.revenuecat, {
  REVENUECAT_WEBHOOK_AUTH?: string, // Webhook authorization header
});

Query Methods

Method Description
hasEntitlement(ctx, { appUserId, entitlementId }) Check if user has active entitlement
getActiveEntitlements(ctx, { appUserId }) Get all active entitlements
getAllEntitlements(ctx, { appUserId }) Get all entitlements (active and inactive)
getActiveSubscriptions(ctx, { appUserId }) Get all active subscriptions (includes grace period)
getAllSubscriptions(ctx, { appUserId }) Get all subscriptions
getSubscriptionsInGracePeriod(ctx, { appUserId }) Get subscriptions currently in billing grace period
isInGracePeriod(ctx, { originalTransactionId }) Check grace period status for a subscription
getCustomer(ctx, { appUserId }) Get customer record
getExperiment(ctx, { appUserId, experimentId }) Get user's variant for a specific experiment
getExperiments(ctx, { appUserId }) Get all experiments user is enrolled in
getTransfer(ctx, { eventId }) Get transfer event by ID
getTransfers(ctx, { limit? }) Get recent transfers (default limit: 100)
getInvoice(ctx, { invoiceId }) Get invoice by ID
getInvoices(ctx, { appUserId }) Get all invoices for user
getVirtualCurrencyBalance(ctx, { appUserId, currencyCode }) Get balance for a specific currency
getVirtualCurrencyBalances(ctx, { appUserId }) Get all currency balances for user
getVirtualCurrencyTransactions(ctx, { appUserId, currencyCode? }) Get virtual currency transactions

This component is a read-only sync layer. To grant promotional entitlements, use the RevenueCat API directly — the webhook will sync the state automatically.

Webhook Events

View all 18 supported webhook events
Event Behavior
INITIAL_PURCHASE Creates subscription, grants entitlements
RENEWAL Extends entitlement expiration
CANCELLATION Keeps entitlements until expiration
EXPIRATION Revokes entitlements
BILLING_ISSUE Keeps entitlements during grace period
SUBSCRIPTION_PAUSED Does not revoke entitlements
SUBSCRIPTION_EXTENDED Extends expiration (customer support)
TRANSFER Moves entitlements between users
UNCANCELLATION Clears cancellation status
PRODUCT_CHANGE Updates subscription product
NON_RENEWING_PURCHASE Grants entitlements for one-time purchase
TEMPORARY_ENTITLEMENT_GRANT Grants temp access during store outage
REFUND Revokes entitlements immediately
REFUND_REVERSED Restores entitlements after refund undone
TEST Dashboard test event (logged only)
INVOICE_ISSUANCE Web Billing invoice created
VIRTUAL_CURRENCY_TRANSACTION Virtual currency adjustment
EXPERIMENT_ENROLLMENT A/B test enrollment (tracked)
SUBSCRIBER_ALIAS Migrates entitlements/subscriptions from anonymous to real user ID (deprecated)

Important

CANCELLATION does not revoke entitlements. Users keep access until EXPIRATION.

Database Schema

The component creates ten tables:

Table Purpose
customers User identity, aliases, and subscriber attributes
subscriptions Purchase records with product and payment details
entitlements Access control state (active/inactive, expiration)
experiments A/B test enrollments from RevenueCat experiments
transfers Entitlement transfer records between users
invoices Web Billing invoice records
virtualCurrencyBalances Virtual currency balances per user per currency
virtualCurrencyTransactions Individual virtual currency adjustments
webhookEvents Event log for idempotency and debugging (30-day retention)
rateLimits Webhook endpoint rate limiting (100 req/min per app)

Limitations

  • No initial sync — Existing subscribers before webhook setup won't appear until they trigger a new event (renewal, cancellation, etc.)
  • Webhook-driven only — Data comes exclusively from webhooks; no API polling or backfill mechanism
  • Raw payload storage — Webhook payloads are stored as-is for debugging. These may contain subscriber attributes or other data you've configured in RevenueCat. Events are auto-deleted after 30 days.
  • Production usage — Core entitlement checking (hasEntitlement) is production-tested. Other query methods (transfers, invoices, virtual currency) are unit-tested but not yet battle-tested in production apps.

Testing

Register the component in your tests:

import { convexTest } from "convex-test";
import revenuecatTest from "convex-revenuecat/test";

function initConvexTest() {
  const t = convexTest();
  revenuecatTest.register(t);
  return t;
}

test("check premium access", async () => {
  const t = initConvexTest();
  // Your test code here
});

Example

See the example/ directory for a complete working example with:

  • Component registration
  • Webhook handler setup
  • Query and mutation examples

Troubleshooting

Webhook returns 401 Unauthorized

The authorization header doesn't match.

  1. Verify the environment variable is set:

    npx convex env get REVENUECAT_WEBHOOK_AUTH
  2. Ensure the value in RevenueCat matches exactly (no extra spaces)

  3. Redeploy after setting the variable:

    npx convex deploy
Webhook returns 404 Not Found

The webhook URL is incorrect or the HTTP handler isn't mounted.

  1. Verify your convex/http.ts exports the router as default
  2. Check the path matches: /webhooks/revenuecat
  3. Confirm your deployment URL is correct (check Convex Dashboard)
Events received but entitlements not updating
  1. Check the webhook event log:

    npx convex logs
  2. Check the webhookEvents table in the Convex Dashboard to see processed events

  3. Verify app_user_id in RevenueCat matches what you're querying

User has entitlement in RevenueCat but not in Convex

The webhook may not have been received yet, or was received before the component was set up.

Option 1: Trigger a new event (make a test purchase in sandbox)

Option 2: Use the RevenueCat dashboard to resend historical webhooks

Resources

Contributing

See CONTRIBUTING.md for development setup.

License

Apache-2.0

About

Convex component for webhook-driven RevenueCat subscription state. Stores entitlements in your database for reactive, real-time access control.

Topics

Resources

License

Contributing

Stars

Watchers

Forks

Packages

 
 
 

Contributors