/** * What Wellformed costs, and what each tier allows. * * Two flat tiers, unlimited people on both. No seats, no credits, no add-ons — * every extra dimension of a price list is a support question, a billing edge * case and a migration path forever. The limits below are the only two numbers * that ever change what someone pays. * * A plan belongs to a **workspace**, not to an account, because a workspace is * what a customer buys: one database, one bill, one thing to cancel. The same * person can be a guest in someone else's paid workspace and never see an * invoice for it. * * Shared so that the price on the marketing page and the limit the server * enforces cannot drift apart. */ export const PLAN_IDS = ['free', 'team'] as const; export type PlanId = (typeof PLAN_IDS)[number]; export interface Plan { id: PlanId; name: string; /** Euros per month, excluding VAT. Zero for the free tier. */ priceEur: number; /** Rows across every table in the workspace; null means no limit. */ rowLimit: number | null; /** Tables in the workspace; null means no limit. */ tableLimit: number | null; /** One line, on the pricing card, saying who this is for. */ blurb: string; includes: string[]; } export const PLANS: Record = { free: { id: 'free', name: 'Free', priceEur: 0, rowLimit: 1_000, tableLimit: 5, blurb: 'Enough to move a real spreadsheet over and see whether it holds.', includes: [ 'Everything in the engine: links, formulas, rollups, views', 'Unlimited people, any mix of admins, editors and viewers', 'The full audit trail', 'Export the whole workspace as a SQLite file, whenever you like', ], }, team: { id: 'team', name: 'Team', priceEur: 20, rowLimit: 250_000, tableLimit: null, blurb: 'One flat price per workspace. Add everyone you work with.', includes: [ 'Everything in Free, without the ceiling', 'As many tables as the work needs', '250,000 rows per workspace', 'Still unlimited people — there is no per-seat price to plan around', ], }, }; export const DEFAULT_PLAN: PlanId = 'free'; export function isPlanId(value: string): value is PlanId { return (PLAN_IDS as readonly string[]).includes(value); } /** The named plan, falling back to free — an unknown plan must never mean "unlimited". */ export function planOf(id: string | null | undefined): Plan { return id && isPlanId(id) ? PLANS[id] : PLANS[DEFAULT_PLAN]; } /** How a limit reads in a sentence: "1,000 rows", "unlimited tables". */ export function describeLimit(limit: number | null, noun: string): string { return limit === null ? `unlimited ${noun}` : `${limit.toLocaleString('en-US')} ${noun}`; }