Skip to content
EntryWick documentation

The plan catalog in Stripe — operators

Who runs it: whoever deploys, and whoever changes a price. php artisan ew:billing-sync.

What they came to do: make Stripe's Products and Prices match what PlanCatalog says EntryWick sells, so a checkout has something to charge.

True when they leave: every plan that is sold for money has a Stripe Product and a Price per billing mode, their ids are on plans.stripe, and running the command again would change nothing.

What it does

For each plan in plans, in the Stripe mode the configured STRIPE_SECRET belongs to:

Plan Product Prices
Free — (never charged)
Starter, Growth, Scale starter, growth, scale {plan}_event (one-time), {plan}_annual (yearly)
Enterprise — (quoted, invoiced by hand — PRICING §10)

--dry-run prints what would change and sends nothing.

Decisions already made

  • Idempotent by construction, not by remembering. The Product's id is the plan code and each Price carries a lookup key, so a second run finds them instead of making a second set. Nothing depends on plans.stripe being right, which is why a restored database or a hand-edited row cannot produce duplicates.
  • A price change makes a new Price. Stripe amounts are immutable. The new Price takes the lookup key (transfer_lookup_key) and the old one is archived, not deleted: a tenant already subscribed keeps the price they bought, which is how PRICING §10's grandfathering works. New checkouts get the new one.
  • Test and live are separate catalogs. The mode is recorded on plans.stripe.livemode, and Plan::stripePriceId() returns null when it does not match the key the box now holds — so a mis-keyed box refuses to start a checkout instead of sending Stripe an id from the other mode and failing at the payer's browser.
  • No Stripe secret is not an error. The command says so and succeeds, so a deploy to a box without keys stays green; the purchase paths refuse on their own with the same message.
  • It never deletes anything at Stripe. Every correction is a create-and-archive, because money objects that something might still reference are not ours to remove.

Running it

  • Staging: the deploy runs it after PlatformSeeder, with the test-mode keys written from the STAGING_STRIPE_* repository secrets (docs/NEEDS_FROM_OWNER.md, week 16).
  • After changing a price in PlanCatalog (a pricing decision — the doc changes in the same PR): re-seed the plans (php artisan db:seed --class=Database\\Seeders\\PlatformSeeder) and run ew:billing-sync.
  • Locally: it needs a sk_test_ key in .env; without one it is a no-op.

Open questions

  • One currency (EW_BILLING_CURRENCY, USD). A non-US organization pays in dollars until someone asks otherwise — and the first ask means a price per currency, not a conversion.
  • Nothing prunes a Product for a plan that stops being sold; is_active = false on our side leaves Stripe's object in place. Deliberate for now (archiving a product people are subscribed to is its own decision), but unowned.