A Convex component that syncs RevenueCat subscription state via webhooks.
Query entitlements directly from your Convex database.
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]
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.
- 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)
- Convex project (v1.31.6 or later)
- RevenueCat account with webhook access
npm install convex-revenuecat// 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;// 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;Generate a secure random string for webhook authorization:
openssl rand -base64 32Add 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.
- Open the RevenueCat Dashboard
- Select your project
- Go to Project Settings → Integrations → Webhooks
- Click + New
- 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 |
- Click Save
Find your Convex deployment URL in the Convex Dashboard under your project's Settings → URL & Deploy Key.
- In RevenueCat, go to your webhook configuration
- Click Send Test Event
- Verify the event was received:
npx convex logsYou should see a log entry showing the TEST event was processed.
If the test fails, check Troubleshooting below.
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",
});
},
});export const getSubscriptions = query({
args: { appUserId: v.string() },
handler: async (ctx, args) => {
return await revenuecat.getActiveSubscriptions(ctx, {
appUserId: args.appUserId,
});
},
});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_idyou pass toPurchases.logIn()must match what you query withhasEntitlement(). Use a consistent identifier (e.g., your Convex user ID). - The
entitlementIdparameter (e.g.,"premium") must match exactly what you configured in the RevenueCat dashboard.
- Missing users: Queries return empty arrays or
null(never throw). Use this for loading states. - Billing issues: During grace periods,
hasEntitlement()returnstrueandgetActiveSubscriptions()includes the subscription. - Lifetime purchases: Subscriptions without
expirationAtMsare always considered active.
const revenuecat = new RevenueCat(components.revenuecat, {
REVENUECAT_WEBHOOK_AUTH?: string, // Webhook authorization header
});| 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.
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.
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) |
- 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.
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
});See the example/ directory for a complete working example with:
- Component registration
- Webhook handler setup
- Query and mutation examples
Webhook returns 401 Unauthorized
The authorization header doesn't match.
-
Verify the environment variable is set:
npx convex env get REVENUECAT_WEBHOOK_AUTH
-
Ensure the value in RevenueCat matches exactly (no extra spaces)
-
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.
- Verify your
convex/http.tsexports the router as default - Check the path matches:
/webhooks/revenuecat - Confirm your deployment URL is correct (check Convex Dashboard)
Events received but entitlements not updating
-
Check the webhook event log:
npx convex logs
-
Check the
webhookEventstable in the Convex Dashboard to see processed events -
Verify
app_user_idin 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
See CONTRIBUTING.md for development setup.