Migration guide

Migrate from RevenueCat without losing a subscriber

Import your project, run both systems side by side with forwarded store notifications, then ship one line. Your app code, offerings, API keys and customers stay the same.

Step 1

Import your RevenueCat project

The importer runs on your own machine with your own RevenueCat secret key (read-only is enough). It reads RevenueCat's REST API v2 and writes to your RevenueDot server:

  • Apps and their public SDK keys, so shipped builds keep working
  • Products, entitlements, offerings and packages, with metadata and the current offering
  • Customers, aliases, attributes, subscriptions and one-time purchases
  • Current access, so nobody loses a subscription on switch day

It is resumable and idempotent, so a re-run is also the incremental sync during the side-by-side run. Try --dry-run or --limit 50 first. Apple purchases are keyed by their original transaction; Google purchase tokens are recovered from order IDs through Google's Orders API with your own service account.

Importterminal
npx revenuedot import --from-revenuecat \
  --rc-key sk_...        # RevenueCat secret key, v2, read-only
  --rc-project proj...   # from the RevenueCat dashboard URL
  --to https://api.revenuedot.app \
  --to-key sk_...        # RevenueDot secret key
Checkterminal
# Compare active entitlements customer by customer
npx revenuedot import verify --rc-key sk_... --rc-project proj... --to ... --to-key sk_...

# Print the cutover steps with your own app ids and URLs
npx revenuedot import plan --to ... --to-key sk_...

Step 2

Run both systems side by side

Change the server notification URLs in App Store Connect and Google Play (Pub/Sub push) to RevenueDot. RevenueDot processes each notification and forwards it unchanged to RevenueCat, so both systems stay accurate while your current app version still talks to RevenueCat.

  • Compare customer info and webhooks between the two for as long as you like
  • Re-run the importer to pick up anything RevenueCat saw first
  • Undo at any time by pointing the notification URLs back
Notification URLsper app
# App Store Connect → App Information → App Store Server Notifications (v2)
https://api.revenuedot.app/v1/notifications/apple/<app id>

# Google Play → Monetization setup → Real-time developer notifications
# Pub/Sub push subscription endpoint
https://api.revenuedot.app/v1/notifications/google/<app id>

# RevenueDot dashboard → Apps → your app → Forwarding URL
# paste the notification URL RevenueCat gave you

The dashboard shows each app's exact notification URL and when the last notification arrived.

Step 3

Switch: one line in every SDK

Set the proxy URL before configure in your next release. Self-hosting? Use your own server's URL. When most active users are on the new version, turn RevenueCat off.

iOSSwift
// Before Purchases.configure
Purchases.proxyURL = URL(string: "https://api.revenuedot.app")!
Purchases.configure(withAPIKey: "appl_...")
AndroidKotlin
// Before Purchases.configure
Purchases.proxyURL = URL("https://api.revenuedot.app")
React Native and ExpoTypeScript
await Purchases.setProxyURL("https://api.revenuedot.app");
FlutterDart
await Purchases.setProxyURL("https://api.revenuedot.app");
WebTypeScript
Purchases.configure({
  apiKey: "rcb_...",
  appUserId,
  httpConfig: { proxyURL: "https://api.revenuedot.app" },
});
Capacitor and IonicTypeScript
await Purchases.setProxyURL({ url: "https://api.revenuedot.app" });
Kotlin MultiplatformKotlin
Purchases.proxyURL = "https://api.revenuedot.app"
UnityC#
// Inspector: Purchases component → Proxy URL
// https://api.revenuedot.app
CordovaTypeScript
Purchases.setProxyURL("https://api.revenuedot.app");

Signature checks

With the stock RevenueCat SDK, turn off its response-signature check (Trusted Entitlements), or it reports every RevenueDot response as unverified. On Android, the stock SDK also still sends paywall and ad events to RevenueCat. The RevenueDot forks below fix both.

Or swap the package

MIT forks of all ten RevenueCat SDKs

Each fork keeps RevenueCat's class and method names and every name your code imports. It defaults to api.revenuedot.app, trusts RevenueDot's signing key so entitlements verify, and sends every event to your server.

iOS forkiOS, macOS, tvOS, watchOS, visionOS
// Swift Package Manager
.package(url: "https://github.com/revenuedot/purchases-ios", exact: "<version>-revenuedot")
// CocoaPods
pod "RevenueDotPurchases"   // still: import RevenueCat

Pod RevenueDotPurchases; SPM from github.com/revenuedot/purchases-ios with -revenuedot tags. import RevenueCat stays. revenuedot/purchases-ios

