Backend entitlements

This section provides details on how to manage entitlements with your own backend

Architecture & general functionning

Managing entitlements with your own backend is the most secure approach.


In this setup, responsibilities are shared between your servers and Purchasely’s servers.

The Purchasely's servers are in charge of:

  • processing and verifying transactions
  • notifying your servers of any update concerning a user's entitlement (eg: a subscription is renewed)
  • notifying the SDK and your app of such entitlements updates

The developer's servers are responsible for:

  • listening to, processing and acknowledging Entitlement Events sent through the Purchasely webhook
  • granting/revoking entitlements to the users based on these events
  • securing contents themselves (eg: with DRM for streaming platform) and managing accesses to contents depending on users' entitlements

A typical timeline is as follows:

  • The user opens a Purchasely paywall and makes a purchase.
  • This purchase is made through the store, which returns a receipt to the Purchasely SDK.
  • The receipt is sent by the SDK to Purchasely’s servers for verification and recording of the purchase.
  • The information about this purchase (user, plan, renewal date, etc.) is then sent to your servers via our webhook.
  • Your servers record the purchase information and confirm to our servers that they have acknowledged the purchase.
  • Our servers then pass the verified purchase information back to our SDK.
  • Finally, our SDK hands control back to your application

To fully implement this process, we'll go step by step:

  1. Configuring the webhook
  2. Listening to webhook events
  3. Authenticating messages (optional but recommended)
  4. Updating users entitlements
  5. Providing an API to the app to fetch users' entitlements

Configuring the webhook

Purchasely’s servers will make HTTP calls to your servers to pass informations concerning new/updated entitlements: this calls are called "webhooks".

Here is how to configure them.

  1. Fill in Client webhook URL in your Purchasely Console (this is the URL that will be called to send you the webhooks)
    Purchasely Console > [YOUR APP] > Webhooks

  2. Enable Entitlement Events (ACTIVATE & DEACTIVATE)


📘

Event acknowledgement expected

As soon as you have configured a Client webhook URL, the Purchasely Platform will expect an acknowledgement for the all events sent on the webhook.

You can start by implementing a simple HTTP 200 response for every message sent on the webhook.


Managing webhook messages on your backend

Managing entitlements on your backend require 4 steps:

  1. Listening to Entitlement Events sent on the Purchasely Webhook and acknowledging them to confirm that:
    1. they have been processed
    2. the corresponding entitlements have been updated
  2. Authenticating messages (optional but recommended)
  3. Updating users entitlements in your database by extracting the relevant information from the Entitlement Events
  4. Providing an API to your app to fetch the users' entitlements

1. Listening to webhook events

Even if a lot of different events can be sent in the webhook, you should only listen to the ACTIVATE and DEACTIVATE events to manage your entitlements.

Upon the reception of an event, you MUST return a HTTP 200 to confirm to the Purchasely Platform that your backend has successfully handled the event.

📘

Return (almost) always a 200 response.

If you don't, the Purchasely Platform will continue sending you the message following our retry strategy. This table contains approximate retry waiting times:

#Next retry backoffTotal waiting time
10d 0h 0m 20s0d 0h 0m 20s
20d 0h 0m 26s0d 0h 0m 46s
30d 0h 0m 46s0d 0h 1m 32s
40d 0h 1m 56s0d 0h 3m 28s
50d 0h 4m 56s0d 0h 8m 24s
60d 0h 11m 10s0d 0h 19m 34s
70d 0h 22m 26s0d 0h 42m 0s
80d 0h 40m 56s0d 1h 22m 56s
90d 1h 9m 16s0d 2h 32m 12s
100d 1h 50m 26s0d 4h 22m 38s
110d 2h 47m 50s0d 7h 10m 28s
120d 4h 5m 16s0d 11h 15m 44s
130d 5h 46m 56s0d 17h 2m 40s
140d 7h 57m 26s1d 1h 0m 6s
150d 10h 41m 46s1d 11h 41m 52s
160d 14h 5m 20s2d 1h 47m 12s
170d 18h 13m 56s2d 20h 1m 8s
180d 23h 13m 46s3d 19h 14m 54s
191d 5h 11m 26s5d 0h 26m 20s
201d 12h 13m 56s6d 12h 40m 16s
211d 20h 28m 40s8d 9h 8m 56s
222d 6h 3m 26s10d 15h 12m 22s
232d 17h 6m 26s13d 8h 18m 48s
243d 5h 46m 16s16d 14h 5m 4s
253d 20h 11m 56s20d 10h 17m 0s

