Using your own backend
If you handle subscription logic and receipt validation on your own backend, follow this guide to keep Superwall in sync.
Superwall works out of the box without any backend integration. You only need this guide if your own server is involved in subscription logic.
Common reasons to integrate your own backend include:
- You validate receipts server-side (via the App Store Server API or the Google Play Developer API) instead of trusting the device.
- Your app's entitlements are served from your own API rather than derived from the local receipt.
- You share subscription state across platforms (for example, a subscription purchased on the web that unlocks features in your app).
You can integrate your own backend with Superwall in one of two ways:
- Letting Superwall handle purchases (recommended): Keep Superwall's default purchasing behavior and forward completed transactions to your backend for validation and record-keeping.
- Using a purchase controller: Take end-to-end control of the purchasing pipeline: you make the purchase, validate it on your server, and tell Superwall the user's subscription status.
PurchaseController if your backend must be the source of truth for the subscription status Superwall uses on-device. If you just need your server to know about purchases, the first option is much simpler.Letting Superwall handle purchases
By default, Superwall handles purchasing, restoring, and subscription status for you — there's nothing to configure. To keep your backend informed, listen for completed transactions and send them to your server for validation.
1. Forward transactions to your backend
Use the Superwall delegate to respond to completed transactions and send their identifiers to your backend. Your backend can then verify the purchase directly with Apple or Google:
import { useSuperwallEvents } from "expo-superwall"
export function TransactionSync() {
useSuperwallEvents({
onSuperwallEvent: async ({ event }) => {
if (event.event !== "transactionComplete") {
return
}
// Send the transaction to your backend. Your backend can verify it
// with the App Store Server API or the Google Play Developer API.
await sendTransactionToBackend({
productId: event.product.productIdentifier,
transaction: event.transaction,
})
},
})
return null
}2. Keep your backend in sync with webhooks
Renewals, cancellations, refunds, and billing issues happen off-device, so listening in the app isn't enough. Superwall can send webhooks to your backend for subscription and payment events — use them to keep your server's subscription records up to date without polling store APIs yourself.
When Superwall handles purchases, don't set subscriptionStatus yourself — the SDK updates it automatically after purchases, restores, and receipt checks. If your backend needs to control the status Superwall uses on-device, use a purchase controller instead.
Using a purchase controller
With a PurchaseController, you take over purchasing and restoring, and you become responsible for setting Superwall.shared.subscriptionStatus from your backend's entitlement state. See Purchases and Subscription Status for a full explanation of this mode.
1. Create a PurchaseController
Create a new file called MyBackendPurchaseController, then copy and paste the following. Replace fetchSubscriptionStatusFromBackend() and sendTransactionToBackend(...) with calls to your own API — they're placeholders for whatever your backend exposes:
import { useEffect } from "react"
import {
CustomPurchaseControllerProvider,
SuperwallProvider,
useUser,
} from "expo-superwall"
const SUPERWALL_API_KEYS = {
ios: "MY_SUPERWALL_IOS_API_KEY",
android: "MY_SUPERWALL_ANDROID_API_KEY",
}
// Fetches entitlements from your backend and keeps Superwall in sync.
function SubscriptionSync() {
const { setSubscriptionStatus } = useUser()
useEffect(() => {
const sync = async () => {
// Ask your backend which entitlements this user has, e.g. ["pro"].
const entitlementIds = await fetchSubscriptionStatusFromBackend()
setSubscriptionStatus({
status: entitlementIds.length === 0 ? "INACTIVE" : "ACTIVE",
entitlements: entitlementIds.map((id) => ({
id,
type: "SERVICE_LEVEL",
})),
})
}
sync()
}, [setSubscriptionStatus])
return null // This component just handles the sync
}
export default function App() {
return (
<CustomPurchaseControllerProvider
controller={{
onPurchase: async (params) => {
try {
// Purchase `params.productId` with your own billing implementation
// (StoreKit / Google Play Billing / a library like react-native-iap).
// On Android, `params.basePlanId` and `params.offerId` identify the
// subscription offer selected on the paywall.
// Then send the transaction or purchase token to your backend for
// validation.
await purchaseWithYourBilling(params)
} catch (error: any) {
return { type: "failed", error: error.message }
}
},
onPurchaseRestore: async () => {
try {
// Restore purchases with your billing implementation and send any
// transactions your backend doesn't know about to your backend.
await restoreWithYourBilling()
} catch (error: any) {
return { type: "failed", error: error.message }
}
},
}}
>
<SuperwallProvider apiKeys={SUPERWALL_API_KEYS}>
<SubscriptionSync />
{/* Your app content */}
</SuperwallProvider>
</CustomPurchaseControllerProvider>
)
}As discussed in Purchases and Subscription Status, this PurchaseController is responsible for handling the subscription-related logic. Take a few moments to look through the code to understand how it does this.
2. Configure Superwall
Initialize an instance of MyBackendPurchaseController and pass it in to Superwall.configure(apiKey:purchaseController:):
The CustomPurchaseControllerProvider example above is the complete setup — it wraps your SuperwallProvider and handles all purchase and restore logic, while SubscriptionSync keeps the subscription status up to date from your backend.
3. Sync the subscription status
Until you set the subscription status, it's .unknown — and paywall presentation is automatically delayed until it changes. Call syncSubscriptionStatus() when the app launches and whenever your backend's entitlement state changes, so Superwall always knows whether to show a paywall.
Keeping the status in sync across launches and devices
Your backend is the source of truth, so treat the on-device status as a cache of it:
- Identify users consistently. Call
identifywith the same user ID your backend uses so entitlements follow the user across devices. See User Management. - Fetch entitlements on launch. Call your backend's entitlement endpoint when the app launches (and when it returns to the foreground) and update the subscription status with the result.
subscriptionStatusis cached between app launches, so the previous value is used until your fetch completes. - Make server notifications your source of truth. Renewals, cancellations, refunds, and billing issues happen off-device. Subscribe your backend to App Store Server Notifications and Google Play's Real-time Developer Notifications so its entitlement records stay correct — the app then picks up changes the next time it syncs.
How is this guide?