Skip to content

Polar sandbox to production, the checklist that stops launch-day silence

Sandbox and production are separate Polar deployments with separate tokens, product ids and webhook secrets. Nothing carries over. Everything that has to be recreated, in order, and how to prove it worked.

Polar4 min readships at docs/solutions/polar/sandbox-to-production-checklist.md

Tags: polar · sandbox · production · launch · environments · webhooks · checklist

The failure looks like this. You ship on a Friday. Checkout loads, cards go through, money appears in the Polar dashboard, and nobody's account unlocks. No error in the logs, nothing red anywhere.

The cause is almost always one value in production that still points at sandbox, or one thing that exists in sandbox and was never created in production. Polar keeps the two apart: a different API host (sandbox-api.polar.sh against api.polar.sh), dashboard, organization, tokens, product ids, webhook endpoints and signing secrets. Production is a second setup, not a switch.

Before launch day

Start merchant onboarding early. Polar is the seller, so it needs your business details, and a person reviews them. Start days before you need it. Until it completes, your production organization cannot take money.

Know what you sell. In this repo the answer is one file: src/lib/pricing.ts. Every plan and price is there, so there is no sandbox catalogue to copy by hand.

The checklist

  1. Create the production organization at https://polar.sh and finish onboarding.

  2. Create a production token. Settings, Developers, New Token. Sandbox tokens do not work on the production host, and the reverse.

  3. Create the products from the catalogue. With the production token and POLAR_ENVIRONMENT=production loaded:

    bun run billing:sync-plans
    

    It creates one product per catalogue price (Polar sells one price per product) with price_id metadata, and prints the five BILLING_PRICE_* lines. The ids are new UUIDs. That is fine: no id is written in code.

  4. Create the webhook endpoint. Settings, Webhooks, Add endpoint:

    • URL https://<your-domain>/api/webhooks/polar
    • Format Raw. Discord and Slack formats sign a chat message, and the handler answers 500 with a note to switch.
    • Events: order.paid, order.refunded, subscription.created, subscription.updated, subscription.active, subscription.canceled, subscription.uncanceled, subscription.revoked, subscription.past_due.

    Copy the signing secret. Endpoints made since 8 Sep 2026 sign with Standard Webhooks; src/lib/billing/polar-webhooks.ts accepts both that and the older format, so paste it as shown.

  5. Set the environment in your host, for the production environment only:

    POLAR_ACCESS_TOKEN=<production token>
    POLAR_WEBHOOK_SECRET=<production endpoint secret>
    POLAR_ORGANIZATION_ID=<production organization id>
    POLAR_ENVIRONMENT=production
    BILLING_PRICE_PRO_MONTHLY=...   (and the other four)
    

    POLAR_ENVIRONMENT picks the API host. A production token with it left at sandbox fails to authenticate; a sandbox token with production does the same the other way. Both are quiet until someone clicks Buy.

  6. Deploy, with migrations run. The billing tables must exist before the first webhook lands, or the handler answers 500 and Polar retries into a table that is not there.

  7. Run bun run verify with the production values. The Polar check asserts the token belongs to that environment and organization, and that every BILLING_PRICE_* points at a live product with the catalogue's amount, currency and interval.

  8. Buy your own product with a real card. The smallest price you sell. Watch the chain: the order in the dashboard, 200 in the endpoint's delivery log, the row in billing_subscriptions or billing_purchases, and /billing showing the plan. Refund yourself and watch the plan go away.

That last step is the only one that proves the other seven.

Easy to miss

  • Preview deployments often inherit production variables. A preview with a production token can take real money against a branch. Scope production values to production and give previews the sandbox set.
  • The webhook route is public by design. The signature protects it. Do not put it behind deployment protection or a password: Polar cannot log in, and the deliveries fail with a 401 that looks nothing like a signature problem.
  • The delivery log answers most launch-day questions. Settings, Webhooks, your endpoint: every attempt, its status and its body, with a redeliver button. A redelivery of a handled event answers duplicate, which is fine.
  • Ten failures in a row disable the endpoint. A wrong secret on launch day can switch it off before you notice. Re-enable it after the fix and run bun run billing:reconcile to pick up what it missed.
  • Schedule the nightly jobs (billing:reconcile, billing:prune-events) in production too. Polar sends no dispute webhook; reconcile is what revokes a charged-back lifetime deal.