Amba

Entitlements

Track which users have access to which features. Amba is the entitlements layer — wire any subscription source (or none) into it.

An entitlement is Amba's abstraction for "does this user have access to a feature." It's a row in user_entitlements keyed by (app_user_id, entitlement_id) that your app reads at runtime to gate paid content.

Amba is the entitlements layer. The subscription source is your choice. Grant entitlements directly from your server (Stripe, Paddle, manual ops, gift codes, comp grants, anything), wire in an optional integration like RevenueCat or Superwall, or mix the two. Every entitlement ends up in the same table and is read through the same client API regardless of where it came from.

Data model

user_entitlements:

ColumnPurpose
app_user_idThe user who owns the entitlement
entitlement_idLogical identifier (e.g. "premium")
product_idStore product identifier
is_activeWhether the entitlement is currently valid
store"app_store" / "play_store" / "stripe" / "manual" / ...
period_type"trial" / "intro" / "normal" / ...
purchase_dateFirst purchase timestamp
expiration_dateWhen the current period ends (NULL for lifetime)

The columns are whitelisted for segment targeting — see Segment operators.

Grant entitlements from your server

This is the universal path — works for any source.

curl -X POST '${BASE_URL}/admin/projects/{projectId}/users/{userId}/entitlements' \
  -H 'Authorization: Bearer ${DEV_TOKEN}' \
  -H 'Content-Type: application/json' \
  -d '{
    "entitlement_id": "premium",
    "is_active": true,
    "store": "stripe",
    "product_id": "stripe_premium_yearly",
    "expiration_date": "2027-04-01T12:00:00Z",
    "raw_data": { "stripe_subscription_id": "sub_…" }
  }'

The handler upserts on (app_user_id, entitlement_id), so a second call for the same pair refreshes the row in place. To revoke, pass is_active: false. To clear a column (e.g. extend a finite subscription to lifetime), pass that field as null.

Use this directly from:

  • Stripe / Paddle / web checkout webhooks — your backend receives the payment event, upserts the entitlement.
  • Manual admin tooling — support comps a month, refund a chargeback, grant a gift code.
  • Migrations from legacy systems — backfill entitlements once at import time.
  • App Store / Play Store Server Notifications — if you'd rather wire StoreKit Server Notifications and Google Play RTDN directly than use a middleman.
  • Your own paywall / subscription service — anything that knows whether a user should have access can write to this endpoint.

See the full reference: POST /admin/projects/:projectId/users/:userId/entitlements.

Optional: sync from RevenueCat

If you use RevenueCat for mobile subscriptions, Amba accepts its webhook and maps the events into the same user_entitlements table. Skip this section if you don't use RevenueCat.

RevenueCat eventEffect
INITIAL_PURCHASEUpsert active entitlement row
RENEWALExtend expiration_date
CANCELLATIONMark for expiry at period end
EXPIRATIONis_active = false
BILLING_ISSUEFlag on the entitlement for UI to surface

Setup: RevenueCat integration.

Optional: sync paywall events from Superwall

If you use Superwall, Amba mirrors its paywall events (shown, dismissed, purchased, ...) as engagement events named superwall_<event> so streaks, XP rules, and segments can target paywall behavior alongside first-party events. Skip if you don't use Superwall.

Setup: Superwall integration.

Checking entitlements in the SDK

Both methods hit GET /client/entitlements:

import { Amba } from '@layers/amba-expo';
 
const list = await Amba.entitlements.getAll();
// UserEntitlement[] — only active entitlements by default
 
const hasPremium = await Amba.entitlements.isActive('premium');
// boolean — true if the user has an active entitlement with that id

UserEntitlement shape:

interface UserEntitlement {
  entitlement_id: string;
  product_id: string | null;
  is_active: boolean;
  store: string | null;
  period_type: string | null;
  purchase_date: string | null; // ISO-8601
  expiration_date: string | null; // ISO-8601
}

Example: gate a premium feature

import { useEffect, useState } from 'react';
import { Amba } from '@layers/amba-expo';
 
function PremiumGate({ children }) {
  const [unlocked, setUnlocked] = useState<boolean | null>(null);
  useEffect(() => {
    Amba.entitlements
      .isActive('premium')
      .then(setUnlocked)
      .catch(() => setUnlocked(false));
  }, []);
  if (unlocked === null) return null;
  if (!unlocked) return <Paywall />;
  return children;
}

Targeting by entitlement

Entitlement fields can drive segment rules, which in turn drive push campaigns and remote-config overrides:

{
  "name": "Active Premium",
  "rules": {
    "operator": "AND",
    "conditions": [
      { "field": "entitlements.is_active", "op": "eq", "value": true },
      { "field": "entitlements.product_id", "op": "eq", "value": "premium_monthly" }
    ]
  }
}

See Segment operators for the full list of supported entitlement fields.

Local caching

The SDK does not cache entitlements client-side beyond the fetch lifetime. For performance-sensitive gates, fetch once on launch (after Amba.init() resolves) and keep the result in React state / a context.

Do not persist entitlement state to local storage and trust it later — subscription state can change server-side (cancellations, billing issues). Always confirm against the server before granting access to paid content.

Routes reference

MethodPathDescription
GET/client/entitlementsList the current user's entitlements (active).
POST/admin/projects/:projectId/users/:userId/entitlementsServer-side grant / refresh. Universal path — works for any subscription source.
POST/webhooks/revenuecatOptional. Accepts RevenueCat subscription events.
POST/webhooks/superwallOptional. Accepts Superwall paywall events.

Next

On this page