Amba

Entitlements

How Amba tracks user subscription state from RevenueCat and Superwall, and how to check it from your app.

An entitlement is Amba's abstraction for "does this user have access to a feature". For the utility apps Amba targets, mobile in-app purchases come from RevenueCat (the subscription truth) with paywall metadata from Superwall — both sync into Amba via verified webhooks. For non-mobile flows that don't go through RevenueCat — Stripe / Paddle web checkout, manual support grants, gift codes, ToS-violation revokes, migrations from legacy systems — there is an admin endpoint that writes the same user_entitlements row directly. Every entitlement, regardless of origin, ends up in the same table and is read through the same client API.

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" / ...
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.

Sync from RevenueCat

When RevenueCat fires a subscription event, the webhook route verifies the bearer token and Amba upserts the user_entitlements row in the background. Event mapping:

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

Sync is push-only (RevenueCat → Amba). Amba is not the source of truth for purchases; RevenueCat is.

Sync from Superwall

Superwall webhooks primarily carry paywall events (shown, dismissed, purchased). Amba mirrors these as engagement_events rows named superwall_<event> so streaks, XP rules, and segments can target paywall behavior alongside first-party events. Full subscription state still flows through RevenueCat.

Server-side grants (Stripe, Paddle, manual)

When a purchase happens outside the App Store / Play Store — Stripe Checkout for a web companion, Paddle invoicing, support manually granting a comp month, gift codes, ToS-violation revokes — call the admin endpoint to upsert the user_entitlements row directly:

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 — same semantics as the RevenueCat sync activity. To revoke, pass is_active: false. To clear a column (e.g. extend a finite subscription to lifetime), pass that field as null.

For mobile in-app purchases, prefer the RevenueCat webhook — it remains the source of truth for App Store / Play Store subscriptions and handles renewal / billing-issue / cancellation transitions automatically. The admin path is for everything else.

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

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 for non-RevenueCat flows (Stripe, Paddle, manual).
POST/webhooks/revenuecatInbound subscription events (see Webhooks).
POST/webhooks/superwallInbound paywall events.

Next

On this page