Any other response code than HTTP 200 will generate a retry.

🚧

Not returning a 200 will pause subsequent webhooks for the user.

To ensure webhooks are always sent in the correct order (otherwise an ACTIVATE/DEACTIVATE could become a DEACTIVATE/ACTIVATE, causing unintended access for your user), we will not send any subsequent webhooks for the affected user until the first one succeeds on your end (ie: until we receive a 200 response).

Once the first webhook succeeds, any paused webhooks will be sent immediately, maintaining the correct order.

🚧

Anonymous users

Sometimes you way receive webhooks targeting an Anonymous User, even if you're not supposed to have one.

In this case, just return a 200 response and ignore the content of the webhook. When the user logs in, the subscription will be automatically associated with them, and you’ll receive the corresponding webhook.

If you don’t return a 200 response, we’ll keep sending the anonymous user’s webhook, and you’ll never receive the one for the connected user (because we want to guarantee the order of webhooks, as explained before).


2. Authenticating messages (optional but recommended)

This step is optional and can be implemented later on. However, we strongly encourage to authenticate webhook messages to avoid attacks such as Man in the middle.

The authentication information is contained in the HEADER of the HTTP request :

  • X-PURCHASELY-REQUEST-SIGNATURE : request signature
  • X-PURCHASELY-TIMESTAMP : request timestamp

⚠️ Depending on your framework, you may receive the headers under another format:

  • Ruby on Rails: HTTP_X_PURCHASELY_REQUEST_SIGNATURE
  • NestJS: x-purchasely-request-signature

⚠️ Do not use the deprecated X-PURCHASELY-SIGNATURE header

Sample codes for signature verification:

const crypto = require("crypto");

// Request headers
// ---------------
const xPurchaselyRequestSignature = "f3c2a452e9ea72f41107321aeaf7999f1054148866a710c9b23f9f501785e2a4";
const xPurchaselyTimestamp = "1698322022";

// Request body
// ------------
// Please use the raw body and not an already parsed body.
// For example with Node:
//      DO: req.rawBody.toString()
//   DON'T: JSON.stringify(req.body)
const body = "{\"a_random_key\":\"a_random_value_ad\"}";

// Signature verification
// ----------------------
const webhookSharedSecret = "foobar";
const dataToSign = xPurchaselyTimestamp + body;
const computedSignature = crypto
                          .createHmac("sha256", webhookSharedSecret)
                          .update(dataToSign)
                          .digest("hex");

if (computedSignature === xPurchaselySignature) {
  // request authenticated
}
require 'openssl'

# Request headers
# ---------------
x_purchasely_signature = "f3c2a452e9ea72f41107321aeaf7999f1054148866a710c9b23f9f501785e2a4"
x_purchasely_timestamp = "1698322022"

# Request body
# ------------
# Please use the raw body and not an already parsed body.
# For example with Ruby on Rails: request.raw_post
body = "{\"a_random_key\":\"a_random_value_ad\"}"

# Signature verification
# ----------------------
webhook_shared_secret = "foobar"
data_to_sign = x_purchasely_timestamp + body
computed_signature = OpenSSL::HMAC.hexdigest('sha256', webhook_shared_secret, data_to_sign)

if (computed_signature == x_purchasely_signature) {
  # request authenticated
}
// Imports
// -------
import javax.crypto.Mac
import javax.crypto.spec.SecretKeySpec

// Request headers
// ---------------
val xPurchaselySignature = "f3c2a452e9ea72f41107321aeaf7999f1054148866a710c9b23f9f501785e2a4"
val xPurchaselyTimestamp = "1698322022"

// Request body
// ------------
// Please use the raw body and not an already parsed body.
val body = "{\"a_random_key\":\"a_random_value_ad\"}"