Android forkAndroid
// build.gradle.kts
implementation("app.revenuedot.purchases:purchases:<version>")
// still: import com.revenuecat.purchases.*

Maven group app.revenuedot.purchases, same artifact ids. Kotlin packages com.revenuecat.purchases.* stay. revenuedot/purchases-android

React Native and Expo forkiOS, Android
// package.json: an npm alias keeps every import
"react-native-purchases": "npm:@revenuedot/react-native-purchases@<version>"

npm @revenuedot/react-native-purchases through an alias, so imports do not change. revenuedot/react-native-purchases

Flutter forkiOS, Android, web
# pubspec.yaml
purchases_flutter:
  git:
    url: https://github.com/revenuedot/purchases-flutter
    ref: <version>-revenuedot

Git dependency with -revenuedot tags; the package name purchases_flutter stays. The fork also fixes setProxyURL on Flutter web. revenuedot/purchases-flutter

Web forkBrowsers
// package.json
"@revenuecat/purchases-js": "npm:@revenuedot/purchases-js@<version>"

npm @revenuedot/purchases-js through an alias. The fork also sends analytics events to the proxy URL. revenuedot/purchases-js

Capacitor and Ionic forkiOS, Android
// package.json
"@revenuecat/purchases-capacitor": "npm:@revenuedot/purchases-capacitor@<version>"

Install through the alias so Capacitor's generated native names stay the same. revenuedot/purchases-capacitor

Kotlin Multiplatform forkiOS, Android
implementation("app.revenuedot.purchases:purchases-kmp-core:<version>")

Maven app.revenuedot.purchases:purchases-kmp-*. Packages com.revenuecat.purchases.kmp.* stay. revenuedot/purchases-kmp

Unity forkiOS, Android
// OpenUPM
openupm add com.revenuedot.purchases-unity

OpenUPM com.revenuedot.purchases-unity. using RevenueCat; stays. revenuedot/purchases-unity

Cordova forkiOS, Android
cordova plugin add @revenuedot/cordova-plugin-purchases

npm @revenuedot/cordova-plugin-purchases. The plugin id and the global Purchases stay. revenuedot/cordova-plugin-purchases

Fork packages publish to their registries with the first release; until then, build from the repositories. The shared layer for the cross-platform SDKs is revenuedot/purchases-hybrid-common.

FAQ

Migration questions

How do I migrate from RevenueCat without losing subscribers?

Run the importer with a read-only RevenueCat secret key to copy your catalog, SDK keys, customers and purchase history. Point App Store and Google Play notifications at RevenueDot, which forwards each one to RevenueCat so both systems stay current. Then ship the one-line proxy change. Current access is imported, so no subscriber loses access on switch day.

Do I have to change my app to switch?

One line. Set the SDK's proxy URL to your RevenueDot server before configure. Offerings, purchases, restores, entitlements and customer info work as before. With the stock SDK you also turn off its response-signature check, or you use a RevenueDot SDK fork, which verifies RevenueDot's signatures.

How do Google Play purchases migrate if RevenueCat doesn't export purchase tokens?

The importer looks up each purchase token from the order IDs in RevenueCat's data through Google's Orders API, using your own Play service account. Renewal notifications and one syncPurchases() call in the app fill any gaps.

Do my existing RevenueCat API keys keep working?

Yes. The importer copies your apps' public SDK key strings (appl_, goog_, rcb_) into RevenueDot, so builds already in the App Store and Google Play authenticate against RevenueDot without a new key.

Will the importer send webhooks for old purchases?

No. Imported history is written directly, with no events and no webhooks, so your backend does not see a flood of old purchases. Pass --emit-events only if you want them.

What does the importer not bring over yet?

Paywalls, targeting rules, experiments, integrations other than webhooks and virtual currency balances. RevenueCat's v2 API does not expose subscription refunds, so a refunded subscription imports as expired.

Which stores and SDK features are supported?

App Store (StoreKit 1 and 2, App Store Server API, Server Notifications v2) and Google Play (Play Developer API, real-time notifications) first. Because RevenueDot serves the API the RevenueCat SDKs call, Expo, StoreKit 2 and current Google Play Billing work through the SDKs you already use. Amazon and Stripe come next.

Can I self-host RevenueCat?

RevenueCat itself is closed source and runs only in RevenueCat's cloud. RevenueDot is a self-hostable server that works with the RevenueCat SDK, so self-hosting means running RevenueDot with Docker and Postgres on your own servers, in the region you choose.

Get started

Try the migration on a copy first.

Run the importer with --dry-run against a self-hosted RevenueDot on your laptop. Nothing changes in RevenueCat or in your app.