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, entitlements mostly come from RevenueCat (the subscription truth) with paywall metadata from Superwall — both sync into Amba via verified webhooks, and both end up in the same user_entitlements table.

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.

Checking entitlements in the SDK

Both methods hit GET /client/entitlements:

import { Amba } from '@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 '@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/webhooks/revenuecatInbound subscription events (see Webhooks)
POST/webhooks/superwallInbound paywall events

Next

On this page