// Signature verification
// ----------------------
val webhookSharedSecret = "foobar"
val dataToSign = xPurchaselyTimestamp + body
val hmac = Mac.getInstance("HmacSHA256")
hmac.init(SecretKeySpec(webhookSharedSecret.toByteArray(), "HmacSHA256"))
val computedSignature = hmac.doFinal(dataToSign.toByteArray()).joinToString("") { "%02x".format(it) }

if (computedSignature == xPurchaselySignature) {
    // request authenticated
}

You can also compute the difference between the current timestamp and the received timestamp, then decide if the difference is within your tolerance.


3. Updating users entitlements

The ACTIVATE / DEACTIVATE events are carrying the information needed to determined which entitlements shall be granted / revoked to which user:

  • the plan purchased
  • the store used for purchasing
  • the user will be identified by their user ID if they are logged in, or by an anonymous ID if they are not.

Sample payload:

  • the subset tabs display only the mandatory fields necessary for managing entitlements
  • the full payload tabs show the entire event payloads
{
  "purchasely_one_time_purchase_id":"otp_xxx", // if the purchase is a one-time-purchase,
  "purchasely_subscription_id":"subs_xxx", // if the purchase is a subscription
  "event_name": "ACTIVATE",
  "store": "APPLE_APP_STORE",
  "user_id": "<user id provided by the app to the sdk>", // if the user is logged in
  "anonymous_user_id": "<user id provided by the app to the sdk>", // if the user is not logged in
  "plan": "<plan id set in the Purchasely console>"
}
{
  "purchasely_one_time_purchase_id":"otp_xxx", // if the purchase is a one-time-purchase,
  "purchasely_subscription_id":"subs_xxx", // if the purchase is a subscription
  "event_name": "ACTIVATE",
  "store": "APPLE_APP_STORE",
  "user_id": "<user id provided by the app to the sdk>", // if the user is signed-in
  "anonymous_user_id": "<user id provided by the app to the sdk>", // if the user is signed-out
  "plan": "<plan id set in the Purchasely console>"
}
{
  "plan": "monthly",
  "store": "GOOGLE_PLAY_STORE",
  "product": "PURCHASELY_PLUS",
  "user_id": "toto",
  "event_id": "5e45109f-7fac-45f8-a7e4-464892d5d35d",
  "event_name": "ACTIVATE",
  "offer_type": "NONE",
  "api_version": 3,
  "device_type": "PHONE",
  "environment": "SANDBOX",
  "purchased_at": "2023-12-12T14:13:11.777Z",
  "purchase_type": "RENEWING_SUBSCRIPTION",
  "store_country": "FR",
  "next_renewal_at": "2023-12-12T14:23:11.777Z",
  "purchased_at_ms": 1702390391777,
  "event_created_at": "2023-12-12T14:19:26.120Z",
  "is_family_shared": false,
  "store_product_id": "com.purchasely.plus.monthly",
  "customer_currency": "EUR",
  "plan_price_in_eur": 9.99,
  "next_renewal_at_ms": 1702390991777,
  "event_created_at_ms": 1702390766120,
  "previous_offer_type": "NONE",
  "store_app_bundle_id": "com.purchasely.demo",
  "subscription_status": "AUTO_RENEWING",
  "store_transaction_id": "GPA.3355-5688-7970-28037..5",
  "original_purchased_at": "2023-12-12T13:48:16.233Z",
  "original_purchased_at_ms": 1702388896233,
  "cumulated_revenues_in_eur": 69.9,
  "effective_next_renewal_at": "2023-12-12T14:23:11.777Z",
  "purchasely_subscription_id": "subs_D7GnVQbUxvY6YxoeK6nhyPDkmyCVcfe",
  "effective_next_renewal_at_ms": 1702390991777,
  "store_original_transaction_id": "GPA.3355-5688-7970-28037",
  "plan_price_in_customer_currency": 9.99
}
{
  "plan": "monthly",
  "store": "GOOGLE_PLAY_STORE",
  "product": "PURCHASELY_PLUS",
  "user_id": "toto",
  "event_id": "3ab7e67a-6c88-44fe-8804-39897d601136",
  "event_name": "DEACTIVATE",
  "offer_type": "NONE",
  "api_version": 3,
  "device_type": "PHONE",
  "environment": "SANDBOX",
  "purchased_at": "2023-12-12T14:18:11.777Z",
  "purchase_type": "RENEWING_SUBSCRIPTION",
  "store_country": "FR",
  "next_renewal_at": "2023-12-12T14:23:11.777Z",
  "purchased_at_ms": 1702390691777,
  "event_created_at": "2023-12-12T14:24:09.412Z",
  "is_family_shared": false,
  "store_product_id": "com.purchasely.plus.monthly",
  "customer_currency": "EUR",
  "plan_price_in_eur": 9.99,
  "next_renewal_at_ms": 1702390991777,
  "event_created_at_ms": 1702391049412,
  "previous_offer_type": "NONE",
  "store_app_bundle_id": "com.purchasely.demo",
  "subscription_status": "UNPAID",
  "store_transaction_id": "GPA.3355-5688-7970-28037..5",
  "original_purchased_at": "2023-12-12T13:48:16.233Z",
  "original_purchased_at_ms": 1702388896233,
  "cumulated_revenues_in_eur": 69.9,
  "effective_next_renewal_at": "2023-12-12T14:23:11.777Z",
  "purchasely_subscription_id": "subs_D7GnVQbUxvY6YxoeK6nhyPDkmyCVcfe",
  "effective_next_renewal_at_ms": 1702390991777,
  "store_original_transaction_id": "GPA.3355-5688-7970-28037",
  "plan_price_in_customer_currency": 9.99
}

