Ship/Now

Guides · 2 min

Add a plan

A new tier, end to end: database row, config entry, and the entitlement that follows from it.

Prices live in the database so the pricing section and the checkout route read the same number. The config holds the mapping.

Add the row

In supabase/schema.sql, extend the seed at the bottom. on conflict do update keeps the file re-runnable, so changing a price is an edit and a db:push, not a manual query.

insert into public.plans (id, name, price_cents, currency, sort) values
  ('team', 'Team', 49900, 'eur', 3)
on conflict (id) do update set
  name = excluded.name,
  price_cents = excluded.price_cents,
  sort = excluded.sort;

Add it to the config

plans: [
  { id: "starter", name: "Starter", priceId: "price_..." },
  { id: "pro", name: "Pro", priceId: "price_..." },
  { id: "team", name: "Team", priceId: "price_..." },
]

PlanId widens on its own, so every function that takes a plan id now accepts "team" and still refuses typos.

Name it in both languages

Add the marketing copy to messages/en.json and messages/fr.json. The plan name in the database is the label on the receipt; the pitch belongs in the messages.

Decide what it unlocks

This is the part people forget. A plan that costs more has to do more, and that entitlement is checked server side, in the webhook and on every page that depends on it. Never in the browser.

Prices change

Never edit a price in a way that rewrites history. orders.amount_cents stores what was actually charged, so a past receipt keeps showing what the customer paid even after you double the price. If you find yourself computing an old order's total from the current plan row, stop: that is how a receipt becomes wrong.

Recurring instead of one-off

Switch the session to mode: "subscription" and pass a real Stripe price id rather than price_data. Then handle three more events:

EventDo
customer.subscription.createdWrite the row, set the period end
customer.subscription.updatedPlan change, pause, or a failed renewal
customer.subscription.deletedRevoke at the period end, not immediately

The subscriptions table is already in the schema for this.

Something wrong or missing on this page? Tell us.