Migrating from Adapty
Concept mapping and step-by-step history import from Adapty to Rovenue.
Rovenue covers the same subscription-management use cases as Adapty but runs on your own infrastructure. This page maps familiar Adapty concepts to their Rovenue equivalents, then walks through importing your existing subscriber and transaction history with the built-in data import tool.
Concept mapping
| Adapty | Rovenue |
|---|---|
Adapty.activate(apiKey) | Rovenue.configure({ apiKey }) — baseUrl is optional (self-hosting only) |
Adapty.getProfile().accessLevels | entitlement(id) / entitlementsAll() |
Adapty.identify(customerUserId) | identify(appUserId) — server-side bind + auto-transfer of prior installs |
Adapty.getPaywall() | getFunnel(placementId) — placements serve native funnels (see Placements) |
Adapty.getPaywallProducts() | Rovenue.getOfferings() |
SDK-driven Adapty.makePurchase(product) | Rovenue.purchase(pkg) — getOfferings() then purchase(pkg) |
Adapty.Delegates.onLatestProfileLoad | addChangeListener (RN) / changes AsyncStream (Swift) / SharedFlow (Kotlin) |
Purchase flow
The purchase flow in Rovenue mirrors Adapty's: call getOfferings() to
fetch your configured products, then pass the chosen package to purchase(pkg).
The SDK triggers the native store sheet (StoreKit 2 on iOS, Play Billing on
Android), validates the receipt server-side, and resolves with updated
entitlements and credit balance — no separate receipt-posting step required.
const offerings = await Rovenue.getOfferings();
const pkg = offerings.current?.packages[0];
if (pkg) {
try {
const result = await Rovenue.purchase(pkg);
if (result.isDeferred) { /* Ask-to-Buy / deferred — not an error, wait for it to resolve */ }
// result.entitlements / result.virtualCurrencies are already up to date
} catch (e) {
if (e instanceof RovenueError && e.kind === 'PurchaseCanceled') { /* user dismissed — ignore */ }
else throw e;
}
}
await Rovenue.restorePurchases();Key differences from Adapty
- Self-hosted. Rovenue runs on your own infrastructure — no revenue share, no third-party data processing. You own the database and control retention.
- Virtual currencies built-in. Rovenue ships a first-class multi-currency ledger for consumable products, loyalty systems, and metered billing. Balances are read client-side; spending is a server-side operation via
POST /v1/virtual-currencies/:appUserId/:code/transactions. - Identity model.
identify(appUserId)performs a server-side bind and automatically transfers any prior install's purchases and credits to the current device (similar to Adapty's identify-and-merge behaviour). CalllogOut()on sign-out to mint a freshrovenueId. Bulk or programmatic merges are available via the secret-key backend API. - Open source (AGPLv3). You can read, audit, and fork the full stack.
Importing your history
Rovenue's data import tool is a column-mapping engine with vendor presets, not a vendor-specific parser. It has built-in presets for RevenueCat's export shapes, but deliberately no Adapty preset: Adapty's own S3 export column table could not be confirmed against first-party documentation, and shipping a guessed mapping would risk silently mis-importing your data rather than flagging what it can't confirm. Every Adapty import instead goes through the generic mapper, described below — this is a deliberate design choice made because guessing wrong here is worse than asking you to map columns once by hand, not a stopgap for a mapping we simply haven't gotten to yet.
Because there's no preset to detect against, the upload step never proposes a mapping automatically for an Adapty export. That's expected — proceed straight to mapping the columns yourself.
Export from Adapty
From the Adapty dashboard, request a data export of your subscribers and transactions (Adapty delivers this as files in your own S3 bucket or a downloadable CSV, depending on your plan). Download the transaction-level file — the one with a row per purchase/renewal/refund, not a customer-summary rollup — since that is what has enough detail to reconstruct purchase history.
Upload and map columns by hand
Upload the file to the import tool. Since no preset matches an Adapty export, you'll see an empty mapping — assign each of your file's source columns to the closest canonical field:
| Canonical field | Typical Adapty column | Required |
|---|---|---|
subscriberExternalId | your customer_user_id | yes |
store | store / platform column — accepted values only: app_store, play_store, stripe, promotional (case-insensitive; nothing else is guessed) | yes |
productIdentifier | product id / vendor product id | yes |
purchaseDate | purchase/transaction date | yes |
storeTransactionId | store transaction id (Apple original transaction id / Google purchase token / Stripe subscription id) | no, but strongly recommended |
expiresDate | expiration date | no |
isTrial / isIntroOffer | trial / introductory-price flags | no |
priceUsd | price in USD | no |
refundedAt | refund date | no |
Column names in your actual export may differ from the ones above — Adapty's
exact export schema is not something this guide can promise, which is the
same reason there's no built-in preset for it. Use the import tool's column
preview (it peeks your uploaded file's header) to see your file's real
column names and map from there. Only subscriberExternalId, store,
productIdentifier, and purchaseDate are required; leave anything you
can't map unmapped rather than guessing — an unmapped optional field is
simply not imported, which is always safer than a wrong mapping.
The store column only recognizes exactly app_store, play_store,
stripe, and promotional (case-insensitive, nothing else is guessed). A
column that instead reads ios, android, or a platform name Adapty
itself uses will fail every row in the file with an unrecognized-value
error — recode the column to one of the four accepted values before
importing.
A row with no usable value for storeTransactionId at all is not
imported — it fails as an invalid row (MISSING_STORE_TRANSACTION_ID,
visible in the dry run's invalidRow count) because there is nothing to
key a purchase on, and this importer never invents a value that claims to
be a real store transaction identifier. This is different from a row that
DOES have a transaction id but can't be re-verified live — the
"history-only" outcome the RevenueCat guide describes for Android rows
missing a purchase token — which imports fine, just without live-store
re-verification. If your export can't reliably provide a
storeTransactionId, get one into it before importing; there is no
separate supplemental pass for Adapty the way there is for RevenueCat's
Google-token file (and that supplemental pass isn't functional yet either
— see that guide's note on it).
Dry run
Every import runs a dry run before it can commit — there's no way to skip it. Starting one queues an asynchronous job (the dashboard polls for completion); on a large export this can take a while, exactly because it reads the whole file. The dry run reports, per row, whether it will create a new purchase, update an existing one, or be skipped (unresolved product, sandbox, no store transaction id, invalid data). Since your mapping was built by hand rather than detected, review this output carefully — it's the first real signal that your column assignments matched your file's actual data.
Verify and commit
Committing runs two phases. Phase A writes every row the file carries as history — subscribers, purchases, revenue events — without contacting Apple, Google, or Stripe. Phase B then re-validates every row that carries a real store transaction id against the store itself and sets its live status. A row the store no longer recognises is left alone as history, never deleted — this is a deliberate difference from Adapty's own importer, which drops a stale transaction outright.
Large imports can outrun a single verification pass — store APIs are
rate-limited, and Android in particular is a documented quota hazard. If
Phase B runs out of budget partway through, the job ends in
VERIFICATION_INCOMPLETE rather than failing outright; every anchor already
resolved is remembered, so resuming the job continues from where it left
off instead of re-verifying from scratch. A job the tool is still actively
verifying shows as VERIFYING.
Cut the SDK over
Once you're satisfied with the import, point your app at Rovenue: install
the SDK for your platform, call configure(), and call identify(appUserId)
with the same id your export used as customer_user_id /
subscriberExternalId so the SDK resolves to the subscriber your import
created. restorePurchases() re-validates any purchase the store still
knows about, which is a useful sanity check immediately after cutover.
What doesn't survive the move
Rovenue's import is a best-effort history reconstruction, not a full data migration, and no importer — in either direction — can promise otherwise. Adapty's own documentation makes the same disclaimer about importing competitors' data, and it applies here too. Specifically, the following are not carried over:
- Full event history. The import writes purchases and revenue events from the export's transaction rows; it does not reconstruct every underlying lifecycle event Adapty ever recorded.
- Original historical prices where the export itself doesn't carry them — the import only ever uses the price a row actually reports; it never estimates or backfills one.
- Full renewal chains beyond what the export lists as individual rows. If your export doesn't include a renewal, Rovenue has no way to know it happened.
- Promotional entitlements with no underlying store transaction. An access grant Adapty made outside a real purchase has nothing to import from unless your export represents it as a row with its own identifiable transaction id.
Uploaded export files are end-user PII and are deleted automatically after
a retention window once a job finishes. A job still actively resumable
(VERIFICATION_INCOMPLETE / VERIFYING) is never swept while it's in
that state — the sweep would strand it incomplete forever. A FAILED or
CANCELLED job, though also re-runnable from the mapping-editor step
without re-uploading, IS swept on the same clock as a finished job once
the window elapses; plan any mapping fix and retry within that window, or
re-upload the same file as a new job afterward.