The users entitlements must be associated to the user ID or anonymous user ID and stored in your database. You must solely rely on the webhook events to update them.

🚧

Do not rely on the "billing cycle end dates" to invalidate subscriptions on your end

Never use the next_renewal_at / effective_next_renewal_at to invalidate a subscription and always use the DEACTIVATE event sent on the webhook for this sole purpose. These dates are only here to help your marketing team take actions (or if you want to display the next renewal date in your app).

Why is relying on these dates a mistake?

If your/Purchasely/stores's servers encounter an issue, you might invalidate a user's rights simply because your data isn't up to date and the effective_next_renewal_at seems to be in the past.

What should you do instead?

  • Upon subscription renewal, the Purchasely Platform always sends a ACTIVATE message
  • Upon subscription termination, the Purchasely Platform always sends a DEACTIVATE message in the right timing, in other words when the entitlements should be revoked on your end

If you ever need a fail safe to unsubscribe users in case an issue occurs with the Store/Purchasely/your servers, you should let at least a 24h-margin with the given effective_next_renewal_at.

More details on Entitlement Events


Sample backend code for:

  • receiving webhook events
  • extracting the relevant information
  • updating the entitlements in the database
  • acknowledging the processing of the events
# This example uses Sinatra and ActiveRecord as ORM

#                                    CONTROLLER                                #
# ============================================================================ #
# Uses Sinatra

post '/webhook' do
  request.body.rewind
  payload = JSON.parse(request.body.read)

  entitlement = find_or_initialize_entitlement(payload)
  entitlement.attributes = {
    effective_next_renewal_at: ms_to_time(payload["effective_next_renewal_at_ms"]),
    status: payload["status"],
    store: payload["store"], # will never change
    type: payload["type"], # will never change
    last_payload: payload,
  }
  entitlement.save!

  # Acknowledges the processing of the event by answering a 200
  status 200
end


#                                      MODEL                                   #
# ============================================================================ #
# Uses ActiveRecord

# Entitlement
#
# For each purchase, it will keep track of:
#   - the purchase type
#   - the user owning it
#   - the plan bought
#   - the status of the sub (in short: active/not active)
#
# ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️
# ⚠️ Be sure to add a UNIQ CONSTRAINT on (purchasely_id, plan, anonymous_user_id, user_id) ⚠️
# ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️
class Entitlement < ActiveRecord::Base
  attr_accessor(
    :id,
    # filled in when the purchase is associated to a not logged in user
    :anonymous_user_id,
    # filled in when the purchase is associated to a logged in user
    :user_id,
    # RENEWING_SUBSCRIPTION | NON_RENEWING_SUBSCRIPTION |
    # CONSUMABLE | NON_CONSUMABLE
    :type, 
    # subs_xxx | otp_yyy
    :purchasely_id, 
    # Purchasely identifier of your plan, as defined in the Purchasely console
    # under the "id" input
    :plan, 
    # backup of the last payload sent
    :last_payload, 
    # At this date the user will loose their entitlement (it is equal to the max
    # between the next_renewal_at, grace_period_expires_at and defer_end_at)
    :effective_next_renewal_at,
    # AUTO_RENEWING | AUTO_RENEWING_CANCELED | IN_GRACE_PERIOD | ON_HOLD | PAUSED |
    # REVOKED | DEACTIVATED | UNPAID
    :status, 
    # APPLE_APP_STORE | GOOGLE_PLAY_STORE | STRIPE | HUAWEI_APP_GALLERY |
    # AMAZON_APP_STORE
    :store,
    :created_at,
    :updated_at
  )

  ACTIVE_STATUSES = %w[
    AUTO_RENEWING
    AUTO_RENEWING_CANCELED
    IN_GRACE_PERIOD
  ]
