# Migrate an existing app

Move your configuration, verify existing purchases, then release the new integration in a controlled cohort. CherrySub provides a catalog importer in the dashboard and CLI, a RevenueCat V2 catalog reader, and a resumable CLI purchase importer. These tools preserve store identifiers and stable App User IDs while making the scope of each step explicit.

## Choose an import path

| Source | What moves | Entry point |
| --- | --- | --- |
| App Store Connect | Subscriptions and one-time product identifiers and names | **Products → Import from store → App Store** |
| Google Play | Subscriptions, their base plan IDs, and modern one-time products | **Products → Import from store → Google Play** |
| RevenueCat V2 API | Matching apps' products, entitlement definitions and mappings, offerings, and packages | `cherrysub migrate revenuecat` |
| Another provider or your own backend | Configuration converted to the portable catalog CSV/JSON format | **Migration → CSV / JSON**, or `cherrysub migrate file` |
| Existing purchase evidence | Verified Apple StoreKit 2 transactions or Google purchase tokens under your existing App User IDs | `cherrysub migrate purchases` |

The current hosted product is staging. Choose the destination deployment explicitly before configuring store notifications or releasing a production client. The importers do not write to RevenueCat, change store products, cancel subscriptions, or charge customers.

## Prepare the destination

Create or select the CherrySub project and app. The app's Apple bundle ID and Google package name must match the source. An owner or admin with a verified email performs catalog imports and manages store credentials. Member accounts can inspect ordinary product data but cannot import it.

Connect Google Play before listing its products or verifying Google purchase tokens. Apple catalog reads need an **App Store Connect API key**, distinct from the **In-App Purchase key** used for server history operations. Upload the App Store Connect `.p8`, Issuer ID, and Key ID in the import screen. It is encrypted separately and only masked metadata is returned. Saving validates the credential format; **Load store products** checks the key's actual catalog access. Replacing the catalog key clears the loaded selection. Disconnecting Apple removes both managed Apple keys.

Your store account must be able to read the destination app. The importer checks its bundle/package identity, and an Apple numeric app ID, when configured. [Apple connection](/docs/app-store) · [Google connection](/docs/google-play).

## Import directly from a store

Open **Products → Import from store**, choose the store, and load its catalog. CherrySub reads every page before enabling the preview. Search the products, select the required items, and optionally attach one existing access level using a store-managed mapping. Leaving products unmapped is supported; no purchase or access-level definition is required to register them.

The preview lists additions, existing entries that will be kept, and conflicts. Apply only after reviewing it. A catalog import creates configuration; it does not create customers, purchases, or access grants. Next, check [access mappings](/docs/access-levels) and [product sets](/docs/product-sets) before verifying existing purchases.

The same store read is available from the CLI. Without `--apply`, it only previews the result:

```sh
cherrysub migrate store apple APP_ID
cherrysub migrate store google APP_ID --product premium,credits
cherrysub migrate store google APP_ID --product premium,credits --apply
```

The dashboard reads up to 100 pages and 1,500 products for selection. A single normalized import can contain 2,000 configuration entries in total. Products, levels, mappings, sets, and packages each count toward that limit, so mapping a large selection may require smaller batches. CLI `--product` filters exact store IDs; it still completes source pagination before creating a plan. Failed or repeated pages stop the import rather than silently applying a partial catalog.

## Move a RevenueCat catalog

Install and sign in to the [CherrySub CLI](/docs/cli). Use a RevenueCat **V2 secret API key** with read access to Apps, Products, Entitlements, and Offerings. Its public SDK key and V1 secret key are not substitutes. Supply the source project ID to avoid needing project-list permissions. The CLI prompts for the key without displaying it; automation can supply `CHERRYSUB_REVENUECAT_API_KEY` through its secret environment.

```sh
# Read the source and preview changes in your selected CherrySub project.
cherrysub migrate revenuecat APP_ID --source-project RC_PROJECT_ID

# Apply after reviewing the additions and confirming.
cherrysub migrate revenuecat APP_ID --source-project RC_PROJECT_ID --apply

# Export a private portable file for review in the dashboard.
cherrysub migrate revenuecat APP_ID --source-project RC_PROJECT_ID --output ./catalog.json
```

`--project` always identifies the **CherrySub** project; `--source-project` identifies the **RevenueCat** project. Bundle/package matching selects source apps. If more than one source app matches a platform, pass the intended IDs with `--source-app`. Explicit selection still must match the destination identity. Apple App Store, Mac App Store, and Google Play are supported source types; other store types need a separate mapping plan.

