Migrations
Migrate subscribers between plan versions — an OPERATE verb you run against live state, never authored in the manifest.
Migrate subscribers between plan versions — an OPERATE verb you run against live state, never authored in the manifest.
When you change a plan, existing subscribers don't move automatically. By default they're grandfathered: they keep their current plan version (and price) at renewal, while new subscribers get the new version. Moving existing subscribers onto a new version is a migration — a transition over live subscriber state, not a property of the product.
That distinction is why migration is an operate verb, not part of your
@Product class. A manifest is a snapshot of what the product is; a migration
is a transition you run once, against subscribers who exist right now. There is
no @Migration decorator — you run a migration via the CLI or MCP.
# Move "pro" subscribers from version 1 to the latest version at each
# subscriber's next renewal.
farthershore plan migrate croncloud pro \
--from 1 \
--to head \
--policy next_renewal \
--format json
You pass the plan key plus a source and target version. Each version is an
integer (e.g. 1) or the alias head / latest; the server resolves them to the
right plan versions. A policy decides when and how subscribers move.
| Flag | Meaning |
|---|---|
<planKey> | Positional — the stable plan key to migrate within (e.g. pro). |
--from <version> | Source version: an integer, or head/latest. Required. |
--to <version> | Target version: an integer, or head/latest. Required. |
--policy <policy> | When/how to migrate (see below). Required. |
--proration <p> | Optional billing reconciliation: none, prorate, or credit. |
--complete-by <iso> | ISO 8601 deadline — required by the by_date policy. |
| Policy | Behaviour |
|---|---|
grandfather | Keep existing subscribers on their current version (the default — running it explicitly is a no-op transition). |
next_renewal | Migrate each subscriber at their own next renewal. |
immediate | Migrate everyone now. |
by_date | Migrate everyone by a deadline (pass --complete-by). |
opt_in | Let subscribers choose when to move. |
# Migrate everyone immediately, prorating the price difference.
farthershore plan migrate croncloud pro \
--from 1 --to 2 --policy immediate --proration prorate --format json
# Migrate everyone by a hard deadline.
farthershore plan migrate croncloud pro \
--from 1 --to head --policy by_date --complete-by 2026-09-01T00:00:00Z --format json
Migration is owner-only and idempotent — it accepts an idempotency key, so a
retried call won't schedule the batch twice. Use --dry-run to preview the batch
the policy would schedule without committing it.
Grandfathering is the verified default: do nothing and existing subscribers keep
their old plan version (and price) at renewal. Run a migration only when you
actually want to move them. immediate changes what live subscribers are billed
— prefer next_renewal or by_date unless you mean to charge the difference now.
# 1. (in product/product.config.ts) bump pro's price, then push.
# 2. confirm the new version is live:
farthershore plan list croncloud --format json
# 3. move existing subscribers at their next renewal:
farthershore plan migrate croncloud pro --from 1 --to head --policy next_renewal --format json
Migration is exposed over MCP as fs_plan_migrate (an operate action — it changes
live subscriber state, not the code-managed definition). It takes productId,
planKey, fromVersion, toVersion, policy, optional proration, and
completeBy. Unlike plan create/update/delete — which are contract class and
must be done in the repo — migration is intentionally callable via the API.