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:
| Event | Do |
|---|---|
customer.subscription.created | Write the row, set the period end |
customer.subscription.updated | Plan change, pause, or a failed renewal |
customer.subscription.deleted | Revoke 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.