diff --git a/apps/www/components/Pricing/PricingAddons.tsx b/apps/www/components/Pricing/PricingAddons.tsx index 339c65fb027..e2052f4faf6 100644 --- a/apps/www/components/Pricing/PricingAddons.tsx +++ b/apps/www/components/Pricing/PricingAddons.tsx @@ -5,6 +5,7 @@ import { useTheme } from 'next-themes' import Image from 'next/image' import Link from 'next/link' import { FC } from 'react' +import { ADDONS, usd } from 'shared-data/pricing-catalog' import { Button, cn } from 'ui' import CostControlAnimation from './CostControlAnimation' @@ -26,7 +27,7 @@ const addons = [ name: 'Custom Domain', heroImg: 'custom-domain-on', icon: 'custom-domain-upgrade', - price: 'Flat fee $10/month', + price: `Flat fee ${usd(ADDONS.customDomain.priceMonthlyPerDomain)}/month`, description: 'Use your own domain for your Supabase project to present a branded experience to your users.', ctaText: 'Documentation', @@ -38,7 +39,7 @@ const addons = [ name: 'Point in Time Recovery', heroImg: 'pitr-on', icon: 'pitr-upgrade', - price: 'Starts from $100/month', + price: `Starts from ${usd(ADDONS.pitr.priceMonthlyPer7DaysRetention)}/month`, description: 'Roll back to any specific point in time, down to the second.', ctaText: 'Documentation', ctaLink: 'https://supabase.com/docs/guides/platform/backups', diff --git a/apps/www/components/Pricing/PricingComputeSection.tsx b/apps/www/components/Pricing/PricingComputeSection.tsx index 861dc6454bc..0faf6e058c5 100644 --- a/apps/www/components/Pricing/PricingComputeSection.tsx +++ b/apps/www/components/Pricing/PricingComputeSection.tsx @@ -5,6 +5,7 @@ import Link from 'next/link' import React, { useEffect, useRef, useState } from 'react' import { useWindowSize } from 'react-use' import { plans as allPlans } from 'shared-data/plans' +import { PLAN_BILLING, usd } from 'shared-data/pricing-catalog' import { Button, cn } from 'ui' import { ToggleGroup, ToggleGroupItem } from 'ui/src/components/shadcn/ui/toggle-group' @@ -98,7 +99,7 @@ const PricingComputeSection = () => { )}

- Paid plans include $10/mo in compute credits, enough to cover one Micro instance. + {`Paid plans include ${usd(PLAN_BILLING.pro.computeCreditsMonthly)}/mo in compute credits, enough to cover one Micro instance.`}

diff --git a/apps/www/components/Pricing/PricingDiskSection.tsx b/apps/www/components/Pricing/PricingDiskSection.tsx index a62f563fd33..a4b30841d61 100644 --- a/apps/www/components/Pricing/PricingDiskSection.tsx +++ b/apps/www/components/Pricing/PricingDiskSection.tsx @@ -1,28 +1,26 @@ -import { Button } from 'ui' -import Panel from '../Panel' -import Link from 'next/link' import { ArrowUpRight } from 'lucide-react' +import Link from 'next/link' +import { DISK_PRICING, qty, usd } from 'shared-data/pricing-catalog' +import { Button } from 'ui' -const diskTypes = [ - { - name: 'General Purpose', - tagline: 'Balance between price and performance', - maxSize: '16 TB', - size: '8 GB included\nthen $0.125 per GB', - iops: '3,000 IOPS included\nthen $0.024 per IOPS', - throughput: '125 MB/s included\nthen $0.095 per MB/s', - durability: '99.9%', - }, - { - name: 'High Performance', - tagline: 'For mission critical applications', - maxSize: '60 TB', - size: '$0.195 per GB', - iops: '$0.119 per IOPS', - throughput: 'Scales automatically with IOPS', - durability: '99.999%', - }, -] +import Panel from '../Panel' + +const diskTypes = Object.values(DISK_PRICING).map((disk) => ({ + name: disk.displayName, + tagline: disk.tagline, + maxSize: `${disk.maxSizeTb} TB`, + size: disk.includedPerProject + ? `${disk.includedPerProject.sizeGb} GB included\nthen ${usd(disk.perUnitMonth.sizeGb)} per GB` + : `${usd(disk.perUnitMonth.sizeGb)} per GB`, + iops: disk.includedPerProject + ? `${qty(disk.includedPerProject.iops, 'comma')} IOPS included\nthen ${usd(disk.perUnitMonth.iops)} per IOPS` + : `${usd(disk.perUnitMonth.iops)} per IOPS`, + throughput: + disk.includedPerProject && disk.perUnitMonth.throughputMBps !== null + ? `${disk.includedPerProject.throughputMBps} MB/s included\nthen ${usd(disk.perUnitMonth.throughputMBps)} per MB/s` + : (disk.throughputNote ?? ''), + durability: `${disk.durabilityPercent}%`, +})) const PricingDiskSection = () => (
diff --git a/packages/shared-data/plans.ts b/packages/shared-data/plans.ts index 76ef6e1239f..67b6a3d8806 100644 --- a/packages/shared-data/plans.ts +++ b/packages/shared-data/plans.ts @@ -1,3 +1,5 @@ +import { ADDONS, gbDisplay, METERS, PLAN_BILLING, qty, usd, withUnit } from './pricing-catalog' + export type PlanId = 'free' | 'pro' | 'team' | 'enterprise' export interface PricingInformation { @@ -27,16 +29,19 @@ export const plans: PricingInformation[] = [ costUnit: '/ month', href: 'https://supabase.com/dashboard/new?plan=free', priceLabel: '', - priceMonthly: 0, + priceMonthly: PLAN_BILLING.free.priceMonthly, description: 'Perfect for passion projects & simple websites.', preface: 'Get started with:', features: [ 'Unlimited API requests', - '50,000 monthly active users', - ['500 MB database size', 'Shared CPU • 500 MB RAM'], - ['5 GB egress'], - ['5 GB cached egress'], - '1 GB file storage', + `${qty(METERS.maus.includedByPlan.free, 'comma')} monthly active users`, + [ + `${gbDisplay(METERS.databaseDiskSize.includedByPlan.free)} database size`, + 'Shared CPU • 500 MB RAM', + ], + [`${qty(METERS.egress.includedByPlan.free)} GB egress`], + [`${qty(METERS.cachedEgress.includedByPlan.free)} GB cached egress`], + `${qty(METERS.storageSize.includedByPlan.free)} GB file storage`, 'Community support', ], footer: 'Free projects are paused after 1 week of inactivity. Limit of 2 active projects.', @@ -51,18 +56,36 @@ export const plans: PricingInformation[] = [ href: 'https://supabase.com/dashboard/new?plan=pro', priceLabel: 'From', warning: 'Includes one project running on Micro compute.', - priceMonthly: 25, + priceMonthly: PLAN_BILLING.pro.priceMonthly, description: 'For production applications with the power to scale.', features: [ - ['100,000 monthly active users', 'then $0.00325 per MAU'], - ['8 GB disk size per project', 'then $0.125 per GB'], - ['250 GB egress', 'then $0.09 per GB'], - ['250 GB cached egress', 'then $0.03 per GB'], - ['100 GB file storage', 'then $0.0213 per GB'], + [ + `${qty(METERS.maus.includedByPlan.pro, 'comma')} monthly active users`, + `then ${usd(METERS.maus.price)} per MAU`, + ], + [ + `${gbDisplay(METERS.databaseDiskSize.includedByPlan.pro)} disk size per project`, + `then ${usd(METERS.databaseDiskSize.price)} per GB`, + ], + [ + `${withUnit(METERS.egress.includedByPlan.pro, 'GB')} egress`, + `then ${usd(METERS.egress.price)} per GB`, + ], + [ + `${withUnit(METERS.cachedEgress.includedByPlan.pro, 'GB')} cached egress`, + `then ${usd(METERS.cachedEgress.price)} per GB`, + ], + [ + `${withUnit(METERS.storageSize.includedByPlan.pro, 'GB')} file storage`, + `then ${usd(METERS.storageSize.price)} per GB`, + ], 'Email support', 'Daily backups stored for 7 days', '7-day log retention', - ['Add Log Drains', 'additional $60 per drain, per project'], + [ + 'Add Log Drains', + `additional ${usd(ADDONS.logDrain.priceMonthlyPerDrain)} per drain, per project`, + ], ], preface: 'Everything in the Free Plan, plus:', cta: 'Get Started', @@ -76,7 +99,7 @@ export const plans: PricingInformation[] = [ href: 'https://supabase.com/dashboard/new?plan=team', priceLabel: 'From', warning: 'Includes one project running on Micro compute.', - priceMonthly: 599, + priceMonthly: PLAN_BILLING.team.priceMonthly, description: 'Add features such as SSO, control over backups, and industry certifications.', features: [ 'SOC2 & ISO 27001', diff --git a/packages/shared-data/pricing-catalog.ts b/packages/shared-data/pricing-catalog.ts index 230268bf6af..7b35c5e76d1 100644 --- a/packages/shared-data/pricing-catalog.ts +++ b/packages/shared-data/pricing-catalog.ts @@ -53,18 +53,24 @@ export function qty(n: number, style?: 'comma' | 'millionWord'): string { return String(n) } +const THIN_SPACE = ' ' + +export function withUnit(n: number, unit: string, style?: 'comma' | 'millionWord'): string { + return `${qty(n, style)}${THIN_SPACE}${unit}` +} + export function gbDisplay(sizeGb: number): string { - return sizeGb < 1 ? `${sizeGb * 1000} MB` : `${sizeGb} GB` + return sizeGb < 1 ? withUnit(sizeGb * 1000, 'MB') : withUnit(sizeGb, 'GB') } const GP3_PER_GB_MONTH = 0.125 -export const PLAN_BILLING: Record = { +export const PLAN_BILLING = { free: { priceMonthly: 0, computeCreditsMonthly: null, spendCapAvailable: false }, pro: { priceMonthly: 25, computeCreditsMonthly: 10, spendCapAvailable: true }, team: { priceMonthly: 599, computeCreditsMonthly: 10, spendCapAvailable: false }, enterprise: { priceMonthly: null, computeCreditsMonthly: null, spendCapAvailable: false }, -} +} as const satisfies Record export const DISK_PRICING: Record<'gp3' | 'io2', DiskTypeEntry> = { gp3: { diff --git a/packages/shared-data/pricing.ts b/packages/shared-data/pricing.ts index 30dc6695f9c..3e9eb896d0e 100644 --- a/packages/shared-data/pricing.ts +++ b/packages/shared-data/pricing.ts @@ -1,3 +1,5 @@ +import { ADDONS, gbDisplay, METERS, qty, usd, withUnit } from './pricing-catalog' + type Pricing = { database: PricingCategory auth: PricingCategory @@ -122,9 +124,15 @@ export const pricing: Pricing = { key: 'database.size', title: 'Database size', plans: { - free: '500 MB database size per project included', - pro: ['8 GB disk size per project included', 'then $0.125 per GB'], - team: ['8 GB disk size per project included', 'then $0.125 per GB'], + free: `${gbDisplay(METERS.databaseDiskSize.includedByPlan.free)} database size per project included`, + pro: [ + `${gbDisplay(METERS.databaseDiskSize.includedByPlan.pro)} disk size per project included`, + `then ${usd(METERS.databaseDiskSize.price)} per GB`, + ], + team: [ + `${gbDisplay(METERS.databaseDiskSize.includedByPlan.team)} disk size per project included`, + `then ${usd(METERS.databaseDiskSize.price)} per GB`, + ], enterprise: 'Custom', }, usage_based: true, @@ -156,9 +164,9 @@ export const pricing: Pricing = { title: 'Point in time recovery', plans: { free: false, - pro: '$100 per month per 7 days retention', - team: '$100 per month per 7 days retention', - enterprise: '$100 per month per 7 days retention, >28 days retention available', + pro: `${usd(ADDONS.pitr.priceMonthlyPer7DaysRetention)} per month per 7 days retention`, + team: `${usd(ADDONS.pitr.priceMonthlyPer7DaysRetention)} per month per 7 days retention`, + enterprise: `${usd(ADDONS.pitr.priceMonthlyPer7DaysRetention)} per month per 7 days retention, >28 days retention available`, }, usage_based: false, }, @@ -178,8 +186,8 @@ export const pricing: Pricing = { title: 'Branching', plans: { free: false, - pro: '$0.01344 per branch, per hour', - team: '$0.01344 per branch, per hour', + pro: `${usd(METERS.branching.price)} per branch, per hour`, + team: `${usd(METERS.branching.price)} per branch, per hour`, enterprise: 'Custom', }, usage_based: true, @@ -188,9 +196,15 @@ export const pricing: Pricing = { key: 'database.egress', title: 'Egress', plans: { - free: '5 GB included', - pro: ['250 GB included', 'then $0.09 per GB'], - team: ['250 GB included', 'then $0.09 per GB'], + free: `${withUnit(METERS.egress.includedByPlan.free, 'GB')} included`, + pro: [ + `${withUnit(METERS.egress.includedByPlan.pro, 'GB')} included`, + `then ${usd(METERS.egress.price)} per GB`, + ], + team: [ + `${withUnit(METERS.egress.includedByPlan.team, 'GB')} included`, + `then ${usd(METERS.egress.price)} per GB`, + ], enterprise: 'Custom', }, usage_based: true, @@ -201,14 +215,14 @@ export const pricing: Pricing = { plans: { free: false, pro: [ - '$0.053 per pipeline per hour', - '$3.00 per GB processed during ongoing replication', - '$0.60 per GB processed during initial sync', + `${usd(METERS.replicationPipelineHours.price)} per pipeline per hour`, + `${usd(METERS.replicationOngoingGb.price, 2)} per GB processed during ongoing replication`, + `${usd(METERS.replicationInitialSyncGb.price, 2)} per GB processed during initial sync`, ], team: [ - '$0.053 per pipeline per hour', - '$3.00 per GB processed during ongoing replication', - '$0.60 per GB processed during initial sync', + `${usd(METERS.replicationPipelineHours.price)} per pipeline per hour`, + `${usd(METERS.replicationOngoingGb.price, 2)} per GB processed during ongoing replication`, + `${usd(METERS.replicationInitialSyncGb.price, 2)} per GB processed during initial sync`, ], enterprise: 'Custom', }, @@ -235,9 +249,15 @@ export const pricing: Pricing = { key: 'auth.maus', title: 'MAUs', plans: { - free: '50,000 included', - pro: ['100,000 included', 'then $0.00325 per MAU'], - team: ['100,000 included', 'then $0.00325 per MAU'], + free: `${qty(METERS.maus.includedByPlan.free, 'comma')} included`, + pro: [ + `${qty(METERS.maus.includedByPlan.pro, 'comma')} included`, + `then ${usd(METERS.maus.price)} per MAU`, + ], + team: [ + `${qty(METERS.maus.includedByPlan.team, 'comma')} included`, + `then ${usd(METERS.maus.price)} per MAU`, + ], enterprise: 'Custom', }, usage_based: true, @@ -325,8 +345,14 @@ export const pricing: Pricing = { title: 'Advanced Multi-Factor Auth - Phone', plans: { free: false, - pro: ['$75 per month for first project', 'then $10 per month per additional projects'], - team: ['$75 per month for first project', 'then $10 per month per additional projects'], + pro: [ + `${usd(ADDONS.advancedMfaPhone.priceMonthlyFirstProject)} per month for first project`, + `then ${usd(ADDONS.advancedMfaPhone.priceMonthlyAdditionalProject)} per month per additional projects`, + ], + team: [ + `${usd(ADDONS.advancedMfaPhone.priceMonthlyFirstProject)} per month for first project`, + `then ${usd(ADDONS.advancedMfaPhone.priceMonthlyAdditionalProject)} per month per additional projects`, + ], enterprise: 'Custom', }, usage_based: false, @@ -335,9 +361,15 @@ export const pricing: Pricing = { key: 'auth.thirdPartyMAUs', title: 'Third-Party MAUs', plans: { - free: '50,000 included', - pro: ['100,000 included', 'then $0.00325 per MAU'], - team: ['100,000 included', 'then $0.00325 per MAU'], + free: `${qty(METERS.thirdPartyMaus.includedByPlan.free, 'comma')} included`, + pro: [ + `${qty(METERS.thirdPartyMaus.includedByPlan.pro, 'comma')} included`, + `then ${usd(METERS.thirdPartyMaus.price)} per MAU`, + ], + team: [ + `${qty(METERS.thirdPartyMaus.includedByPlan.team, 'comma')} included`, + `then ${usd(METERS.thirdPartyMaus.price)} per MAU`, + ], enterprise: 'Custom', }, usage_based: true, @@ -347,8 +379,14 @@ export const pricing: Pricing = { title: 'Single Sign-On (SAML 2.0)', plans: { free: false, - pro: ['50 included', 'then $0.015 per MAU'], - team: ['50 included', 'then $0.015 per MAU'], + pro: [ + `${qty(METERS.ssoMaus.includedByPlan.pro)} included`, + `then ${usd(METERS.ssoMaus.price)} per MAU`, + ], + team: [ + `${qty(METERS.ssoMaus.includedByPlan.team)} included`, + `then ${usd(METERS.ssoMaus.price)} per MAU`, + ], enterprise: 'Contact Us', }, @@ -420,9 +458,15 @@ export const pricing: Pricing = { key: 'storage.size', title: 'Storage', plans: { - free: '1 GB included', - pro: ['100 GB included', 'then $0.0213 per GB'], - team: ['100 GB included', 'then $0.0213 per GB'], + free: `${withUnit(METERS.storageSize.includedByPlan.free, 'GB')} included`, + pro: [ + `${withUnit(METERS.storageSize.includedByPlan.pro, 'GB')} included`, + `then ${usd(METERS.storageSize.price)} per GB`, + ], + team: [ + `${withUnit(METERS.storageSize.includedByPlan.team, 'GB')} included`, + `then ${usd(METERS.storageSize.price)} per GB`, + ], enterprise: 'Custom', }, usage_based: true, @@ -431,9 +475,15 @@ export const pricing: Pricing = { key: 'storage.cachedEgress', title: 'Cached Egress', plans: { - free: '5 GB included', - pro: ['250 GB included', 'then $0.03 per GB'], - team: ['250 GB included', 'then $0.03 per GB'], + free: `${withUnit(METERS.cachedEgress.includedByPlan.free, 'GB')} included`, + pro: [ + `${withUnit(METERS.cachedEgress.includedByPlan.pro, 'GB')} included`, + `then ${usd(METERS.cachedEgress.price)} per GB`, + ], + team: [ + `${withUnit(METERS.cachedEgress.includedByPlan.team, 'GB')} included`, + `then ${usd(METERS.cachedEgress.price)} per GB`, + ], enterprise: 'Custom', }, usage_based: true, @@ -476,8 +526,14 @@ export const pricing: Pricing = { title: 'Image Transformations', plans: { free: false, - pro: ['100 origin images included', 'then $5 per 1000 origin images'], - team: ['100 origin images included', 'then $5 per 1000 origin images'], + pro: [ + `${qty(METERS.imageTransformations.includedByPlan.pro)} origin images included`, + `then ${usd(METERS.imageTransformations.price)} per ${qty(METERS.imageTransformations.per)} origin images`, + ], + team: [ + `${qty(METERS.imageTransformations.includedByPlan.team)} origin images included`, + `then ${usd(METERS.imageTransformations.price)} per ${qty(METERS.imageTransformations.per)} origin images`, + ], enterprise: 'Custom', }, usage_based: true, @@ -492,9 +548,15 @@ export const pricing: Pricing = { key: 'functions.invocations', title: 'Invocations', plans: { - free: '500,000 included', - pro: ['2 Million included', 'then $2 per 1 Million'], - team: ['2 Million included', 'then $2 per 1 Million'], + free: `${qty(METERS.functionsInvocations.includedByPlan.free, 'comma')} included`, + pro: [ + `${qty(METERS.functionsInvocations.includedByPlan.pro, 'millionWord')} included`, + `then ${usd(METERS.functionsInvocations.price)} per ${qty(METERS.functionsInvocations.per, 'millionWord')}`, + ], + team: [ + `${qty(METERS.functionsInvocations.includedByPlan.team, 'millionWord')} included`, + `then ${usd(METERS.functionsInvocations.price)} per ${qty(METERS.functionsInvocations.per, 'millionWord')}`, + ], enterprise: 'Custom', }, usage_based: true, @@ -520,9 +582,15 @@ export const pricing: Pricing = { key: 'realtime.concurrentConnections', title: 'Concurrent Peak Connections', plans: { - free: '200 included', - pro: ['500 included', 'then $10 per 1000'], - team: ['500 included', 'then $10 per 1000'], + free: `${qty(METERS.realtimeConnections.includedByPlan.free)} included`, + pro: [ + `${qty(METERS.realtimeConnections.includedByPlan.pro)} included`, + `then ${usd(METERS.realtimeConnections.price)} per ${qty(METERS.realtimeConnections.per)}`, + ], + team: [ + `${qty(METERS.realtimeConnections.includedByPlan.team)} included`, + `then ${usd(METERS.realtimeConnections.price)} per ${qty(METERS.realtimeConnections.per)}`, + ], enterprise: 'Custom concurrent connections and volume discount', }, usage_based: true, @@ -531,9 +599,15 @@ export const pricing: Pricing = { key: 'realtime.messagesPerMonth', title: 'Messages Per Month', plans: { - free: '2 Million included', - pro: ['5 Million included', 'then $2.50 per Million'], - team: ['5 Million included', 'then $2.50 per Million'], + free: `${qty(METERS.realtimeMessages.includedByPlan.free, 'millionWord')} included`, + pro: [ + `${qty(METERS.realtimeMessages.includedByPlan.pro, 'millionWord')} included`, + `then ${usd(METERS.realtimeMessages.price, 2)} per Million`, + ], + team: [ + `${qty(METERS.realtimeMessages.includedByPlan.team, 'millionWord')} included`, + `then ${usd(METERS.realtimeMessages.price, 2)} per Million`, + ], enterprise: 'Volume discounts on messages', }, usage_based: true, @@ -599,8 +673,16 @@ export const pricing: Pricing = { title: 'Log Drain', plans: { free: false, - pro: ['$60 per drain per month', '+ $0.20 per million events', '+ $0.09 per GB egress'], - team: ['$60 per drain per month', '+ $0.20 per million events', '+ $0.09 per GB egress'], + pro: [ + `${usd(ADDONS.logDrain.priceMonthlyPerDrain)} per drain per month`, + `+ ${usd(ADDONS.logDrain.perMillionEvents, 2)} per million events`, + `+ ${usd(ADDONS.logDrain.perGbEgress)} per GB egress`, + ], + team: [ + `${usd(ADDONS.logDrain.priceMonthlyPerDrain)} per drain per month`, + `+ ${usd(ADDONS.logDrain.perMillionEvents, 2)} per million events`, + `+ ${usd(ADDONS.logDrain.perGbEgress)} per GB egress`, + ], enterprise: 'Custom', }, usage_based: true, @@ -720,9 +802,9 @@ export const pricing: Pricing = { title: 'Custom Domains', plans: { free: false, - pro: '$10 per domain per month per project add on', - team: '$10 per domain per month per project add on', - enterprise: '1, additional $10/domain/month', + pro: `${usd(ADDONS.customDomain.priceMonthlyPerDomain)} per domain per month per project add on`, + team: `${usd(ADDONS.customDomain.priceMonthlyPerDomain)} per domain per month per project add on`, + enterprise: `1, additional ${usd(ADDONS.customDomain.priceMonthlyPerDomain)}/domain/month`, }, usage_based: false, },