Farther ShoreDocs
Go to Farther Shore
What is FartherShore
Install the CLI
Quickstart
Core concepts
The @Product class
Meters & resources
Features & routes
Capabilities & entitlements
Plans & pricing
The Manifest IR
Bring your own backend
Transport modes
Metering & verification
Runtime tokens
Frontend SDK
Root & data components
Auth & sessions
Entitlement gates
Connect Stripe
Subscriptions & usage
Plan changes & grandfathering
Reprice: edit and publishHow changes cascade: subscriberChangePolicyActively migrate subscribers: farthershore plan migrateRemoving a planRelated
Billing strategies
Apply & deploy
Environments
Migrations
Docs versions & archive
Operate with an agent
Operation classes
MCP server
End-to-end via CLI/MCP
CLI reference
@farthershore/product
@farthershore/backend
@farthershore/farthershore-js
Environment variables
Response & deny codes
Add a metered capability
Gate a feature
Change a price
Prepaid credits
Meter AI tokens
Operate via an agent
Prepare for launch
Status
Docs/Monetize/Plan changes & grandfathering

Plan changes & grandfathering

Change prices safely; existing subscribers keep their terms by default.

PreviousSubscriptions & usageNextBilling strategies

You change a plan the same way you author it: edit the @Plan in your product repo and publish. The key safety property is grandfathering — when you reprice or restructure a plan, existing subscribers keep the terms they signed up under. A new price applies to new subscribers immediately; current subscribers move only when their billing period rolls over, and only for changes that are safe to apply that way. Actively pulling existing subscribers onto a new version is a separate, explicit operate action — never a side effect of publishing.

This split is deliberate. A @Product class is a snapshot of what the product is now. Moving live subscribers is a transition over real billing state — so it is an operate verb (farthershore plan migrate), not something a declaration can silently trigger.

Reprice: edit and publish

@Plan("pro", {
  name: "Pro",
  price: { amount: 24900, currency: "usd", interval: "month" }, // was 19900
  limits: { requests: { rate: 6000, interval: "minute", enforcement: "enforce" } },
})
pro!: unknown;
# Commit product/ and push — the GitHub bot validates and applies the release.
git add product/ && git commit -m "raise Pro to $249" && git push

# Or cut the release yourself; the bump is auto-derived from the change:
farthershore product publish croncloud
farthershore product publish croncloud --dry-run   # preview the bump + reasons

Publishing cuts a new plan version. New subscribers get $249. Everyone already on Pro keeps $199 until you migrate them — that's the default.

A live product's contract is managed as code. Editing pricing through the API or CLI returns MANAGED_BY_CODE (exit 4) — author the change in product/ and push instead. A breaking change is refused by publish unless you pass --accept-breaking.

How changes cascade: subscriberChangePolicy

The product's billing.subscriberChangePolicy decides, per kind of change, whether an existing subscriber switches immediately or at period_end (their next renewal — the grandfathering window). It's authored on the product billing block via @Raw (the typed sugar lives at the platform schema, so use the escape hatch):

import { Product, Plan, Raw } from "@farthershore/product";

@Product({ name: "croncloud", origin: "https://api.example.com" })
@Raw({
  productPatch: {
    billing: {
      subscriberChangePolicy: {
        default: "period_end",
        when: {
          price_increase: "period_end",   // grandfather raises until renewal
          price_decrease: "immediate",    // pass savings along now
          feature_added: "immediate",     // more value, apply now
          feature_removed: "period_end",
          limit_increased: "immediate",
          limit_reduced: "period_end",
        },
        // Consent gates — required to apply the harsher direction immediately:
        allowImmediatePriceIncrease: false,
        allowImmediateEntitlementReduction: false,
      },
    },
  },
})
export default class CronCloud {
  @Plan("pro", {
    name: "Pro",
    price: { amount: 24900, currency: "usd", interval: "month" },
    limits: { requests: { rate: , : , :  } },
  })
  pro!: ;
}

The defaults are subscriber-friendly: price increases and entitlement reductions wait for period_end (grandfathered), while price decreases and upgrades apply immediately. Timing is the only knob — Stripe owns the proration math at switch time.

The consent gates are enforced at compile time. Setting price_increase: "immediate" without allowImmediatePriceIncrease: true fails validation; the same guard covers feature_removed, limit_reduced, and credit_reduced under allowImmediateEntitlementReduction. You can't accidentally yank terms from a paying subscriber mid-period.

Actively migrate subscribers: farthershore plan migrate

When you do want existing subscribers off an old plan version, run the operate verb. It takes the plan key plus the source/target versions and a policy.

farthershore plan migrate croncloud pro --from 1 --to head --policy next_renewal
farthershore plan migrate croncloud pro --from 1 --to 2 --policy immediate --proration prorate
  • <productIdOrName> <planKey> — the product and the stable plan key (pro).
  • --from <version> / --to <version> — integers, or head/latest.
  • --policy <policy> — one of:
    • grandfather — keep existing subscribers as-is (no move).
    • next_renewal — migrate each subscriber at their own renewal.
    • immediate — migrate everyone now.
    • by_date — migrate everyone by a deadline (pass --complete-by <iso8601>).
    • opt_in — subscribers choose whether to move.
  • --proration <none|prorate|credit> — optional; how Stripe settles the switch.

This is an OPERATE-class action: it's allowed on code-managed products (it changes live subscriber state, not the product definition), it's idempotent, and it's available over MCP as fs_plan_migrate. Preview first with --dry-run.

farthershore plan migrate croncloud pro --from 1 --to head --policy next_renewal --dry-run

Removing a plan

You cannot delete a plan that still has subscribers — the API returns PLAN_HAS_ACTIVE_SUBSCRIPTIONS. Migrate them off it first (above), or retire it gracefully with the plan's archive option (strategy: "auto" | "explicit" | "block", with an optional transitionTo plan), then publish.

Related

  • Subscriptions & usage — the plan shape you're changing.
  • Billing strategies — the pricing patterns.
  • Connect Stripe — required before any publish.
6000
interval
"minute"
enforcement
"enforce"
unknown