RevenueCat provides a variety to get your backend integrated with subscription services. One of the possible solutions is the RevenueCat Firebase integration. This integration allows you to receive customer information and subscription events right on your Firebase backend.
Which benefits can you get from integrating RevenueCat to Firestore? The RevenueCat Firebase integration makes it possible to track subscriptions status and execute your custom logic on your backend side.
With RevenueCat Firebase extension you can:
1. Receive the latest customer information
2. Handle subscription events, such as trials, renewals, expirations, etc.
3. Integrate and manage user access with Custom Claims
4. Execute your custom logic for each subscription event
RevenueCat can be integrated with Firebase by using the RevenueCat extension. During plugin installation you can setup 4 features:
1. Collection where to store subscription events
2. Collection where to store customers info
3. Enable or disable Custom Claims and entitlements integration
4. Enable subscription event triggers
To install this extension you need to have both a Firebase and a RevenueCat project. You can install the extension using the Firebase Console or with CLI. The provided screenshots are from the Firebase Console installation.
To enable subscriptions events storage, enter the collection name where they will be stored.
When the subscription status is changed, RevenueCat sends an event corresponding to this change. There are 12 possible events:
1. Initial purchase
2. Non renewing purchase
3. Renewal
4. Product change
5. Cancellation
6. Uncancellation
7. Billing issue
8. Subscription paused
9. Transfer
10. Expiration
11. Test
12. Subscription alias (deprecated)
Each subscription event will be stored in a separate document in the specified collection. You can implement your custom logic by listening to document changes in this collection or make it more straightforward with event handlers.
Below are JSON samples for some of the listed events.
{
"aliases": [
"FK0CyI0zSJmBG53YjHt8gYGhTMP0",
"$RCAnonymousID:382e60cb78ce1a2bd91c86af3b6294b9"
],
"app_id": "appf742acb32c",
"app_user_id": "FK0CyI0zSJmBG53YjHt8gYGhTMP0",
"commission_percentage": 0.125,
"country_code": "UA",
"currency": "UAH",
"entitlement_id": null,
"entitlement_ids": [
"standard"
],
"environment": "SANDBOX",
"event_timestamp_ms": 1665576707874,
"expiration_at_ms": 1665577119081,
"id": "6E988AC0-47AF-579D-D581-18EC79B70391",
"is_family_share": false,
"offer_code": null,
"original_app_user_id": "$RCAnonymousID:382e60cb78ce1a2bd91c86af3b6294b9",
"original_transaction_id": "GPA.7988-3317-7927-18610",
"period_type": "NORMAL",
"presented_offering_id": null,
"price": 14.603,
"price_in_purchased_currency": 539.99,
"product_id": "tbrgroup.standard.monthly",
"purchased_at_ms": 1665576703083,
"store": "PLAY_STORE",
"subscriber_attributes": {
"$androidId": {
"updated_at_ms": 1665566380081,
"value": "e3985a6f8ffcb769"
},
"$email": {
"updated_at_ms": 1665566426724,
"value": "tbremail@gmail.com"
}
},
"takehome_percentage": 0.85,
"tax_percentage": 0.1667,
"transaction_id": "GPA.7988-3317-7927-18610",
"type": "INITIAL_PURCHASE"
}
{
"aliases": [
"FK0CyI0zSJmBG53YjHt8gYGhTMP0",
"$RCAnonymousID:382e60cb78ce1a2bd91c86af3b6294b9"
],
"app_id": "appf742acb32c",
"app_user_id": "FK0CyI0zSJmBG53YjHt8gYGhTMP0",
"commission_percentage": 0.125,
"country_code": "UA",
"currency": "UAH",
"entitlement_id": null,
"entitlement_ids": [
"standard"
],
"environment": "SANDBOX",
"event_timestamp_ms": 1665582112844,
"expiration_at_ms": 1665582465551,
"id": "2A24218E-4BDF-DAE8-A862-D55290246734",
"is_family_share": false,
"is_trial_conversion": false,
"offer_code": null,
"original_app_user_id": "$RCAnonymousID:382e60cb78ce1a2bd91c86af3b6294b9",
"original_transaction_id": "GPA.7988-3317-7927-18610",
"period_type": "NORMAL",
"presented_offering_id": null,
"price": 14.603,
"price_in_purchased_currency": 539.99,
"product_id": "tbrgroup.standard.monthly",
"purchased_at_ms": 1665580665551,
"store": "PLAY_STORE",
"subscriber_attributes": {
"$androidId": {
"updated_at_ms": 1665566380081,
"value": "e3985a6f8ffcb769"
},
"$email": {
"updated_at_ms": 1665566426724,
"value": "tbremail@gmail.com"
}
},
"takehome_percentage": 0.85,
"tax_percentage": 0.1667,
"transaction_id": "GPA.7988-3317-7927-18610..5",
"type": "RENEWAL"
}
{
"aliases": [
"FK0CyI0zSJmBG53YjHt8gYGhTMP0",
"$RCAnonymousID:382e60cb78ce1a2bd91c86af3b6294b9"
],
"app_id": "appf742acb32c",
"app_user_id": "FK0CyI0zSJmBG53YjHt8gYGhTMP0",
"commission_percentage": 0.125,
"country_code": "UA",
"currency": "UAH",
"entitlement_id": null,
"entitlement_ids": [
"standard"
],
"environment": "SANDBOX",
"event_timestamp_ms": 1665580130780,
"expiration_at_ms": 1665580128528,
"id": "13FEF71E-4B31-B0CB-B926-41837C86EB6E",
"is_family_share": false,
"new_product_id": "tbrgroup.standard.yearly",
"offer_code": null,
"original_app_user_id": "$RCAnonymousID:382e60cb78ce1a2bd91c86af3b6294b9",
"original_transaction_id": "GPA.7988-3317-7927-18610",
"period_type": "NORMAL",
"presented_offering_id": null,
"price": 0,
"price_in_purchased_currency": 0,
"product_id": "tbrgroup.standard.monthly",
"purchased_at_ms": 1665578844936,
"store": "PLAY_STORE",
"subscriber_attributes": {
"$androidId": {
"updated_at_ms": 1665566380081,
"value": "e3985a6f8ffcb769"
},
"$email": {
"updated_at_ms": 1665566426724,
"value": "tbremail@gmail.com"
}
},
"takehome_percentage": 0.85,
"tax_percentage": 0.1667,
"transaction_id": "GPA.7988-3317-7927-18610",
"type": "PRODUCT_CHANGE"
}
{
"aliases": [
"FK0CyI0zSJmBG53YjHt8gYGhTMP0",
"$RCAnonymousID:382e60cb78ce1a2bd91c86af3b6294b9"
],
"app_id": "appf742acb32c",
"app_user_id": "FK0CyI0zSJmBG53YjHt8gYGhTMP0",
"cancel_reason": "UNSUBSCRIBE",
"commission_percentage": 0.125,
"country_code": null,
"currency": "UAH",
"entitlement_id": null,
"entitlement_ids": [
"standard"
],
"environment": "SANDBOX",
"event_timestamp_ms": 1665577372363,
"expiration_at_ms": 1665576999458,
"id": "80E855E8-4411-1D44-BBCD-BA4CEDB1C5A4",
"is_family_share": false,
"offer_code": null,
"original_app_user_id": "$RCAnonymousID:382e60cb78ce1a2bd91c86af3b6294b9",
"original_transaction_id": "GPA.7988-3317-7927-18610",
"period_type": "NORMAL",
"presented_offering_id": null,
"price": 0,
"price_in_purchased_currency": 0,
"product_id": "tbrgroup.standard.monthly",
"purchased_at_ms": 1665576703083,
"store": "PLAY_STORE",
"subscriber_attributes": {
"$androidId": {
"updated_at_ms": 1665566380081,
"value": "e3985a6f8ffcb769"
},
"$email": {
"updated_at_ms": 1665566426724,
"value": "tbremail@gmail.com"
}
},
"takehome_percentage": 0.85,
"tax_percentage": 0.1667,
"transaction_id": "GPA.7988-3317-7927-18610",
"type": "CANCELLATION"
}
{
"aliases": [
"FK0CyI0zSJmBG53YjHt8gYGhTMP0",
"$RCAnonymousID:382e60cb78ce1a2bd91c86af3b6294b9"
],
"app_id": "appf742acb32c",
"app_user_id": "FK0CyI0zSJmBG53YjHt8gYGhTMP0",
"commission_percentage": 0.15,
"country_code": "US",
"currency": "USD",
"entitlement_id": null,
"entitlement_ids": [
"standard"
],
"environment": "PRODUCTION",
"event_timestamp_ms": 1686318917336,
"expiration_at_ms": 1686405315296,
"grace_period_expiration_at_ms": 1687528258057,
"id": "OB1FFF21-42AF-1A20-BC2F-6B72C0FC68FD",
"is_family_share": false,
"offer_code": null,
"original_app_user_id": "$RCAnonymousID:382e60cb78ce1a2bd91c86af3b6294b9",
"original_transaction_id": "GPA.7988-3317-7927-18610",
"period_type": "TRIAL",
"presented_offering_id": null,
"price": 0,
"price_in_purchased_currency": 0,
"product_id": "tbrgroup.standard.yearly",
"purchased_at_ms": 1686318658057,
"store": "PLAY_STORE",
"subscriber_attributes": {
"$androidId": {
"updated_at_ms": 1665566380081,
"value": "e3985a6f8ffcb769"
},
"$email": {
"updated_at_ms": 1665566426724,
"value": "tbremail@gmail.com"
}
},
"takehome_percentage": 0.85,
"tax_percentage": 0,
"transaction_id": "GPA.7988-3317-7927-18610..0",
"type": "BILLING_ISSUE"
}
{
"aliases": [
"$RCAnonymousID:48d54d20ad4bfa29e256b4680d3b737f",
"jxIE6mAjcSD3PBu6ZefO7WOHq98T"
],
"app_id": "appf742acb32c",
"app_user_id": "$RCAnonymousID:48d54d20ad4bfa29e256b4680d3b737f",
"event_timestamp_ms": 1665660047298,
"id": "C196E7BC-4AFA-C4C2-ACB5-00B3D6951BED",
"origin_app_user_id": "$RCAnonymousID:382e60cb78ce1a2bd91c86af3b6294b9",
"store": "PLAY_STORE",
"transferred_from": [
"FK0CyI0zSJmBG53YjHt8gYGhTMP0",
"$RCAnonymousID:382e60cb78ce1a2bd91c86af3b6294b9"
],
"transferred_to": [
"$RCAnonymousID:48d54d20ad4bfa29e256b4680d3b737f",
"jxIE6mAjcSD3PBu6ZefO7WOHq98T"
],
"type": "TRANSFER"
}
{
"aliases": [
"FK0CyI0zSJmBG53YjHt8gYGhTMP0",
"$RCAnonymousID:382e60cb78ce1a2bd91c86af3b6294b9"
],
"app_id": "appf742acb32c",
"app_user_id": "FK0CyI0zSJmBG53YjHt8gYGhTMP0",
"commission_percentage": 0.125,
"country_code": "UA",
"currency": "UAH",
"entitlement_id": null,
"entitlement_ids": [
"standard"
],
"environment": "SANDBOX",
"event_timestamp_ms": 1665577372364,
"expiration_at_ms": 1665576999458,
"expiration_reason": "UNSUBSCRIBE",
"id": "568A38AA-4036-A6DB-4FA2-5ECBCD8C9958",
"is_family_share": false,
"offer_code": null,
"original_app_user_id": "$RCAnonymousID:382e60cb78ce1a2bd91c86af3b6294b9",
"original_transaction_id": "GPA.7988-3317-7927-18610",
"period_type": "NORMAL",
"presented_offering_id": null,
"price": 0,
"price_in_purchased_currency": 0,
"product_id": "tbrgroup.standard.monthly",
"purchased_at_ms": 1665576703083,
"store": "PLAY_STORE",
"subscriber_attributes": {
"$androidId": {
"updated_at_ms": 1665566380081,
"value": "e3985a6f8ffcb769"
},
"$email": {
"updated_at_ms": 1665566426724,
"value": "tbremail@gmail.com"
}
},
"takehome_percentage": 0.85,
"tax_percentage": 0.1667,
"transaction_id": "GPA.7988-3317-7927-18610",
"type": "EXPIRATION"
}
Note that RevenueCat doesn’t send some special events for trial cancellation, trial conversion or for some other events which you can expect to see. But it sends all needed information in the base events, so you can detect special events and implement your custom logic according to them.
For example event RENEWAL contains key is_trial_conversion
with boolean value, which you can use to define whether this renewal event was a trial conversion or just a regular renewal.
To enable customers info storage enter the collection name where you want to store them.
When the customer information is changed, the RevenueCat extension updates this information in the customer document. Each customer is stored in a separate document. The customer identifier represents the document identifier.
Unlike subscription events the customer info is always stored in the single document. Each time customer info is updated the customer info document will be completely overwritten. So the customer info document represents the current state of the customer.
The customer document contains information about purchased user entitlements, subscriptions and products. Below you can see the JSON sample of the customer info.
{
"aliases": [
"FK0CyI0zSJmBG53YjHt8gYGhTMP0",
"$RCAnonymousID:382e60cb78ce1a2bd91c86af3b6294b9"
],
"entitlements": {
"standard": {
"expires_date": "2022-10-12T13:19:20Z",
"grace_period_expires_date": null,
"product_identifier": "tbrgroup.standard.yearly",
"purchase_date": "2022-10-12T12:47:24Z"
}
},
"first_seen": "2022-10-12T09:19:39Z",
"last_seen": "2022-10-12T09:20:26Z",
"management_url": "https://play.google.com/store/account/subscriptions",
"non_subscriptions": {},
"original_app_user_id": "$RCAnonymousID:382e60cb78ce1a2bd91c86af3b6294b9",
"original_application_version": null,
"original_purchase_date": null,
"other_purchases": {},
"subscriptions": {
"tbrgroup.standard.monthly": {
"billing_issues_detected_at": null,
"expires_date": "2022-10-12T12:16:39Z",
"grace_period_expires_date": null,
"is_sandbox": true,
"original_purchase_date": "2022-10-12T12:11:43Z",
"period_type": "normal",
"purchase_date": "2022-10-12T12:11:43Z",
"store": "play_store",
"unsubscribe_detected_at": "2022-10-12T12:22:52Z"
},
"tbrgroup.standard.yearly": {
"billing_issues_detected_at": null,
"expires_date": "2022-10-12T13:19:20Z",
"grace_period_expires_date": null,
"is_sandbox": true,
"original_purchase_date": "2022-10-12T12:47:24Z",
"period_type": "normal",
"purchase_date": "2022-10-12T12:47:24Z",
"store": "play_store",
"unsubscribe_detected_at": null
}
}
}
With the RevenueCat extension you can configure your subscription event custom handlers. You can define a separate cloud function for each event. The difference from events collection is that event handlers do not store any data in your Firestore by default. And you can define what information from the event you need to store and which logic is needed to be executed.
Select the channel location to enable event handlers. The channel location will be used as a location to publish your event handler cloud functions.
Then select the events which you want to enable from the dropdown menu. Only selected events will be enabled to receive callbacks from the RevenueCat extension.
These events are the same as listed events in the Events chapter of this article.
After the events are enabled you can deploy the cloud functions for these events. In the extension documentation you can see this code sample which shows how to create event handlers.
Note: The entire code presented is written in TypeScript.
import { onCustomEventPublished } from "firebase-functions/v2/eventarc";
export const eventhandler = onCustomEventPublished(
{
eventType: "com.revenuecat.v1.initial_purchase",
channel: "projects/${projectId}/locations/europe-west4/channels/firebase",
region: "europe-west4",
},
(e) => {
// Handle extension event here.
}
);
You can see that the event callback consists of 5 parts:
1. The name of the cloud function eventhandler
.
2. The event type com.revenuecat.v1.initial_purchase
. The com.revenuecat.v1
part is always the same. You need to change only the last part of the event for different event handlers. The last part represents the name of the event like initial_purchase
.
3. The channel projects/${projectId}/locations/europe-west4/channels/firebase
. It depends on your project id and the channel location which you selected during extension setup. It is the same for all event handlers.
4. The region europe-west4
which you selected during an extension setup.
5. The handler function.
Let’s improve the code above so that it can be used for many event handlers with less effort.
Create a revCatEventarcOpts
function which accepts the event name and returns EventarcTriggerOptions value
.
import { EventarcTriggerOptions } from "firebase-functions/v2/eventarc";
export const revCatEventarcOpts = (
eventName: string
): EventarcTriggerOptions => {
return {
eventType: `com.revenuecat.v1.${eventName}`,
channel: `projects/${projectId}/locations/europe-west4/channels/firebase`,
region: "europe-west4",
};
};
Where projectId
is the variable taken from the environment.
export const projectId = process.env.GCP_PROJECT || process.env.GCLOUD_PROJECT
Use revCatEventarcOpts
function to declare your event handlers and put them together into one object.
export const purchases = {
initial: onCustomEventPublished(
revCatEventarcOpts("initial_purchase"),
(e) => {
// Handle initial_purchase event here.
}
),
renewal: onCustomEventPublished(
revCatEventarcOpts("renewal"),
(e) => {
// Handle renewal event here.
}
),
expiration: onCustomEventPublished(
revCatEventarcOpts("expiration"),
(e) => {
// Handle expiration event here.
}
),
};
Now you can deploy all the event handlers functions using a single command.
$ firebase deploy –only functions:purchases
Or deploy a specific cloud function using the purchases-${event_name}
naming. For example, deploy a renewal function.
$ firebase deploy –only functions:purchases-renewal
Next let’s see how to get data from the events. Call e.data
to get the event payload. Then it will be helpful to use the JSON samples of delivered events. To access the event data you need to use the same naming and the object structure as in the event documents. The RevenueCat extension uses snake_case in their payloads. Below you can see the example of getting fields from renewal event payload.
export const purchases = {
renewal: onCustomEventPublished(
revCatEventarcOpts("renewal"),
(e) => {
const payload = e.data;
const appUserId = payload.app_user_id;
const entitlementIds = payload.entitlement_ids;
const expirationAtMs = payload.expiration_at_ms;
const periodType = payload.period_type;
const isTrialConversion = payload.is_trial_conversion;
}
),
};
During using this integration you may face some common issues. These issues may affect your testing rather than the production work, but it is necessary that you know about them.
The first issue is that RevenueCat doesn’t send the subscription updates immediately to the Firestore. Sometimes you can see a few minutes delay. This is the reason why the subscription can look like it has expired even if it was renewed, but this information hasn’t been updated in the Firestore yet.
The second issue is connected with the fact that sometimes RevenueCat sends subscription events in the wrong order. During subscription testing the trial and subscription periods are much shorter than in the production. So during testing you can receive many events within a few minutes which may cause them to be received in the wrong order.
Once everything has been set up, Firebase starts receiving subscriptions information from RevenueCat. This information is stored in the specified collection and it triggers the selected events.