← All articles
Jun 12, 20263 min read

Comp Subscriptions Without the Jank: RevenueCat Promo Entitlements End to End

  • RevenueCat
  • subscriptions
  • React Native
  • in-app purchases
  • webhooks
  • North
Comp Subscriptions Without the Jank: RevenueCat Promo Entitlements End to End

Every subscription app eventually needs to give premium away — support gestures, beta thanks, founder comps. RevenueCat makes the grant itself one API call. What it doesn't hand you is everything around the call: what the user sees, what your admin tooling does, and the awkward seconds where your database hasn't heard the news yet. Shipping this in North touched all three.

The client: "A gift from North," not "rc_promo_monthly_xxx"

A promotional entitlement is synthetic — there's no real App Store product behind it. RevenueCat reports the entitlement with a product identifier like rc_promo_premium_monthly, which produces two kinds of jank if you treat it like a purchase:

  1. Store lookups fail. The subscription summary logic tried to resolve localized pricing for the product ID against StoreKit — which has never heard of it. Logged warnings on every check for every comped user.
  2. Users see internals. The manage-subscription screen rendered the raw product ID where a plan name belongs. To the person you just comped, that reads as a glitch — the opposite of the warm gesture the grant was supposed to be.

The fix keys off the one reliable signal RevenueCat gives you — the entitlement's store field:

const isComplimentary = ent.store === "PROMOTIONAL";

When true, North skips the store pricing lookup entirely and the UI renders "A gift from North" instead of plan-and-price. One branch, but it converts an accounting artifact into a product moment. If you do nothing else from this post: comped users will read that screen, and it should say something a human would say.

The admin side: grant through RevenueCat, never around it

North's back office grants circle premium via the RevenueCat REST API — it deliberately does not flip the is_premium column in our own database, even though that would be one line and instant. RevenueCat stays the single source of truth for entitlements; the grant flows API → RevenueCat → webhook → our database, the same path a real purchase takes.

The discipline pays for itself the first time anything else touches entitlements. Expirations, revocations, refunds, cross-device restores — all of it flows through the same webhook pipeline. A database column written directly by the admin panel is a second source of truth, and second sources of truth don't drift eventually; they drift immediately and silently disagree forever.

The gap: polling through eventual consistency

The cost of doing it right is a visible delay: the operator clicks "grant premium," RevenueCat accepts, and our database won't reflect it until the webhook lands — seconds later, occasionally more. An admin UI that just refetches once shows nothing happened, and the operator's natural response is clicking grant again.

North's panel handles the gap explicitly: after a grant or revoke, it polls the database up to 6 times at 2-second intervals, holding a pending state until the badge flips or the attempts run out. Bounded, dumb, effective. The alternatives are heavier (push the webhook event to the admin client over realtime) or worse (optimistically flip the UI and hope) — for an internal tool, honest polling with a visible "applying…" state is the right amount of engineering.

It's the admin-panel version of a rule consumer UIs already know: when an action is asynchronous behind the scenes, show the in-between state instead of pretending there isn't one.

The checklist

  • Branch on store === "PROMOTIONAL" — skip store pricing lookups and write a human label.
  • Grant and revoke through the provider's API; let the webhook write your database.
  • Treat the webhook delay as UI state — poll with a cap, show progress, never leave the operator guessing.
  • Audit-log grants like the money operations they are (free premium is negative revenue with a nice smile).

None of it is hard. All of it is the difference between "comp granted" and comp granted well.

WRITTEN BY

Shahzaib Muhammad Akram

Senior Frontend EngineerCyberjaya, Malaysia