end


#                                     HELPERS                                  #
# ============================================================================ #

def find_or_initialize_entitlement(payload)
  Entitlement
    .find_or_initialize_by(
      payload.slice("anonymous_user_id", "plan", "purchasely_id", "user_id")
    ).last
end

def ms_to_time(time_in_ms)
  return nil unless time_in_ms

  Time.at(time_in_ms / 1000)
end
const express = require('express');
const app = express();
const bodyParser = require('body-parser');
const Entitlement = require('./models/entitlement');

app.use(bodyParser.json());


// Controller
// ============================================================================
// Uses Express

app.post('/webhook', async (req, res) => {
  const payload = req.body;

  const entitlement = await findOrInitializeEntitlement(payload);
  entitlement.effective_next_renewal_at = msToTime(payload.effective_next_renewal_at_ms);
  entitlement.status = payload.status;
  entitlement.store = payload.store; // will never change
  entitlement.type = payload.type; // will never change
  entitlement.last_payload = payload;

  await entitlement.save();

  // Acknowledges the processing of the event by answering a 200
  res.status(200).send();
});

const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
  console.log(`Server is running on port ${PORT}`);
});


// Model
// ============================================================================
// Uses Mongoose

const mongoose = require('mongoose');
const Schema = mongoose.Schema;

const EntitlementSchema = new Schema({
  anonymous_user_id: { type: String, index: true },
  user_id: { type: String, index: true },
  type: { type: String, required: true },
  purchasely_id: { type: String, required: true },
  plan: { type: String, required: true },
  last_payload: { type: Object, required: true },
  effective_next_renewal_at: { type: Date },
  status: { type: String, required: true },
  store: { type: String, required: true },
  created_at: { type: Date, default: Date.now },
  updated_at: { type: Date, default: Date.now }
}, {
  timestamps: true
});

EntitlementSchema.index({ anonymous_user_id: 1, plan: 1, purchasely_id: 1, user_id: 1 }, { unique: true });

const Entitlement = mongoose.model('Entitlement', EntitlementSchema);
module.exports = Entitlement;


// Helpers
// ============================================================================

async function findOrInitializeEntitlement(payload) {
  let entitlement = await Entitlement.findOne({
    anonymous_user_id: payload.anonymous_user_id,
    plan: payload.plan,
    purchasely_id: payload.purchasely_id,
    user_id: payload.user_id
  });

  if (!entitlement) {
    entitlement = new Entitlement({
      anonymous_user_id: payload.anonymous_user_id,
      plan: payload.plan,
      purchasely_id: payload.purchasely_id,
      user_id: payload.user_id
    });
  }

  return entitlement;
}

function msToTime(timeInMs) {
  if (!timeInMs) return null;
  return new Date(timeInMs);
}

module.exports = { findOrInitializeEntitlement, msToTime };

Proposed Database structure:

CREATE TABLE entitlements (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    type STRING NOT NULL,
    purchasely_id STRING NOT NULL,
    anonymous_user_id STRING,
    user_id STRING,
    plan STRING NOT NULL,
    status STRING NOT NULL,
    store STRING NOT NULL,
    effective_next_renewal_at DATETIME NOT NULL,
    last_payload JSONB NOT NULL,
    created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
    updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
    CONSTRAINT unique_user UNIQUE (purchasely_id, user_id, anonymous_user_id)
);