The source API key stays on your computer. CherrySub receives the portable configuration, not the RevenueCat key. The reader follows scoped pagination, respects bounded rate-limit retries, and fails when product associations change during export. An export has a 300-request budget plus bounded retries, 30 pages per list, and a ten-minute overall read budget. For a larger catalog, migrate smaller source selections or portable files.

RevenueCat entitlements become access levels, and offerings become product sets. Store-managed mappings preserve entitlement lookup keys. Packages receive a store suffix because CherrySub packages contain one platform/product pair: `$rc_monthly` becomes `rc_monthly_apple` or `rc_monthly_google`. The report lists these changes. An existing current product set stays selected unless you explicitly request `--activate-current`, or enable the equivalent dashboard checkbox. An imported source offering is not automatically activated.

### Google base plans and access policy

RevenueCat can represent a subscription as `productId:basePlanId`. CherrySub separates that into the exact Google subscription product ID and the package's `base_plan_id`. Base plan metadata is preserved in product lists and current product-set responses. Your native Billing integration must use that base plan when choosing the offer token; localized pricing still comes from Google.

CherrySub currently grants access by subscription **product ID**. If source base plans for the same product grant different entitlements, the RevenueCat reader blocks the import. Review and resolve that policy before continuing; combining the grants would change customer access. Legacy Google SDK eligibility variants are skipped with a note, and unsupported conditional associations require manual review.

## Use the portable catalog format

Download the [CSV template](/import-templates/catalog.csv) or [JSON template](/import-templates/catalog.json). Convert another provider's catalog to these fields. A generic analytics or customer export is not a catalog file and is rejected if it contains unrelated columns.

```csv
platform,product_id,display_name,product_type,access_level,access_level_name,grant_mode,product_set,product_set_name,package_id,package_type,position,base_plan_id
google,premium,Premium,subscription,premium,Premium access,store_managed,default,Main offering,monthly_google,monthly,0,monthly
```

Products can be the only entities in a JSON catalog. Optional `access_levels`, `mappings`, and `product_sets` add the corresponding configuration. Use schema `version: 1`, source `manual` for your own JSON, and the exact snake-case fields in the template. Product types are `subscription`, `one_time_product`, or `unknown`. Grant modes are `store_managed` and `lifetime`. A lifetime mapping changes access policy; select it deliberately rather than inferring it from a package label.

Files are limited to 512 KiB and 2,000 normalized entries. Product IDs and display names support up to 512 bytes; product-set display names support 256. Catalog lookup keys and package identifiers use 1–128 letters, digits, dots, underscores, or hyphens. Package positions are unique within a set and range from 0 through 1,000. A Google base plan can appear on a Google package only.

```sh
cherrysub migrate template > catalog.csv
cherrysub migrate file ./catalog.csv APP_ID
cherrysub migrate file ./catalog.json APP_ID --apply
cherrysub migrate history APP_ID
```

The template contains examples. Replace their identities and product IDs before applying them. `--yes` supplies explicit noninteractive confirmation when used with `--apply`; it does not skip validation or conflict detection.

## Understand the preview and apply boundary

The preview is read-only. Its digest binds the source configuration, destination app/project, acting account, current target state, and current-set activation choice. Apply rebuilds the plan under database locks and rejects a stale digest. Refresh the preview if another administrator changes the catalog before application.

Identical entries are kept. Imports add missing configuration; they do not overwrite names, grant modes, or existing packages. An archived access key, incompatible mapping, package identifier collision, or occupied package position blocks the import. Fix the source file or explicitly edit the target through its normal management flow, then preview again.

Each application commits all additions and its summary together. Retrying the same applied request returns its existing result; a changed file cannot reuse an earlier acknowledgment. The dashboard offers a downloadable report and recent successful import history. History retains counts and actor metadata rather than uploaded files or purchase evidence. Keep the portable source and report if you need your own audit archive.

There is no one-click catalog rollback. Remove unwanted packages or mappings through their normal management screens after reviewing their impact. The importer leaves existing configuration intact and keeps the current set unchanged by default to make staged validation easier.

## Verify existing purchases with a checkpoint

Preserve the exact App User IDs used by your application. Export the original purchase tokens or signed transactions from the system that retained them. A RevenueCat catalog API export does **not** include these records. RevenueCat analytics exports, entitlement flags, and subscription totals are not replacements for verifiable store evidence.

Apple inputs must be StoreKit 2 signed transaction JWS values. Legacy StoreKit 1 receipt blobs are not supported by this importer. Google inputs require `product_type` of `subscription` or `one_time_product`. Store APIs may no longer return old or expired evidence; retain your previous reporting system for history that cannot be reconstructed.

