Spreadsheet-UX op een echte relationele engine
Deze repository wordt als momentopname gepubliceerd: een clone bevat de volledige inhoud, maar geen commitgeschiedenis.
/**
* 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<PlanId, Plan> = {
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}`;
}