4. Providing an API to the app to fetch users' entitlements

Once the Entitlement Events have been acknowledged with a response code HTTP 200, the Purchasely Platform notifies the SDK, which returns the result to the app.

The app shall then fetch a backend API and provide the user ID as an entry parameter to determine which entitlements shall be granted to the user inside the app.

If the user is logged-out, the entry parameter should be the anonymous user ID provided by the SDK.

Purchasely.anonymousUserId
Purchasely.anonymousUserId
Purchasely.getAnonymousUserId();
Purchasely.anonymousUserId;
Purchasely.getAnonymousUserId((anonymousId) => {
	console.log("Purchasely anonymous Id: " + anonymousId);
});
private PurchaselyRuntime.Purchasely _purchasely;

_purchasely.GetAnonymousUserId();

The backend shall respond with the entitlements associated with the user ID or anonymous user ID.

Sample backend code for managing the backend API:

#                                    CONTROLLER                                #
# ============================================================================ #
# Uses Sinatra

# API endpoint to fetch user entitlements
get '/entitlements' do
  anonymous_user_id = params['anonymous_user_id'].presence
  user_id = params['user_id'].presence
  if anonymous_user_id.nil? && user_id.nil?
    status 400
    return json({error: 'user_id or anonymous_user_id is required'})
  end

  entitlements =
    Entitlement
      .where(anonymous_user_id:, user_id:) # filter on the current user
      .where(status: Entitlement::ACTIVE_STATUSES) # only retrieve the active statuses
      .to_a
  status 200
  json entitlements.map { |e| {plan: e.plan} }
end


#                                SIMPLIFIED MODEL                              #
# ============================================================================ #
# Uses ActiveRecord

class Entitlement < ActiveRecord::Base
  attr_accessor(
    # AUTO_RENEWING | AUTO_RENEWING_CANCELED | IN_GRACE_PERIOD | ON_HOLD | PAUSED |
    # REVOKED | DEACTIVATED | UNPAID
    :status, 
  )
  
  ACTIVE_STATUSES = %w[
    AUTO_RENEWING
    AUTO_RENEWING_CANCELED
    IN_GRACE_PERIOD
  ]
end
// Controller
// ============================================================================
// Uses Express

const express = require('express');
const app = express();
const Entitlement = require('./models/entitlement');

app.get('/entitlements', async (req, res) => {
  const { anonymous_user_id, user_id } = req.query;

  if (!anonymous_user_id && !user_id) {
    return res.status(400).json({ error: 'user_id or anonymous_user_id is required' });
  }

  try {
    const entitlements = await Entitlement.find({
      $or: [
        { anonymous_user_id: anonymous_user_id || null },
        { user_id: user_id || null }
      ],
      status: { $in: Entitlement.ACTIVE_STATUSES }
    }).exec();

    res.status(200).json(entitlements.map(e => ({ plan: e.plan })));
  } catch (err) {
    res.status(500).json({ error: 'An error occurred while fetching entitlements' });
  }
});

const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
  console.log(`Server is running on port ${PORT}`);
});


// Model
// ============================================================================
// Uses Mongoose

const mongoose = require('mongoose');
const Schema = mongoose.Schema;

const EntitlementSchema = new Schema({
  anonymous_user_id: { type: String, index: true },
  user_id: { type: String, index: true },
  type: { type: String, required: true },
  purchasely_id: { type: String, required: true },
  plan: { type: String, required: true },
  last_payload: { type: Object, required: true },
  effective_next_renewal_at: { type: Date },
  status: { type: String, required: true },
  store: { type: String, required: true },
  created_at: { type: Date, default: Date.now },
  updated_at: { type: Date, default: Date.now }
}, {
  timestamps: true
});

EntitlementSchema.index({ anonymous_user_id: 1, plan: 1, purchasely_id: 1, user_id: 1 }, { unique: true });

EntitlementSchema.statics.ACTIVE_STATUSES = [
  'AUTO_RENEWING',
  'AUTO_RENEWING_CANCELED',
  'IN_GRACE_PERIOD'
];

const Entitlement = mongoose.model('Entitlement', EntitlementSchema);
module.exports = Entitlement;

What’s Next

You're all set with managing the entitlements! Now learn how to test your integration