```sh
cherrysub migrate purchases --template > purchases.csv
# Populate the file with your own verified-source evidence and stable user IDs.
cherrysub migrate purchases ./purchases.csv

# Supply CHERRYSUB_APP_SECRET through a secret environment, or enter it at the prompt.
cherrysub migrate purchases ./purchases.csv --apply --checkpoint ./purchase-import.jsonl

# After interruption, use the same input file, API origin, and app secret.
cherrysub migrate purchases ./purchases.csv --apply --resume --checkpoint ./purchase-import.jsonl
```

The **app secret selects the destination app**. The dashboard selection, positional app IDs from catalog commands, and `--project` do not scope this runtime command. Use the intended `iap_sk_` key and profile/API origin. The request uses bearer authentication without sending the dashboard cookie.

Preflight reads at most 64 MiB and 20,000 records. It checks required fields and blocks a purchase assigned to different App User IDs in the same file, including different Apple transactions in the same original transaction chain. It does not verify an Apple signature locally or decide access from untrusted JWS claims. Every applied record goes to `/v1/receipts`; only an accepted, server-verified receipt with a valid event ID enters the completed checkpoint.

Completed records are skipped on resume. The private JSONL checkpoint stores input/key hashes, API origin, row numbers, row digests, and accepted event IDs. It contains no receipt, purchase token, app secret, or plaintext App User ID. An incomplete final append is recovered and that record can be retried. A lock prevents concurrent use of the same checkpoint. After an abrupt process crash, remove a stale `.lock` only after confirming no importer still runs.

Do not edit an input file while resuming it. An API-origin, key, or file mismatch rejects the checkpoint. Correct rejected records in a separate file with a new checkpoint, or retry the unchanged file after fixing destination configuration. The row report contains safe errors and HTTP status codes. Authentication failures, rate limits, service failures, and network errors stop the run; previously accepted records remain checkpointed.

Normal access-change webhook delivery may occur. This is ordinary verified ingestion, not a silent bulk-history service. Establish access mappings first and prepare downstream receivers for existing customer observations. No arbitrary grants, synthetic transactions, tax/proceeds history, aliases, paywall layouts, experiments, or targeting rules are manufactured.

## Integrate the client and cut over

If purchase evidence is unavailable server-side, use your application and platform's supported restore/sync path as customers return. Follow the actual SDK methods in [Receipts and restores](/docs/receipts-and-restores); there is no assumption that every platform exposes a RevenueCat-compatible `syncPurchases` method. Returning-user sync cannot migrate customers who never reopen the app.

Keep a single owner for finishing, acknowledging, or consuming each store purchase. Test anonymous-to-authenticated transitions and verified restore ownership without assuming another provider's customer alias/merge semantics. [Customer identity](/docs/customer-identity).

1. Compare imported product IDs, access lookup keys, package order, and Google base plans.
2. Verify existing and new sandbox purchases, renewal, expiry, refund, restore, and your intended account-switch policy.
3. Compare current access against authoritative store evidence. Track the customers and cases sampled in your own cutover checklist.
4. Configure notification destinations and webhook consumers for the intended deployment, preserving a rollback plan for retained clients.
5. Release to a controlled cohort and monitor failed verification, unfinished purchases, delivery retries, and access mismatches.
6. Retire the previous integration only after the replacement behavior is proven.

First-seen cohorts, historical revenue, and lifetime metrics can differ from a provider that observed customers earlier. CherrySub does not currently offer an automated cross-provider parity work queue or complete historical ledger import. Document the observation start date and retain source reports rather than forcing totals to match.

## RevenueCat comparison and sources

RevenueCat documents both server-side receipt import and client-side purchase synchronization. It also offers coordinated bulk imports and product selection from connected stores. CherrySub's current workflow adds a portable catalog preview, explicit conflict handling, atomic application, an import report, and local purchase checkpoints. These are concrete workflow features; full historical migration and provider-specific customer merging remain separate work.

The Google catalog reader uses the modern `oneTimeProducts` endpoint as well as subscriptions. This includes product configurations beyond the legacy one-time-product import restriction described in RevenueCat's current product guide. Store permissions and response availability still determine what can be listed.

Sources reviewed September 9, 2026: [RevenueCat subscription migration](https://www.revenuecat.com/docs/migrating-to-revenuecat/migrating-existing-subscriptions), [RevenueCat receipt imports](https://www.revenuecat.com/docs/migrating-to-revenuecat/migrating-existing-subscriptions/receipt-imports), [RevenueCat product import](https://www.revenuecat.com/docs/offerings/products-overview), and [Google one-time-product listing](https://developers.google.com/android-publisher/api-ref/rest/v3/monetization.onetimeproducts/list).
