mirror of
https://github.com/supabase/supabase.git
synced 2026-10-05 09:25:06 +03:00
feat: add team plan
This commit is contained in:
1 parent
121c28b111
commit
e3df2f453d
30 files changed
+744
-732
No files matched your search
@@ -88,9 +88,9 @@ export default function ComputePricingModal({ showComputeModal, setShowComputeMo
|
||||
<td key={column.key} className="p-3">
|
||||
{column.key === 'dedicated' ? (
|
||||
column.value ? (
|
||||
<IconPricingIncludedCheck tier="Pro plan" />
|
||||
<IconPricingIncludedCheck plan="Pro plan" />
|
||||
) : (
|
||||
<IconPricingMinus tier="Free plan" />
|
||||
<IconPricingMinus plan="Free plan" />
|
||||
)
|
||||
) : (
|
||||
column.value
|
||||
@@ -124,9 +124,9 @@ export default function ComputePricingModal({ showComputeModal, setShowComputeMo
|
||||
>
|
||||
{column.key === 'dedicated' ? (
|
||||
column.value ? (
|
||||
<IconPricingIncludedCheck tier="Pro plan" />
|
||||
<IconPricingIncludedCheck plan="Pro plan" />
|
||||
) : (
|
||||
<IconPricingMinus tier="Free plan" />
|
||||
<IconPricingMinus plan="Free plan" />
|
||||
)
|
||||
) : (
|
||||
column.value
|
||||
|
||||
@@ -12,7 +12,7 @@ export const Check = () => (
|
||||
export const IconPricingIncludedCheck = (props: any) => (
|
||||
<span className="mx-auto">
|
||||
<Check />
|
||||
<span className="sr-only">Included in {props.tier}</span>
|
||||
<span className="sr-only">Included in {props.plan}</span>
|
||||
</span>
|
||||
)
|
||||
|
||||
@@ -32,7 +32,7 @@ export const IconPricingMinus = (props: any) => (
|
||||
clipRule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
<span className="sr-only">Not included in {props.tier}</span>
|
||||
<span className="sr-only">Not included in {props.plan}</span>
|
||||
</>
|
||||
)
|
||||
|
||||
|
||||
@@ -48,35 +48,35 @@ export const PricingTableRowDesktop = (props: any) => {
|
||||
)}
|
||||
</th>
|
||||
|
||||
{Object.entries(feat.tiers).map((entry: any, i) => {
|
||||
const tierName = entry[0]
|
||||
const tierValue = entry[1]
|
||||
{Object.entries(feat.plans).map((entry: any, i) => {
|
||||
const planName = entry[0]
|
||||
const planValue = entry[1]
|
||||
|
||||
return (
|
||||
<td
|
||||
key={i}
|
||||
className={[
|
||||
`px-6 tier-${tierName}`,
|
||||
typeof tierValue === 'boolean' ? 'text-center' : '',
|
||||
`px-6 tier-${planName}`,
|
||||
typeof planValue === 'boolean' ? 'text-center' : '',
|
||||
].join(' ')}
|
||||
>
|
||||
{typeof tierValue === 'boolean' && tierValue === true ? (
|
||||
<IconPricingIncludedCheck tier={tierValue} />
|
||||
) : typeof tierValue === 'boolean' && tierValue === false ? (
|
||||
{typeof planValue === 'boolean' && planValue === true ? (
|
||||
<IconPricingIncludedCheck plan={planValue} />
|
||||
) : typeof planValue === 'boolean' && planValue === false ? (
|
||||
<div className="text-scale-900">
|
||||
<IconPricingMinus tier={tierValue} />
|
||||
<IconPricingMinus plan={planValue} />
|
||||
</div>
|
||||
) : (
|
||||
<span className="text-scale-1200 text-xs dark:text-white flex items-center gap-3">
|
||||
{feat.tooltips?.[tierName] && (
|
||||
{feat.tooltips?.[planName] && (
|
||||
<span
|
||||
className="shrink-0 hover:text-scale-300 cursor-pointer transition-colors"
|
||||
data-tip={feat.tooltips[tierName]}
|
||||
data-tip={feat.tooltips[planName]}
|
||||
>
|
||||
<IconPricingInfo />
|
||||
</span>
|
||||
)}
|
||||
{tierValue}
|
||||
{planValue}
|
||||
</span>
|
||||
)}
|
||||
</td>
|
||||
@@ -96,7 +96,7 @@ export const PricingTableRowDesktop = (props: any) => {
|
||||
|
||||
export const PricingTableRowMobile = (props: any) => {
|
||||
const category = props.category
|
||||
const tier = props.tier
|
||||
const plan = props.plan
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -127,16 +127,16 @@ export const PricingTableRowMobile = (props: any) => {
|
||||
</span>
|
||||
</th>
|
||||
<td className="py-3 pr-4 text-right">
|
||||
{typeof feat.tiers[tier] === 'boolean' && feat.tiers[tier] === true ? (
|
||||
{typeof feat.plans[plan] === 'boolean' && feat.plans[plan] === true ? (
|
||||
<div className="inline-block">
|
||||
<IconPricingIncludedCheck tier={tier} />
|
||||
<IconPricingIncludedCheck plan={plan} />
|
||||
</div>
|
||||
) : typeof feat.tiers[tier] === 'boolean' && feat.tiers[tier] === false ? (
|
||||
) : typeof feat.plans[plan] === 'boolean' && feat.plans[plan] === false ? (
|
||||
<div className="inline-block">
|
||||
<IconPricingMinus tier={tier} />
|
||||
<IconPricingMinus plan={plan} />
|
||||
</div>
|
||||
) : (
|
||||
<span className="text-scale-1200 block text-sm">{feat.tiers[tier]}</span>
|
||||
<span className="text-scale-1200 block text-sm">{feat.plans[plan]}</span>
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
@@ -90,6 +90,6 @@ const nextConfig = {
|
||||
|
||||
// next.config.js.
|
||||
export default () => {
|
||||
const plugins = [withMDX, withBundleAnalyzer, withTM(['ui', 'common'])]
|
||||
const plugins = [withMDX, withBundleAnalyzer, withTM(['ui', 'common', 'shared-data'])]
|
||||
return plugins.reduce((acc, next) => next(acc), nextConfig)
|
||||
}
|
||||
@@ -55,6 +55,7 @@
|
||||
"remark": "^13.0.0",
|
||||
"remark-gfm": "^3.0.1",
|
||||
"remark-html": "^15.0.1",
|
||||
"shared-data": "*",
|
||||
"swiper": "^6.5.0",
|
||||
"swr": "^0.5.4",
|
||||
"typescript": "^4.0.3",
|
||||
|
||||
@@ -1,99 +0,0 @@
|
||||
Pricing Changelog
|
||||
|
||||
### 18 Jan 2023
|
||||
|
||||
- Clarified over-usage of Realtime messages per month
|
||||
- On the Pro plan, $2.50 per million after included 5 million messages
|
||||
|
||||
### 09 Jan 2023
|
||||
----
|
||||
|
||||
Database Compute Add ons
|
||||
- Clarified iops values to the latest instance types
|
||||
|
||||
### 02 Nov 2022
|
||||
----
|
||||
|
||||
Database
|
||||
- guidance on backups > 1TB in size
|
||||
- Removed "Realtime functionality" as it is redundant with new realtime section
|
||||
Auth
|
||||
- added “advanced security features”
|
||||
- MAUs changed to unlimited for Enterprise
|
||||
Realtime
|
||||
— Added new realtime section
|
||||
- Concurrent Peak Connections
|
||||
- Max Messages Per Month
|
||||
- Max Message Size
|
||||
Edge Functions
|
||||
- Runtime hours removed
|
||||
- Number of functions, usage pricing added after limit on Pro
|
||||
- Invocations, usage pricing added after limit on Pro
|
||||
Support
|
||||
- Security features moved to own section
|
||||
Platform Security and Compliance
|
||||
- New section made
|
||||
- added Access roles
|
||||
- added Custom Domains
|
||||
- added Vanity URLs added
|
||||
Database Compute Add ons
|
||||
- New label “Optimized Compute”
|
||||
|
||||
|
||||
### 16 June 2022
|
||||
----
|
||||
|
||||
Auth
|
||||
- New item “MAUs (Monthly Active Users)”
|
||||
Security & Compliance
|
||||
- New item: “Security questionnaire help” for enterprise
|
||||
- New item - “Metrics Endpoint” for enterprise and pro plan users
|
||||
Database Compute Add ons
|
||||
- New item: “4XL”
|
||||
- New item: “8XL”
|
||||
- New item: “12XL”
|
||||
- New item: “16XL”
|
||||
Database
|
||||
- New item: “Point in time recovery” for enterprise
|
||||
Clarifications
|
||||
- General - Clearer language that we charge on a per project per month
|
||||
- Storage - Uploads for Pro, changed from 3GB → 5GB in title card
|
||||
- Storage - file upload size - clarified that it can be changed in dashboard
|
||||
- All products - “Transfer” renamed to “Egress”
|
||||
- Database compute add-ons - Number of connections more accurate
|
||||
- Storage - Free plan egress per month clarified
|
||||
|
||||
### 31 March 2022
|
||||
----
|
||||
|
||||
Edge Functions pricing announced
|
||||
|
||||
|
||||
### 29 March 2022
|
||||
----
|
||||
|
||||
— Enterprise pricing plan announced
|
||||
|
||||
— Pay as you go plan now the same as the Pro plan
|
||||
|
||||
|
||||
27 August 2021
|
||||
----
|
||||
|
||||
- Changes "Dedicated support" to "Designated support".
|
||||
- Dedicated = one individual solely focused on an account.
|
||||
- Designated = individual who may be helping across various accounts, but still a single point of contact. May be dedicated or non-dedicated.
|
||||
|
||||
|
||||
28 July 2021
|
||||
----
|
||||
|
||||
- Clarified pricing on the "Optimized database instances" to include the starting price of $50/month.
|
||||
|
||||
|
||||
### 29 March 2021
|
||||
----
|
||||
|
||||
Released our Beta pricing.
|
||||
|
||||
|
||||
+231
-206
@@ -8,21 +8,25 @@ import ReactMarkdown from 'react-markdown'
|
||||
import CTABanner from '~/components/CTABanner'
|
||||
import DefaultLayout from '~/components/Layouts/Default'
|
||||
import { PricingTableRowDesktop, PricingTableRowMobile } from '~/components/Pricing/PricingTableRow'
|
||||
import pricing from '~/data/Pricing.json'
|
||||
import pricing from 'shared-data/Pricing.json'
|
||||
import pricingFaq from '~/data/PricingFAQ.json'
|
||||
import { useTheme } from 'common/Providers'
|
||||
import ComputePricingModal from '~/components/Pricing/ComputePricingModal'
|
||||
import { plans } from 'shared-data/plans'
|
||||
|
||||
export default function IndexPage() {
|
||||
const router = useRouter()
|
||||
const { basePath, asPath } = useRouter()
|
||||
const { isDarkMode } = useTheme()
|
||||
const [showComputeModal, setShowComputeModal] = useState(false)
|
||||
const [activeMobileTier, setActiveMobileTier] = useState('Free')
|
||||
const [activeMobilePlan, setActiveMobilePlan] = useState('Free')
|
||||
|
||||
const meta_title = 'Pricing & fees | Supabase'
|
||||
const meta_description =
|
||||
'Explore Supabase fees and pricing information. Find our competitive pricing tiers, with no hidden pricing. We have a generous free plan for those getting started, and Pay As You Go for those scaling up.'
|
||||
'Explore Supabase fees and pricing information. Find our competitive pricing plans, with no hidden pricing. We have a generous free plan for those getting started, and Pay As You Go for those scaling up.'
|
||||
|
||||
const plansExceptEnterprise = plans.filter((it) => it.name !== 'Enterprise')
|
||||
const planEnterprise = plans.find((it) => it.name === 'Enterprise')!
|
||||
|
||||
// Ability to scroll into pricing sections like storage
|
||||
useEffect(() => {
|
||||
@@ -45,93 +49,6 @@ export default function IndexPage() {
|
||||
}
|
||||
}, [asPath])
|
||||
|
||||
/**
|
||||
* @mildtomato same plan metadata is also in /studio dashboard
|
||||
* would be good if this constant was shared across apps
|
||||
*
|
||||
* https://github.com/supabase/supabase/blob/master/www/pages/pricing/index.tsx
|
||||
*/
|
||||
const tiers = [
|
||||
{
|
||||
name: 'Free',
|
||||
nameBadge: '',
|
||||
costUnit: 'per month per project',
|
||||
href: 'https://supabase.com/dashboard/new/new-project',
|
||||
priceLabel: 'Starting from',
|
||||
priceMonthly: 0,
|
||||
warning: 'Limit of 2 free projects',
|
||||
description: 'Perfect for passion projects & simple websites.',
|
||||
preface: 'Get started with:',
|
||||
features: [
|
||||
'Social OAuth providers',
|
||||
'Up to 500MB database & 1GB file storage',
|
||||
'Up to 2GB bandwidth',
|
||||
'Up to 50MB file uploads',
|
||||
'50,000 monthly active users',
|
||||
'Up to 500K Edge Function invocations',
|
||||
'200 concurrent Realtime connections',
|
||||
'2 million Realtime messages',
|
||||
'1-day log retention',
|
||||
'Community support',
|
||||
],
|
||||
scale: 'Free projects are paused after 1 week of inactivity.',
|
||||
|
||||
cta: 'Get Started',
|
||||
},
|
||||
{
|
||||
name: 'Pro',
|
||||
nameBadge: '',
|
||||
costUnit: 'per month per project',
|
||||
href: 'https://supabase.com/dashboard/new/new-project',
|
||||
from: true,
|
||||
priceLabel: 'Starting from',
|
||||
warning: '+ usage',
|
||||
priceMonthly: 25,
|
||||
description: 'For production applications with the option to scale.',
|
||||
features: [
|
||||
'No project pausing',
|
||||
'8GB database & 100GB file storage',
|
||||
'50GB bandwidth',
|
||||
'5GB file uploads',
|
||||
'100,000 monthly active users',
|
||||
'2M Edge Function invocations',
|
||||
'500 concurrent Realtime connections',
|
||||
'5 million Realtime messages',
|
||||
'7-day log retention',
|
||||
'Email support',
|
||||
'Daily backups',
|
||||
],
|
||||
scale: 'Additional fees apply for usage and storage beyond the limits above.',
|
||||
shutdown: '',
|
||||
preface: 'Everything in the Free plan, plus:',
|
||||
additional: '',
|
||||
cta: 'Get Started',
|
||||
},
|
||||
{
|
||||
name: 'Enterprise',
|
||||
href: 'https://forms.supabase.com/enterprise',
|
||||
description: 'For large-scale applications managing serious workloads.',
|
||||
features: [
|
||||
`Designated Support manager & SLAs`,
|
||||
`Enterprise OAuth providers`,
|
||||
`SSO/ SAML`,
|
||||
`SOC2`,
|
||||
`Custom contracts & invoicing`,
|
||||
`On-premise support`,
|
||||
`24×7×365 premium enterprise support`,
|
||||
`Custom Security questionnaires`,
|
||||
`Private Slack channel`,
|
||||
`Uptime SLA`,
|
||||
],
|
||||
priceLabel: '',
|
||||
priceMonthly: 'Contact us',
|
||||
preface: 'These apply to all projects within the organization:',
|
||||
scale: '',
|
||||
shutdown: '',
|
||||
cta: 'Contact Us',
|
||||
},
|
||||
]
|
||||
|
||||
const addons = [
|
||||
{
|
||||
name: 'Optimized Compute',
|
||||
@@ -174,21 +91,21 @@ export default function IndexPage() {
|
||||
description,
|
||||
priceDescription,
|
||||
price,
|
||||
tier,
|
||||
plan,
|
||||
showDollarSign = true,
|
||||
from = false,
|
||||
}: {
|
||||
description: string
|
||||
priceDescription: string
|
||||
price: string
|
||||
tier: string
|
||||
plan: string
|
||||
showDollarSign?: boolean
|
||||
from?: boolean
|
||||
}) => {
|
||||
return (
|
||||
<div className="mt-8 px-4 mobile-header">
|
||||
<h2 className="gradient-text-brand-500 dark:gradient-text-brand-100 text-3xl font-medium uppercase font-mono">
|
||||
{tier}
|
||||
{plan}
|
||||
</h2>
|
||||
<div className="flex items-baseline gap-2">
|
||||
{from && <span className="text-scale-1200 text-base">From</span>}
|
||||
@@ -242,7 +159,7 @@ export default function IndexPage() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mx-auto flex max-w-7xl flex-col">
|
||||
<div className="mx-auto max-w-7xl flex flex-col">
|
||||
{/* <div className="absolute inset-0 shadow-sm bg-scale-200 h-3/5" /> */}
|
||||
|
||||
<div
|
||||
@@ -252,28 +169,28 @@ export default function IndexPage() {
|
||||
"
|
||||
>
|
||||
<div className="mx-auto max-w-md grid lg:max-w-6xl lg:grid-cols-3 gap-24 lg:gap-5">
|
||||
{tiers.map((tier) => (
|
||||
{plansExceptEnterprise.map((plan) => (
|
||||
<div
|
||||
key={`row-${tier.name}`}
|
||||
key={`row-${plan.name}`}
|
||||
className={[
|
||||
tier.name === 'Pro'
|
||||
plan.name === 'Pro'
|
||||
? 'bg-brand-1100 dark:bg-brand-900 border px-0.5 -mt-8 rounded-[6px]'
|
||||
: '',
|
||||
].join(' ')}
|
||||
>
|
||||
{tier.name === 'Pro' && (
|
||||
{plan.name === 'Pro' && (
|
||||
<p className="text-xs text-center py-2 text-white">Most Popular</p>
|
||||
)}
|
||||
<div
|
||||
key={tier.name}
|
||||
key={plan.name}
|
||||
className={[
|
||||
'flex flex-col overflow-hidden',
|
||||
tier.name === 'Pro' ? '' : 'border h-full rounded-[4px]',
|
||||
plan.name === 'Pro' ? '' : 'border h-full rounded-[4px]',
|
||||
].join(' ')}
|
||||
>
|
||||
<div
|
||||
className={`dark:bg-scale-300 bg-white px-8 pt-6 rounded-tr-[4px] rounded-tl-[4px] ${
|
||||
tier.name === 'Pro' ? 'rounded-tr-[4px] rounded-tl-[4px]' : ''
|
||||
plan.name === 'Pro' ? 'rounded-tr-[4px] rounded-tl-[4px]' : ''
|
||||
}`}
|
||||
>
|
||||
<div className="mb-2 flex items-center gap-2">
|
||||
@@ -282,55 +199,56 @@ export default function IndexPage() {
|
||||
className="gradient-text-brand-500 dark:gradient-text-brand-100 text-2xl font-normal
|
||||
uppercase flex items-center gap-4 font-mono"
|
||||
>
|
||||
{tier.name}
|
||||
{plan.name}
|
||||
</h3>
|
||||
{tier.nameBadge && (
|
||||
<span className="bg-scale-300 text-scale-900 dark:bg-scale-400 dark:text-scale-1100 rounded-md bg-opacity-10 py-0.5 px-2 text-xs [background-image: none]">
|
||||
{tier.nameBadge}
|
||||
{plan.nameBadge && (
|
||||
<span className="bg-brand-500 text-brand-1100 rounded-md bg-opacity-30 py-0.5 px-2 text-xs">
|
||||
{plan.nameBadge}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-scale-1100 my-4 h-[55px] text-sm border-b dark:border-scale-500 pb-4 lg:pr-20">
|
||||
{tier.description}
|
||||
<p className="text-scale-1100 my-4 text-sm border-b dark:border-scale-500 pb-4 2xl:pr-4">
|
||||
{plan.description}
|
||||
</p>
|
||||
|
||||
<div
|
||||
className="
|
||||
text-scale-1200 flex items-baseline
|
||||
text-5xl
|
||||
font-normal
|
||||
lg:text-4xl
|
||||
xl:text-4xl
|
||||
border-b
|
||||
dark:border-scale-500
|
||||
pt-4
|
||||
pb-8
|
||||
min-h-[175px]
|
||||
"
|
||||
className={`
|
||||
text-scale-1200 flex items-baseline
|
||||
text-5xl
|
||||
font-normal
|
||||
lg:text-4xl
|
||||
xl:text-4xl
|
||||
border-b
|
||||
dark:border-scale-500
|
||||
min-h-[175px] ${plan.priceLabel ? 'pt-6' : 'pt-10'}`}
|
||||
>
|
||||
<div className="flex flex-col gap-1">
|
||||
<div className="flex items-end gap-2">
|
||||
<div>
|
||||
<p className="text-scale-900 ml-1 text-xs font-normal">
|
||||
{tier.priceLabel}
|
||||
</p>
|
||||
<p
|
||||
className={`mt-2 gradient-text-scale-500 dark:gradient-text-scale-100 pb-1 ${
|
||||
tier.name !== 'Enterprise' ? 'text-5xl' : 'text-4xl'
|
||||
}`}
|
||||
>
|
||||
{tier.name !== 'Enterprise' && '$'}
|
||||
{tier.priceMonthly}
|
||||
</p>
|
||||
{tier.costUnit && (
|
||||
<p className="text-scale-900 mt-0.5 text-xs">{tier.costUnit}</p>
|
||||
{plan.priceLabel && (
|
||||
<p className="text-scale-900 ml-1 text-xs font-normal">
|
||||
{plan.priceLabel}
|
||||
</p>
|
||||
)}
|
||||
|
||||
{tier.warning && (
|
||||
<div className="flex items-end">
|
||||
<p
|
||||
className={`mt-2 gradient-text-scale-500 dark:gradient-text-scale-100 pb-1 ${
|
||||
plan.name !== 'Enterprise' ? 'text-5xl' : 'text-4xl'
|
||||
}`}
|
||||
>
|
||||
${plan.priceMonthly}
|
||||
</p>
|
||||
<p className="text-scale-900 mb-1.5 ml-1 text-xs">
|
||||
{plan.costUnit}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{plan.warning && (
|
||||
<p className="-mt-2">
|
||||
<span className="bg-brand-500 text-brand-1100 rounded-md bg-opacity-30 py-0.5 px-2 text-xs ">
|
||||
{tier.warning}
|
||||
<span className="bg-scale-200 text-brand-1100 border shadow-sm rounded-md bg-opacity-30 py-0.5 px-2 text-xs">
|
||||
{plan.warning}
|
||||
</span>
|
||||
</p>
|
||||
)}
|
||||
@@ -342,18 +260,18 @@ export default function IndexPage() {
|
||||
<div
|
||||
className={[
|
||||
`dark:border-scale-400 dark:bg-scale-300 flex h-full rounded-bl-[4px] rounded-br-[4px] flex-1 flex-col bg-white px-8 py-6`,
|
||||
tier.name === 'Pro' ? 'mb-0.5 rounded-bl-[4px] rounded-br-[4px]' : '',
|
||||
plan.name === 'Pro' ? 'mb-0.5 rounded-bl-[4px] rounded-br-[4px]' : '',
|
||||
].join(' ')}
|
||||
>
|
||||
{tier.preface && (
|
||||
<p className="text-scale-1100 text-xs mt-2 mb-4">{tier.preface}</p>
|
||||
{plan.preface && (
|
||||
<p className="text-scale-1100 text-xs mt-2 mb-4">{plan.preface}</p>
|
||||
)}
|
||||
{/* <p className="text-scale-900 text-sm">Included with plan:</p> */}
|
||||
<ul role="list" className="text-xs text-scale-1000">
|
||||
{tier.features.map((feature) => (
|
||||
{plan.features.map((feature) => (
|
||||
<li key={feature} className="flex items-center py-2 first:mt-0">
|
||||
<IconCheck
|
||||
className="text-brand-900 h-4 w-4 "
|
||||
className="text-brand-900 h-4 w-4"
|
||||
aria-hidden="true"
|
||||
strokeWidth={3}
|
||||
/>
|
||||
@@ -365,15 +283,13 @@ export default function IndexPage() {
|
||||
|
||||
<div className="flex flex-col gap-6 mt-auto prose">
|
||||
<div className="space-y-2 mt-12">
|
||||
{tier.additional && <p className="text-sm">{tier.additional}</p>}
|
||||
{tier.scale && <p className="text-xs">{tier.scale}</p>}
|
||||
{tier.shutdown && (
|
||||
<p className="text-scale-1000 text-xs">{tier.shutdown}</p>
|
||||
{plan.footer && (
|
||||
<p className="text-xs whitespace-pre-wrap">{plan.footer}</p>
|
||||
)}
|
||||
</div>
|
||||
<a href={tier.href}>
|
||||
<a href={plan.href}>
|
||||
<Button block size="small">
|
||||
{tier.cta}
|
||||
{plan.cta}
|
||||
</Button>
|
||||
</a>
|
||||
</div>
|
||||
@@ -384,6 +300,51 @@ export default function IndexPage() {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div className="max-w-md lg:max-w-6xl grid grid-cols-1 lg:grid-cols-3 mx-auto border bg-scale-100 rounded-md p-8 lg:px-0 mt-8 gap-x-5">
|
||||
<div className="flex flex-col justify-center lg:px-8">
|
||||
<div>
|
||||
<h3
|
||||
className="gradient-text-brand-500 dark:gradient-text-brand-100 text-2xl font-normal
|
||||
uppercase flex items-center gap-4 font-mono"
|
||||
>
|
||||
{planEnterprise.name}
|
||||
</h3>
|
||||
<p className="text-scale-1100 my-4 text-sm pb-2 2xl:pr-4">
|
||||
{planEnterprise.description}
|
||||
</p>
|
||||
|
||||
<a href={planEnterprise.href} className="hidden lg:block">
|
||||
<Button type="default" size="small">
|
||||
{planEnterprise.cta}
|
||||
</Button>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col justify-center lg:col-span-2">
|
||||
<ul role="list" className="text-xs text-scale-1000 lg:grid lg:grid-cols-2 lg:gap-x-5">
|
||||
{planEnterprise.features.map((feature) => (
|
||||
<li key={feature} className="flex items-center py-2 first:mt-0">
|
||||
<IconCheck
|
||||
className="text-brand-900 h-4 w-4 "
|
||||
aria-hidden="true"
|
||||
strokeWidth={3}
|
||||
/>
|
||||
<span className="dark:text-scale-1200 mb-0 ml-3 ">{feature}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
|
||||
<a href={planEnterprise.href} className="visible lg:hidden mt-8">
|
||||
<Button block type="default" size="small">
|
||||
{planEnterprise.cta}
|
||||
</Button>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="text-center mt-24">
|
||||
<a href="#compare-plans">
|
||||
<Button size="tiny" type="default">
|
||||
@@ -486,12 +447,10 @@ export default function IndexPage() {
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<div className="">
|
||||
<img
|
||||
className="w-full"
|
||||
src={`${basePath}/images/pricing/spend-cap${isDarkMode ? '' : '-light'}.png`}
|
||||
/>
|
||||
</div>
|
||||
<img
|
||||
className="w-full"
|
||||
src={`${basePath}/images/pricing/spend-cap${isDarkMode ? '' : '-light'}.png`}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -519,79 +478,80 @@ export default function IndexPage() {
|
||||
id="change-plan"
|
||||
name="Change plan"
|
||||
layout="vertical"
|
||||
value={activeMobileTier}
|
||||
value={activeMobilePlan}
|
||||
className="min-w-[120px] bg-slate-400 text-red-500"
|
||||
onChange={(e) => setActiveMobileTier(e.target.value)}
|
||||
onChange={(e) => setActiveMobilePlan(e.target.value)}
|
||||
>
|
||||
<Select.Option value="Free">Free</Select.Option>
|
||||
<Select.Option value="Pro">Pro</Select.Option>
|
||||
<Select.Option value="Team">Team</Select.Option>
|
||||
<Select.Option value="Enterprise">Enterprise</Select.Option>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
{activeMobileTier === 'Free' && (
|
||||
{activeMobilePlan === 'Free' && (
|
||||
<>
|
||||
<MobileHeader
|
||||
tier="Free"
|
||||
plan="Free"
|
||||
price={'0'}
|
||||
priceDescription={'/mo'}
|
||||
description={'Perfect for hobby projects and experiments'}
|
||||
/>
|
||||
<PricingTableRowMobile
|
||||
category={pricing.database}
|
||||
tier={'free'}
|
||||
plan={'free'}
|
||||
icon={Solutions['database'].icon}
|
||||
sectionId="database"
|
||||
/>
|
||||
<PricingTableRowMobile
|
||||
category={pricing.auth}
|
||||
tier={'free'}
|
||||
plan={'free'}
|
||||
icon={Solutions['authentication'].icon}
|
||||
sectionId="auth"
|
||||
/>
|
||||
<PricingTableRowMobile
|
||||
category={pricing.storage}
|
||||
tier={'free'}
|
||||
plan={'free'}
|
||||
icon={Solutions['storage'].icon}
|
||||
sectionId="storage"
|
||||
/>
|
||||
<PricingTableRowMobile
|
||||
category={pricing.realtime}
|
||||
tier={'free'}
|
||||
plan={'free'}
|
||||
icon={Solutions['realtime'].icon}
|
||||
sectionId="realtime"
|
||||
/>
|
||||
<PricingTableRowMobile
|
||||
category={pricing['edge-functions']}
|
||||
tier={'free'}
|
||||
plan={'free'}
|
||||
icon={Solutions['edge-functions'].icon}
|
||||
sectionId="edge-functions"
|
||||
/>
|
||||
<PricingTableRowMobile
|
||||
category={pricing.dashboard}
|
||||
tier={'free'}
|
||||
plan={'free'}
|
||||
icon={pricing.dashboard.icon}
|
||||
sectionId="dashboard"
|
||||
/>
|
||||
<PricingTableRowMobile
|
||||
category={pricing.security}
|
||||
tier={'free'}
|
||||
plan={'free'}
|
||||
icon={pricing.security.icon}
|
||||
sectionId="security"
|
||||
/>
|
||||
<PricingTableRowMobile
|
||||
category={pricing.support}
|
||||
tier={'free'}
|
||||
plan={'free'}
|
||||
icon={pricing.support.icon}
|
||||
sectionId="support"
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
{activeMobileTier === 'Pro' && (
|
||||
{activeMobilePlan === 'Pro' && (
|
||||
<>
|
||||
<MobileHeader
|
||||
tier="Pro"
|
||||
plan="Pro"
|
||||
from={false}
|
||||
price={'25'}
|
||||
priceDescription={'/mo + additional use'}
|
||||
@@ -599,51 +559,103 @@ export default function IndexPage() {
|
||||
/>
|
||||
<PricingTableRowMobile
|
||||
category={pricing.database}
|
||||
tier={'pro'}
|
||||
plan={'pro'}
|
||||
icon={Solutions['database'].icon}
|
||||
/>
|
||||
<PricingTableRowMobile
|
||||
category={pricing.auth}
|
||||
tier={'pro'}
|
||||
plan={'pro'}
|
||||
icon={Solutions['authentication'].icon}
|
||||
/>
|
||||
<PricingTableRowMobile
|
||||
category={pricing.storage}
|
||||
tier={'pro'}
|
||||
plan={'pro'}
|
||||
icon={Solutions['storage'].icon}
|
||||
/>
|
||||
<PricingTableRowMobile
|
||||
category={pricing.realtime}
|
||||
tier={'pro'}
|
||||
plan={'pro'}
|
||||
icon={Solutions['realtime'].icon}
|
||||
/>
|
||||
<PricingTableRowMobile
|
||||
category={pricing['edge-functions']}
|
||||
tier={'pro'}
|
||||
plan={'pro'}
|
||||
icon={Solutions['edge-functions'].icon}
|
||||
/>
|
||||
<PricingTableRowMobile
|
||||
category={pricing.dashboard}
|
||||
tier={'pro'}
|
||||
plan={'pro'}
|
||||
icon={pricing.dashboard.icon}
|
||||
/>
|
||||
<PricingTableRowMobile
|
||||
category={pricing.security}
|
||||
tier={'pro'}
|
||||
plan={'pro'}
|
||||
icon={pricing.security.icon}
|
||||
/>
|
||||
<PricingTableRowMobile
|
||||
category={pricing.support}
|
||||
tier={'pro'}
|
||||
plan={'pro'}
|
||||
icon={pricing.support.icon}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
{activeMobileTier === 'Enterprise' && (
|
||||
{activeMobilePlan === 'Team' && (
|
||||
<>
|
||||
<MobileHeader
|
||||
tier="Enterprise"
|
||||
plan="Team"
|
||||
from={false}
|
||||
price={'599'}
|
||||
priceDescription={'/mo + additional use'}
|
||||
description={'Collaborate with different permissions and access patterns'}
|
||||
/>
|
||||
<PricingTableRowMobile
|
||||
category={pricing.database}
|
||||
plan={'team'}
|
||||
icon={Solutions['database'].icon}
|
||||
/>
|
||||
<PricingTableRowMobile
|
||||
category={pricing.auth}
|
||||
plan={'team'}
|
||||
icon={Solutions['authentication'].icon}
|
||||
/>
|
||||
<PricingTableRowMobile
|
||||
category={pricing.storage}
|
||||
plan={'team'}
|
||||
icon={Solutions['storage'].icon}
|
||||
/>
|
||||
<PricingTableRowMobile
|
||||
category={pricing.realtime}
|
||||
plan={'team'}
|
||||
icon={Solutions['realtime'].icon}
|
||||
/>
|
||||
<PricingTableRowMobile
|
||||
category={pricing['edge-functions']}
|
||||
plan={'team'}
|
||||
icon={Solutions['edge-functions'].icon}
|
||||
/>
|
||||
<PricingTableRowMobile
|
||||
category={pricing.dashboard}
|
||||
plan={'team'}
|
||||
icon={pricing.dashboard.icon}
|
||||
/>
|
||||
<PricingTableRowMobile
|
||||
category={pricing.security}
|
||||
plan={'team'}
|
||||
icon={pricing.security.icon}
|
||||
/>
|
||||
<PricingTableRowMobile
|
||||
category={pricing.support}
|
||||
plan={'team'}
|
||||
icon={pricing.support.icon}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
{activeMobilePlan === 'Enterprise' && (
|
||||
<>
|
||||
<MobileHeader
|
||||
plan="Enterprise"
|
||||
price={'Contact us for a quote'}
|
||||
priceDescription={''}
|
||||
description={
|
||||
@@ -653,42 +665,42 @@ export default function IndexPage() {
|
||||
/>
|
||||
<PricingTableRowMobile
|
||||
category={pricing.database}
|
||||
tier={'enterprise'}
|
||||
plan={'enterprise'}
|
||||
icon={Solutions['database'].icon}
|
||||
/>
|
||||
<PricingTableRowMobile
|
||||
category={pricing.auth}
|
||||
tier={'enterprise'}
|
||||
plan={'enterprise'}
|
||||
icon={Solutions['authentication'].icon}
|
||||
/>
|
||||
<PricingTableRowMobile
|
||||
category={pricing.storage}
|
||||
tier={'enterprise'}
|
||||
plan={'enterprise'}
|
||||
icon={Solutions['storage'].icon}
|
||||
/>
|
||||
<PricingTableRowMobile
|
||||
category={pricing.realtime}
|
||||
tier={'enterprise'}
|
||||
plan={'enterprise'}
|
||||
icon={Solutions['realtime'].icon}
|
||||
/>
|
||||
<PricingTableRowMobile
|
||||
category={pricing['edge-functions']}
|
||||
tier={'enterprise'}
|
||||
plan={'enterprise'}
|
||||
icon={Solutions['edge-functions'].icon}
|
||||
/>
|
||||
<PricingTableRowMobile
|
||||
category={pricing.dashboard}
|
||||
tier={'enterprise'}
|
||||
plan={'enterprise'}
|
||||
icon={pricing.dashboard.icon}
|
||||
/>
|
||||
<PricingTableRowMobile
|
||||
category={pricing.security}
|
||||
tier={'enterprise'}
|
||||
plan={'enterprise'}
|
||||
icon={pricing.security.icon}
|
||||
/>
|
||||
<PricingTableRowMobile
|
||||
category={pricing.support}
|
||||
tier={'enterprise'}
|
||||
plan={'enterprise'}
|
||||
icon={pricing.support.icon}
|
||||
/>
|
||||
</>
|
||||
@@ -712,14 +724,14 @@ export default function IndexPage() {
|
||||
></div>
|
||||
</th>
|
||||
|
||||
{tiers.map((tier) => (
|
||||
{plans.map((plan) => (
|
||||
<th
|
||||
className="text-scale-1200 w-1/4 px-6 pr-2 pt-2 pb-2 text-left text-sm font-normal"
|
||||
scope="col"
|
||||
key={tier.name}
|
||||
key={plan.name}
|
||||
>
|
||||
<h3 className="gradient-text-brand-500 dark:gradient-text-brand-100 text-2xl font-mono font-normal uppercase flex items-center gap-4">
|
||||
{tier.name}
|
||||
{plan.name}
|
||||
</h3>
|
||||
<div
|
||||
className="h-0.25 absolute bottom-0 left-0 w-full"
|
||||
@@ -735,13 +747,13 @@ export default function IndexPage() {
|
||||
scope="col"
|
||||
></th>
|
||||
|
||||
{tiers.map((tier) => (
|
||||
{plans.map((plan) => (
|
||||
<th
|
||||
className="text-scale-1200 w-1/4 px-6 pt-2 pb-2 text-left text-sm font-normal"
|
||||
scope="col"
|
||||
key={`th-${tier.name}`}
|
||||
key={`th-${plan.name}`}
|
||||
>
|
||||
<p className="p text-sm border-b border-scale-700 pb-4">{tier.description}</p>
|
||||
<p className="p text-sm border-b border-scale-700 pb-4">{plan.description}</p>
|
||||
<div
|
||||
className="h-0.25 absolute bottom-0 left-0 w-full"
|
||||
style={{ height: '1px' }}
|
||||
@@ -750,47 +762,50 @@ export default function IndexPage() {
|
||||
))}
|
||||
</tr>
|
||||
<tbody className="border-scale-700 dark:border-scale-400 divide-scale-700 dark:divide-scale-400 divide-y">
|
||||
<tr className="">
|
||||
<tr>
|
||||
<th
|
||||
className="text-scale-900 px-6 py-8 text-left align-top text-sm font-medium dark:text-white"
|
||||
scope="row"
|
||||
></th>
|
||||
|
||||
{tiers.map((tier) => (
|
||||
<td className="h-full px-6 py-2 align-top" key={`price-${tier.name}`}>
|
||||
{plans.map((plan) => (
|
||||
<td className="h-full px-6 py-2 align-top" key={`price-${plan.name}`}>
|
||||
<div className="relative table h-full w-full">
|
||||
<div className="flex flex-col justify-between h-full">
|
||||
<>
|
||||
<span
|
||||
className={`text-scale-1200 ${
|
||||
tier.name !== 'Enterprise' ? 'text-5xl' : 'text-4xl'
|
||||
plan.name !== 'Enterprise' ? 'text-5xl' : 'text-4xl'
|
||||
}`}
|
||||
>
|
||||
{tier.name !== 'Enterprise' && '$'}
|
||||
{tier.priceMonthly}
|
||||
{plan.name !== 'Enterprise' && '$'}
|
||||
{plan.priceMonthly}
|
||||
</span>
|
||||
{tier.name !== 'Enterprise' && (
|
||||
<p className="p text-xs mt-1">per project per month</p>
|
||||
{['Pro', 'Free'].includes(plan.name) && (
|
||||
<p className="p text-xs mt-1">per month</p>
|
||||
)}
|
||||
{['Team'].includes(plan.name) && (
|
||||
<p className="p text-xs mt-1">per month</p>
|
||||
)}
|
||||
</>
|
||||
|
||||
{tier.warning && (
|
||||
{plan.warning && (
|
||||
<p className="-mt-2">
|
||||
<span className="bg-brand-500 text-brand-1100 rounded-md bg-opacity-30 py-0.5 px-2 text-xs ">
|
||||
{tier.warning}
|
||||
<span className="bg-scale-100 text-brand-1100 border shadow-sm rounded-md bg-opacity-30 py-0.5 px-2 text-xs">
|
||||
{plan.warning}
|
||||
</span>
|
||||
</p>
|
||||
)}
|
||||
|
||||
<div className={tier.name === 'Enterprise' ? 'mt-auto' : 'mt-8'}>
|
||||
<Link href={tier.href} as={tier.href}>
|
||||
<div className={plan.name === 'Enterprise' ? 'mt-auto' : 'mt-8'}>
|
||||
<Link href={plan.href} as={plan.href}>
|
||||
<a>
|
||||
<Button
|
||||
size="tiny"
|
||||
type={tier.name === 'Enterprise' ? 'default' : 'primary'}
|
||||
type={plan.name === 'Enterprise' ? 'default' : 'primary'}
|
||||
block
|
||||
>
|
||||
{tier.cta}
|
||||
{plan.cta}
|
||||
</Button>
|
||||
</a>
|
||||
</Link>
|
||||
@@ -875,7 +890,7 @@ export default function IndexPage() {
|
||||
</td>
|
||||
|
||||
<td className="px-6 pt-5">
|
||||
<Link href="https://forms.supabase.com/enterprise">
|
||||
<Link href="https://forms.supabase.com/team">
|
||||
<a>
|
||||
<Button size="tiny" type="primary" block>
|
||||
Contact us
|
||||
@@ -883,6 +898,16 @@ export default function IndexPage() {
|
||||
</a>
|
||||
</Link>
|
||||
</td>
|
||||
|
||||
<td className="px-6 pt-5">
|
||||
<Link href="https://forms.supabase.com/enterprise">
|
||||
<a>
|
||||
<Button size="tiny" type="default" block>
|
||||
Contact us
|
||||
</Button>
|
||||
</a>
|
||||
</Link>
|
||||
</td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
|
||||
Generated
+2
@@ -764,6 +764,7 @@
|
||||
"remark": "^13.0.0",
|
||||
"remark-gfm": "^3.0.1",
|
||||
"remark-html": "^15.0.1",
|
||||
"shared-data": "*",
|
||||
"swiper": "^6.5.0",
|
||||
"swr": "^0.5.4",
|
||||
"typescript": "^4.0.3",
|
||||
@@ -82836,6 +82837,7 @@
|
||||
"remark": "^13.0.0",
|
||||
"remark-gfm": "^3.0.1",
|
||||
"remark-html": "^15.0.1",
|
||||
"shared-data": "*",
|
||||
"swiper": "^6.5.0",
|
||||
"swr": "^0.5.4",
|
||||
"tailwindcss": "^3.1.8",
|
||||
|
||||
@@ -8,17 +8,19 @@
|
||||
"tooltips": {
|
||||
"main": "A Postgres database with no restrictions? You get it. No pseudo limited users, you are the postgres root user. No caveats."
|
||||
},
|
||||
"tiers": {
|
||||
"plans": {
|
||||
"free": true,
|
||||
"pro": true,
|
||||
"team": true,
|
||||
"enterprise": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"title": "Unlimited API requests",
|
||||
"tiers": {
|
||||
"plans": {
|
||||
"free": true,
|
||||
"pro": true,
|
||||
"team": true,
|
||||
"enterprise": true
|
||||
}
|
||||
},
|
||||
@@ -27,9 +29,10 @@
|
||||
"tooltips": {
|
||||
"main": "Billing is based on the average database size in GB throughout the billing period."
|
||||
},
|
||||
"tiers": {
|
||||
"plans": {
|
||||
"free": "500 MB",
|
||||
"pro": "8 GB, then $0.125 per GB",
|
||||
"team": "8 GB, then $0.125 per GB",
|
||||
"enterprise": "Unlimited"
|
||||
}
|
||||
},
|
||||
@@ -37,11 +40,13 @@
|
||||
"title": "Automatic backups",
|
||||
"tooltips": {
|
||||
"main": "Backups are entire copies of your database that can be restored in the future.",
|
||||
"pro": "7 days of backup (if > 1TB, contact for Enterprise pricing)"
|
||||
"pro": "7 days of backup (if > 1TB, contact for Enterprise pricing)",
|
||||
"team": "14 days of backup (if > 1TB, contact for Enterprise pricing)"
|
||||
},
|
||||
"tiers": {
|
||||
"plans": {
|
||||
"free": false,
|
||||
"pro": "7 days",
|
||||
"team": "14 days",
|
||||
"enterprise": "Custom"
|
||||
}
|
||||
},
|
||||
@@ -49,11 +54,13 @@
|
||||
"title": "Point in time recovery",
|
||||
"tooltips": {
|
||||
"main": "PITR cannot be applied retroactively, projects can only be rolled back to the point from which PITR has been applied.",
|
||||
"pro": "$100 per 7 days. If > 28 day rollback period, contact enterprise"
|
||||
"pro": "$100 per 7 days. If > 28 day rollback period, contact enterprise",
|
||||
"team": "$100 per 7 days. If > 28 day rollback period, contact enterprise"
|
||||
},
|
||||
"tiers": {
|
||||
"plans": {
|
||||
"free": false,
|
||||
"pro": "$100 per 7 days",
|
||||
"team": "$100 per 7 days",
|
||||
"enterprise": true
|
||||
}
|
||||
},
|
||||
@@ -62,9 +69,10 @@
|
||||
"tooltips": {
|
||||
"main": "Projects that have no activity or API requests will be paused. They can be reactivated via the dashboard"
|
||||
},
|
||||
"tiers": {
|
||||
"plans": {
|
||||
"free": "After 1 inactive week",
|
||||
"pro": "Never",
|
||||
"team": "Never",
|
||||
"enterprise": "Never"
|
||||
}
|
||||
},
|
||||
@@ -73,9 +81,10 @@
|
||||
"tooltips": {
|
||||
"main": "Billing is based on the total sum of outgoing traffic of your database in GB throughout your billing period. Only download operations are counted towards the limit"
|
||||
},
|
||||
"tiers": {
|
||||
"plans": {
|
||||
"free": "2GB",
|
||||
"pro": "50 GB, then $0.09 per GB",
|
||||
"team": "50 GB, then $0.09 per GB",
|
||||
"enterprise": "Unlimited"
|
||||
}
|
||||
}
|
||||
@@ -88,9 +97,10 @@
|
||||
{
|
||||
"title": "Total Users",
|
||||
"tooltips": { "main": "The maximum number of users your project can have" },
|
||||
"tiers": {
|
||||
"plans": {
|
||||
"free": "Unlimited",
|
||||
"pro": "Unlimited",
|
||||
"team": "Unlimited",
|
||||
"enterprise": "Unlimited"
|
||||
}
|
||||
},
|
||||
@@ -99,74 +109,83 @@
|
||||
"tooltips": {
|
||||
"main": "Distinct users requesting your API throughout the billing period."
|
||||
},
|
||||
"tiers": {
|
||||
"plans": {
|
||||
"free": "50,000",
|
||||
"pro": "100,000, then $0.00325 per MAU",
|
||||
"team": "100,000, then $0.00325 per MAU",
|
||||
"enterprise": "Unlimited"
|
||||
}
|
||||
},
|
||||
{
|
||||
"title": "Social OAuth providers",
|
||||
"tiers": {
|
||||
"plans": {
|
||||
"free": true,
|
||||
"pro": true,
|
||||
"team": true,
|
||||
"enterprise": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"title": "Custom SMTP server",
|
||||
"tiers": {
|
||||
"plans": {
|
||||
"free": true,
|
||||
"pro": true,
|
||||
"team": true,
|
||||
"enterprise": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"title": "Remove Supabase branding from emails",
|
||||
"tiers": {
|
||||
"plans": {
|
||||
"free": false,
|
||||
"pro": true,
|
||||
"team": true,
|
||||
"enterprise": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"title": "Enterprise OAuth providers",
|
||||
"tiers": {
|
||||
"plans": {
|
||||
"free": false,
|
||||
"pro": false,
|
||||
"team": false,
|
||||
"enterprise": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"title": "Audit trails",
|
||||
"tiers": {
|
||||
"plans": {
|
||||
"free": "1 hour",
|
||||
"pro": "7 days",
|
||||
"team": "28 days",
|
||||
"enterprise": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"title": "Supabase Auth emails",
|
||||
"tooltips": { "main": "Rate limits do not apply to Custom SMTP" },
|
||||
"tiers": {
|
||||
"plans": {
|
||||
"free": "30 / hour",
|
||||
"pro": "100 / hour",
|
||||
"team": "100 / hour",
|
||||
"enterprise": "Contact Us"
|
||||
}
|
||||
},
|
||||
{
|
||||
"title": "Single Sign-On (SAML 2.0)",
|
||||
"tiers": {
|
||||
"plans": {
|
||||
"free": false,
|
||||
"pro": "50 free, then $0.015 per MAU",
|
||||
"team": "50 free, then $0.015 per MAU",
|
||||
"enterprise": "Contact Us"
|
||||
}
|
||||
},
|
||||
{
|
||||
"title": "Advanced security features",
|
||||
"tiers": {
|
||||
"plans": {
|
||||
"free": false,
|
||||
"pro": false,
|
||||
"team": false,
|
||||
"enterprise": "Contact Us"
|
||||
}
|
||||
}
|
||||
@@ -182,9 +201,10 @@
|
||||
"tooltips": {
|
||||
"main": "The sum of all objects' size in your storage buckets."
|
||||
},
|
||||
"tiers": {
|
||||
"plans": {
|
||||
"free": "1 GB",
|
||||
"pro": "100 GB, then $0.021 per GB",
|
||||
"team": "100 GB, then $0.021 per GB",
|
||||
"enterprise": "Unlimited"
|
||||
}
|
||||
},
|
||||
@@ -193,26 +213,29 @@
|
||||
"tooltips": {
|
||||
"main": "Total sum of outgoing traffic (egress) from your storage buckets, only download operations are counted."
|
||||
},
|
||||
"tiers": {
|
||||
"plans": {
|
||||
"free": "2 GB",
|
||||
"pro": "200 GB, then $0.09 per GB",
|
||||
"team": "200 GB, then $0.09 per GB",
|
||||
"enterprise": "Unlimited"
|
||||
}
|
||||
},
|
||||
{
|
||||
"title": "Custom access controls",
|
||||
"tiers": {
|
||||
"plans": {
|
||||
"free": true,
|
||||
"pro": true,
|
||||
"team": true,
|
||||
"enterprise": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"title": "Max file upload size",
|
||||
"tooltips": { "main": "You can change the upload size in the dashboard" },
|
||||
"tiers": {
|
||||
"plans": {
|
||||
"free": "50MB",
|
||||
"pro": "5GB",
|
||||
"team": "5GB",
|
||||
"enterprise": "Unlimited"
|
||||
}
|
||||
},
|
||||
@@ -221,9 +244,11 @@
|
||||
"tooltips": {
|
||||
"main": "Distinct count of all images that were transformed in the billing period, ignoring any transformations."
|
||||
},
|
||||
"tiers": {
|
||||
"plans": {
|
||||
"free": false,
|
||||
"pro": "100 origin images, then $5 per 1000 origin images"
|
||||
"pro": "100 origin images, then $5 per 1000 origin images",
|
||||
"team": "100 origin images, then $5 per 1000 origin images",
|
||||
"enterprise": "Unlimited"
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -238,17 +263,19 @@
|
||||
"main": "Billing is based on the sum of all invocations, independent of response status, throughout your billing period."
|
||||
},
|
||||
"main": "Billing is based on the sum of all invocations, independent of response status, throughout your billing period.",
|
||||
"tiers": {
|
||||
"plans": {
|
||||
"free": "500K/month",
|
||||
"pro": "2 Million, then $2 per 1 Million",
|
||||
"team": "2 Million, then $2 per 1 Million",
|
||||
"enterprise": "Unlimited"
|
||||
}
|
||||
},
|
||||
{
|
||||
"title": "Script size",
|
||||
"tiers": {
|
||||
"plans": {
|
||||
"free": "2 MB",
|
||||
"pro": "10 MB",
|
||||
"team": "10 MB",
|
||||
"enterprise": "Unlimited"
|
||||
}
|
||||
},
|
||||
@@ -257,9 +284,10 @@
|
||||
"tooltips": {
|
||||
"main": "Billing is based on the maximum amount of functions at any point in time throughout your billing period."
|
||||
},
|
||||
"tiers": {
|
||||
"plans": {
|
||||
"free": "10",
|
||||
"pro": "100, then $10 per additional 100",
|
||||
"team": "100, then $10 per additional 100",
|
||||
"enterprise": "Unlimited"
|
||||
}
|
||||
}
|
||||
@@ -271,9 +299,10 @@
|
||||
"features": [
|
||||
{
|
||||
"title": "Postgres Changes",
|
||||
"tiers": {
|
||||
"plans": {
|
||||
"free": true,
|
||||
"pro": true,
|
||||
"team": true,
|
||||
"enterprise": true
|
||||
}
|
||||
},
|
||||
@@ -282,9 +311,10 @@
|
||||
"tooltips": {
|
||||
"main": "We only charge for successful connections - not connection attempts!"
|
||||
},
|
||||
"tiers": {
|
||||
"plans": {
|
||||
"free": "200",
|
||||
"pro": "500, then $10 per 1000",
|
||||
"team": "500, then $10 per 1000",
|
||||
"enterprise": "Unlimited concurrent connections and volume discount"
|
||||
}
|
||||
},
|
||||
@@ -293,17 +323,19 @@
|
||||
"tooltips": {
|
||||
"main": "Billing is based on the total sum of Realtime messages throughout your billing period."
|
||||
},
|
||||
"tiers": {
|
||||
"plans": {
|
||||
"free": "2 Million",
|
||||
"pro": "5 Million, then $2.50 per Million",
|
||||
"team": "5 Million, then $2.50 per Million",
|
||||
"enterprise": "Volume discounts on messages"
|
||||
}
|
||||
},
|
||||
{
|
||||
"title": "Max Message Size",
|
||||
"tiers": {
|
||||
"plans": {
|
||||
"free": "250 KB",
|
||||
"pro": "3 MB",
|
||||
"team": "3 MB",
|
||||
"enterprise": "Custom"
|
||||
}
|
||||
}
|
||||
@@ -315,25 +347,28 @@
|
||||
"features": [
|
||||
{
|
||||
"title": "Team members",
|
||||
"tiers": {
|
||||
"plans": {
|
||||
"free": "Unlimited",
|
||||
"pro": "Unlimited",
|
||||
"team": "Unlimited",
|
||||
"enterprise": "Unlimited"
|
||||
}
|
||||
},
|
||||
{
|
||||
"title": "Access controls",
|
||||
"tiers": {
|
||||
"plans": {
|
||||
"free": "Coming soon",
|
||||
"pro": "Coming soon",
|
||||
"team": true,
|
||||
"enterprise": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"title": "Audit trails",
|
||||
"tiers": {
|
||||
"plans": {
|
||||
"free": false,
|
||||
"pro": false,
|
||||
"team": true,
|
||||
"enterprise": true
|
||||
}
|
||||
}
|
||||
@@ -345,73 +380,82 @@
|
||||
"features": [
|
||||
{
|
||||
"title": "On Premises / BYO cloud",
|
||||
"tiers": {
|
||||
"plans": {
|
||||
"free": false,
|
||||
"pro": false,
|
||||
"team": false,
|
||||
"enterprise": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"title": "Log retention (API & Database)",
|
||||
"tiers": {
|
||||
"plans": {
|
||||
"free": "1 day",
|
||||
"pro": "7 days",
|
||||
"team": "28 days",
|
||||
"enterprise": "90 days"
|
||||
}
|
||||
},
|
||||
{
|
||||
"title": "Log drain",
|
||||
"tiers": {
|
||||
"plans": {
|
||||
"free": false,
|
||||
"pro": false,
|
||||
"team": "Coming soon",
|
||||
"enterprise": "Coming soon"
|
||||
}
|
||||
},
|
||||
{
|
||||
"title": "Metrics endpoint",
|
||||
"tiers": {
|
||||
"plans": {
|
||||
"free": false,
|
||||
"pro": true,
|
||||
"team": true,
|
||||
"enterprise": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"title": "SOC2",
|
||||
"tiers": {
|
||||
"plans": {
|
||||
"free": false,
|
||||
"pro": false,
|
||||
"team": true,
|
||||
"enterprise": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"title": "SSO",
|
||||
"tiers": {
|
||||
"plans": {
|
||||
"free": false,
|
||||
"pro": false,
|
||||
"team": "Contact Us",
|
||||
"enterprise": "Contact Us"
|
||||
}
|
||||
},
|
||||
{
|
||||
"title": "99.9% SLA",
|
||||
"tiers": {
|
||||
"plans": {
|
||||
"free": false,
|
||||
"pro": false,
|
||||
"team": false,
|
||||
"enterprise": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"title": "Access Roles",
|
||||
"tiers": {
|
||||
"plans": {
|
||||
"free": "Owner, Developer",
|
||||
"pro": "Owner, Developer",
|
||||
"team": "Additional owner(s), admin, read-only, billing admin, custom",
|
||||
"enterprise": "Additional owner(s), admin, read-only, billing admin, custom"
|
||||
}
|
||||
},
|
||||
{
|
||||
"title": "Vanity URLs",
|
||||
"tiers": {
|
||||
"plans": {
|
||||
"free": false,
|
||||
"pro": true,
|
||||
"team": true,
|
||||
"enterprise": true
|
||||
}
|
||||
},
|
||||
@@ -420,9 +464,10 @@
|
||||
"tooltips": {
|
||||
"enterprise": "Volume discounts available."
|
||||
},
|
||||
"tiers": {
|
||||
"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"
|
||||
}
|
||||
}
|
||||
@@ -434,57 +479,64 @@
|
||||
"features": [
|
||||
{
|
||||
"title": "Community support",
|
||||
"tiers": {
|
||||
"plans": {
|
||||
"free": true,
|
||||
"pro": true,
|
||||
"team": true,
|
||||
"enterprise": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"title": "Email support",
|
||||
"tiers": {
|
||||
"plans": {
|
||||
"free": false,
|
||||
"pro": true,
|
||||
"team": true,
|
||||
"enterprise": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"title": "Email support SLA",
|
||||
"tiers": {
|
||||
"plans": {
|
||||
"free": false,
|
||||
"pro": false,
|
||||
"team": true,
|
||||
"enterprise": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"title": "Designated support",
|
||||
"tiers": {
|
||||
"plans": {
|
||||
"free": false,
|
||||
"pro": false,
|
||||
"team": false,
|
||||
"enterprise": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"title": "On Boarding Support",
|
||||
"tiers": {
|
||||
"plans": {
|
||||
"free": false,
|
||||
"pro": false,
|
||||
"team": false,
|
||||
"enterprise": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"title": "Designated customer success engineer ",
|
||||
"tiers": {
|
||||
"plans": {
|
||||
"free": false,
|
||||
"pro": false,
|
||||
"team": false,
|
||||
"enterprise": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"title": "Security Questionnaire Help",
|
||||
"tiers": {
|
||||
"plans": {
|
||||
"free": false,
|
||||
"pro": false,
|
||||
"team": true,
|
||||
"enterprise": true
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,6 @@
|
||||
import tweets from './tweets'
|
||||
import logConstants from './logConstants'
|
||||
import { plans, PricingInformation } from './plans'
|
||||
|
||||
export { tweets, logConstants }
|
||||
export { tweets, logConstants, plans }
|
||||
export type { PricingInformation }
|
||||
@@ -3,6 +3,7 @@
|
||||
"version": "0.0.0",
|
||||
"description": "Data shared between Supabase apps",
|
||||
"main": "./index.ts",
|
||||
"types": "./index.ts",
|
||||
"scripts": {},
|
||||
"author": "",
|
||||
"license": "MIT"
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
export interface PricingInformation {
|
||||
id: string
|
||||
name: string
|
||||
nameBadge?: string
|
||||
costUnit?: string
|
||||
href: string
|
||||
priceLabel?: string
|
||||
priceMonthly: number | string
|
||||
warning?: string
|
||||
description: string
|
||||
preface: string
|
||||
features: string[]
|
||||
footer?: string
|
||||
cta: string
|
||||
}
|
||||
|
||||
export const plans: PricingInformation[] = [
|
||||
{
|
||||
id: 'tier_free',
|
||||
name: 'Free',
|
||||
nameBadge: '',
|
||||
costUnit: '/ month',
|
||||
href: 'https://supabase.com/dashboard/new/new-project',
|
||||
priceLabel: '',
|
||||
priceMonthly: 0,
|
||||
warning: 'Limit of 2 free projects',
|
||||
description: 'Perfect for passion projects & simple websites.',
|
||||
preface: 'Get started with:',
|
||||
features: [
|
||||
'Unlimited API requests',
|
||||
'Social OAuth providers',
|
||||
'Up to 500MB database space',
|
||||
'Up to 1GB file storage',
|
||||
'Up to 2GB bandwidth',
|
||||
'Up to 50MB file uploads',
|
||||
'Up to 50,000 monthly active users',
|
||||
'Up to 500K Edge Function invocations',
|
||||
'Up to 200 concurrent Realtime connections',
|
||||
'Up to 2 million Realtime messages',
|
||||
'1-day log retention',
|
||||
'Community support',
|
||||
],
|
||||
footer: 'Free projects are paused after 1 week of inactivity.',
|
||||
cta: 'Get Started',
|
||||
},
|
||||
{
|
||||
id: 'tier_pro',
|
||||
name: 'Pro',
|
||||
nameBadge: '',
|
||||
costUnit: '/ month',
|
||||
href: 'https://supabase.com/dashboard/new/new-project',
|
||||
priceLabel: 'From',
|
||||
warning: 'Usage based plan',
|
||||
priceMonthly: 25,
|
||||
description: 'For production applications with the option to scale.',
|
||||
features: [
|
||||
'No project pausing',
|
||||
'Daily backups stored for 7 days',
|
||||
'8GB database space included',
|
||||
'100GB file storage included',
|
||||
'50GB bandwidth included',
|
||||
'5GB file uploads included',
|
||||
'100,000 monthly active users included',
|
||||
'2M Edge Function invocations included',
|
||||
'500 concurrent Realtime connections included',
|
||||
'5 million Realtime messages included',
|
||||
'7-day log retention',
|
||||
'Email support',
|
||||
],
|
||||
footer:
|
||||
'Your cost control settings determine if you allow over-usage.',
|
||||
preface: 'Everything in the Free plan, plus:',
|
||||
cta: 'Get Started',
|
||||
},
|
||||
{
|
||||
id: 'tier_team',
|
||||
name: 'Team',
|
||||
nameBadge: 'New',
|
||||
costUnit: '/ month',
|
||||
href: 'https://forms.supabase.com/team',
|
||||
priceLabel: 'From',
|
||||
warning: 'Usage based plan',
|
||||
priceMonthly: 599,
|
||||
description: 'Collaborate with different permissions and access patterns.',
|
||||
features: [
|
||||
'Additional Organization member roles',
|
||||
'Daily backups stored for 14 days',
|
||||
'Standardised Security Questionnaire',
|
||||
'SOC2',
|
||||
'SSO for Supabase Dashboard',
|
||||
'Priority email support & SLAs',
|
||||
'28-day log retention',
|
||||
],
|
||||
footer: 'Additional fees apply for usage beyond included usage.',
|
||||
preface: 'Everything in the Pro plan, plus:',
|
||||
cta: 'Contact Us',
|
||||
},
|
||||
{
|
||||
id: 'tier_enterprise',
|
||||
name: 'Enterprise',
|
||||
href: 'https://forms.supabase.com/enterprise',
|
||||
description: 'For large-scale applications managing serious workloads.',
|
||||
features: [
|
||||
`Designated Support manager & SLAs`,
|
||||
`Enterprise OAuth providers`,
|
||||
`SSO/SAML`,
|
||||
`On-premise support`,
|
||||
`24×7×365 premium enterprise support`,
|
||||
`Private Slack channel`,
|
||||
],
|
||||
priceLabel: '',
|
||||
priceMonthly: 'Contact us',
|
||||
preface: '',
|
||||
footer: '',
|
||||
cta: 'Contact Us',
|
||||
},
|
||||
]
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"extends": "tsconfig/react-library.json",
|
||||
"include": ["."],
|
||||
"exclude": ["dist", "build", "node_modules"]
|
||||
}
|
||||
@@ -1,22 +1,5 @@
|
||||
import { IconArchive, IconCode, IconDatabase, IconKey, IconZap } from 'ui'
|
||||
|
||||
export const CANCELLATION_REASONS = [
|
||||
'Pricing',
|
||||
"My project isn't getting traction",
|
||||
'Poor customer service',
|
||||
'Missing feature',
|
||||
"I didn't see the value",
|
||||
"Supabase didn't meet my needs",
|
||||
'Dashboard is too complicated',
|
||||
'Postgres is too complicated',
|
||||
'Problem not solved',
|
||||
'Bug issues',
|
||||
'I decided to use something else',
|
||||
'My work has finished/discontinued',
|
||||
'I’m migrating to/starting a new project',
|
||||
'None of the above',
|
||||
]
|
||||
|
||||
export const USAGE_BASED_PRODUCTS = [
|
||||
{
|
||||
title: 'Database',
|
||||
|
||||
@@ -8,11 +8,11 @@ import { Button, Form, Input, Modal, IconArrowLeft, Alert } from 'ui'
|
||||
import { useFlag, useStore } from 'hooks'
|
||||
import { post, patch } from 'lib/common/fetch'
|
||||
import { API_URL, PROJECT_STATUS } from 'lib/constants'
|
||||
import { CANCELLATION_REASONS } from '../Billing.constants'
|
||||
import { UpdateSuccess } from '../'
|
||||
import { SubscriptionPreview } from '../Billing.types'
|
||||
import { StripeSubscription } from 'components/interfaces/Billing'
|
||||
import HCaptcha from '@hcaptcha/react-hcaptcha'
|
||||
import { CANCELLATION_REASONS } from 'components/interfaces/BillingV2/Billing.constants'
|
||||
|
||||
interface ExitSurveyProps {
|
||||
freeTier: any
|
||||
|
||||
@@ -5,6 +5,7 @@ import { Transition } from '@headlessui/react'
|
||||
import PlanCard from './Plans/PlanCard'
|
||||
import { formatTierOptions } from './PlanSelection.utils'
|
||||
import { Button, IconExternalLink } from 'ui'
|
||||
import PlanCardEnterprise from './Plans/PlanCardEnterprise'
|
||||
|
||||
interface Props {
|
||||
visible: boolean
|
||||
@@ -16,11 +17,7 @@ interface Props {
|
||||
const PlanSelection: FC<Props> = ({ visible, tiers, currentPlan, onSelectPlan }) => {
|
||||
const formattedTiers = formatTierOptions(tiers)
|
||||
|
||||
// [Kevin] TODO Remove after team plan is generally available
|
||||
const gridCols =
|
||||
formattedTiers.length === 3
|
||||
? 'gap-8 lg:grid-cols-3'
|
||||
: 'gap-4 lg:grid-cols-3 xl:grid-cols-billingWithTeam'
|
||||
const enterprisePlan = formattedTiers.find((it) => it.name === 'Enterprise')!
|
||||
|
||||
return (
|
||||
<Transition
|
||||
@@ -31,17 +28,26 @@ const PlanSelection: FC<Props> = ({ visible, tiers, currentPlan, onSelectPlan })
|
||||
>
|
||||
<div>
|
||||
<h4 className="text-lg">Change your project's subscription</h4>
|
||||
<div className={`grid py-8 grid-cols-2 ${gridCols}`}>
|
||||
{formattedTiers.map((plan) => {
|
||||
return (
|
||||
<PlanCard
|
||||
key={plan.id}
|
||||
plan={plan}
|
||||
currentPlan={currentPlan}
|
||||
onSelectPlan={() => onSelectPlan(plan)}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
<div className={`grid py-8 grid-cols-1 gap-8 md:grid-cols-3`}>
|
||||
{formattedTiers
|
||||
.filter((it) => it.name !== 'Enterprise')
|
||||
.map((plan) => {
|
||||
return (
|
||||
<PlanCard
|
||||
key={plan.id}
|
||||
plan={plan}
|
||||
currentPlan={currentPlan}
|
||||
onSelectPlan={() => onSelectPlan(plan)}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
<div>
|
||||
<PlanCardEnterprise
|
||||
plan={enterprisePlan}
|
||||
currentPlan={currentPlan}
|
||||
onSelectPlan={() => onSelectPlan(enterprisePlan)}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex justify-center items-center mt-4">
|
||||
<Link href="https://supabase.com/pricing">
|
||||
|
||||
@@ -20,13 +20,23 @@ const PlanCTAButton = ({ plan, currentPlan, onSelectPlan }: PlanCTAButtonProps)
|
||||
const isTeamTier = currentPlan?.supabase_prod_id === PRICING_TIER_PRODUCT_IDS.TEAM
|
||||
|
||||
const getButtonType = (plan: any, currentPlan: any) => {
|
||||
if (
|
||||
currentPlan.supabase_prod_id !== PRICING_TIER_PRODUCT_IDS.ENTERPRISE &&
|
||||
plan.name === 'Enterprise'
|
||||
) {
|
||||
return 'default'
|
||||
}
|
||||
|
||||
if (['Free tier', 'Free plan'].includes(plan.name)) {
|
||||
// Free is always default
|
||||
return 'default'
|
||||
} else if (currentPlan.prod_id === STRIPE_PRODUCT_IDS.FREE) {
|
||||
// If the current plan is free, other plans are primary
|
||||
return 'primary'
|
||||
} else if (currentPlan.prod_id === STRIPE_PRODUCT_IDS.TEAM && !['Team tier', 'Team plan'].includes(plan.name)) {
|
||||
} else if (
|
||||
currentPlan.prod_id === STRIPE_PRODUCT_IDS.TEAM &&
|
||||
!['Team tier', 'Team plan'].includes(plan.name)
|
||||
) {
|
||||
// Non-free plans are default (pro), when team plan is selected
|
||||
return 'default'
|
||||
}
|
||||
@@ -60,18 +70,10 @@ const PlanCTAButton = ({ plan, currentPlan, onSelectPlan }: PlanCTAButtonProps)
|
||||
}
|
||||
}
|
||||
|
||||
if (plan.id === STRIPE_PRODUCT_IDS.TEAM) {
|
||||
if (currentPlan.prod_id === STRIPE_PRODUCT_IDS.TEAM) {
|
||||
return 'Edit plan configuration'
|
||||
} else if (
|
||||
[STRIPE_PRODUCT_IDS.FREE, STRIPE_PRODUCT_IDS.PRO, STRIPE_PRODUCT_IDS.PAYG].includes(
|
||||
currentPlan.prod_id
|
||||
)
|
||||
) {
|
||||
return 'Upgrade to Team'
|
||||
} else {
|
||||
return 'Contact sales'
|
||||
}
|
||||
if (plan.id === STRIPE_PRODUCT_IDS.TEAM && currentPlan.prod_id === STRIPE_PRODUCT_IDS.TEAM) {
|
||||
return 'Edit plan configuration'
|
||||
} else {
|
||||
return 'Contact sales'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -103,6 +105,18 @@ const PlanCTAButton = ({ plan, currentPlan, onSelectPlan }: PlanCTAButtonProps)
|
||||
)
|
||||
}
|
||||
|
||||
if (isTeamTier && plan.name === 'Team plan') {
|
||||
return (
|
||||
<Link href="https://forms.supabase.com/team">
|
||||
<a>
|
||||
<Button disabled={disabled} type={type} block>
|
||||
{ctaText}
|
||||
</Button>
|
||||
</a>
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center space-y-2">
|
||||
<Tooltip.Root delayDuration={0}>
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import { PRICING_TIER_PRODUCT_IDS, STRIPE_PRODUCT_IDS } from 'lib/constants'
|
||||
import { STRIPE_PRODUCT_IDS } from 'lib/constants'
|
||||
import { FC } from 'react'
|
||||
import { IconCheck } from 'ui'
|
||||
|
||||
import { useFlag } from 'hooks'
|
||||
import PlanCTAButton from './PlanCTAButton'
|
||||
import { PRICING_META } from './Plans.Constants'
|
||||
|
||||
@@ -13,12 +12,7 @@ interface Props {
|
||||
}
|
||||
|
||||
const PlanCard: FC<Props> = ({ plan, currentPlan, onSelectPlan }) => {
|
||||
// Team tier is enabled when the flag is turned on OR the user is already on the team tier (manually assigned by us)
|
||||
const teamTierEnabled =
|
||||
useFlag('teamTier') || currentPlan?.supabase_prod_id === PRICING_TIER_PRODUCT_IDS.TEAM
|
||||
|
||||
const planMeta = PRICING_META[plan.id]
|
||||
const isEnterprise = plan.id === 'Enterprise'
|
||||
const isCurrentPlan =
|
||||
plan.id === currentPlan?.prod_id ||
|
||||
(plan.id === STRIPE_PRODUCT_IDS.PRO && currentPlan?.prod_id === STRIPE_PRODUCT_IDS.PRO) ||
|
||||
@@ -26,35 +20,12 @@ const PlanCard: FC<Props> = ({ plan, currentPlan, onSelectPlan }) => {
|
||||
|
||||
return (
|
||||
<div
|
||||
className={[
|
||||
'px-6 py-6 border rounded flex flex-col justify-between',
|
||||
`${isEnterprise ? 'bg-scale-200' : 'bg-scale-300'}`,
|
||||
`${isEnterprise && teamTierEnabled ? 'lg:col-span-3 xl:col-auto' : ''}`,
|
||||
].join(' ')}
|
||||
className={['px-6 py-6 border rounded flex flex-col justify-between bg-scale-300'].join(' ')}
|
||||
>
|
||||
<div
|
||||
className={[
|
||||
'space-y-8',
|
||||
`${isEnterprise ? 'flex flex-col xl:space-x-0 xl:flex-col' : ''}`,
|
||||
`${isEnterprise && teamTierEnabled ? 'lg:flex-row lg:space-x-24' : ''}`,
|
||||
].join(' ')}
|
||||
>
|
||||
<div
|
||||
className={[
|
||||
'space-y-4',
|
||||
`${isEnterprise ? 'xl:w-auto' : ''}`,
|
||||
`${isEnterprise && teamTierEnabled ? 'xl:w-auto' : ''}`,
|
||||
].join(' ')}
|
||||
>
|
||||
<div className={['space-y-8'].join(' ')}>
|
||||
<div className={['space-y-4'].join(' ')}>
|
||||
<div className="flex items-center space-x-2">
|
||||
<p
|
||||
className={[
|
||||
'text-brand-900 text-sm',
|
||||
`${isEnterprise ? 'lg:text-base xl:text-sm font-bold font-mono uppercase' : ''}`,
|
||||
].join(' ')}
|
||||
>
|
||||
{planMeta.name}
|
||||
</p>
|
||||
<p className={['text-brand-900 text-sm'].join(' ')}>{planMeta.name}</p>
|
||||
{isCurrentPlan ? (
|
||||
<div className="text-xs bg-scale-500 text-scale-1000 rounded px-2 py-0.5">
|
||||
Current plan
|
||||
@@ -66,16 +37,7 @@ const PlanCard: FC<Props> = ({ plan, currentPlan, onSelectPlan }) => {
|
||||
)}
|
||||
</div>
|
||||
<p className="text-sm text-scale-1100">{planMeta.description}</p>
|
||||
<div
|
||||
className={[
|
||||
'space-y-2',
|
||||
`${
|
||||
isEnterprise
|
||||
? 'pb-6 border-b lg:pt-6 lg:border-b-0 lg:border-t xl:pt-0 xl:pb-6 xl:border-t-0 xl:border-b'
|
||||
: 'border-b border-t py-8'
|
||||
}`,
|
||||
].join(' ')}
|
||||
>
|
||||
<div className={['space-y-2', 'border-b border-t py-8'].join(' ')}>
|
||||
{planMeta.priceMonthly !== undefined ? (
|
||||
<>
|
||||
<p className="text-sm text-scale-1000">Starting from</p>
|
||||
@@ -92,15 +54,9 @@ const PlanCard: FC<Props> = ({ plan, currentPlan, onSelectPlan }) => {
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className={`${isEnterprise ? 'lg:space-y-0 lg:!mt-0 xl:space-y-4' : 'space-y-4'}`}>
|
||||
<div className="space-y-4">
|
||||
<p className="text-xs text-scale-1000">{planMeta.preface}</p>
|
||||
<ul
|
||||
role="list"
|
||||
className={[
|
||||
`${isEnterprise ? 'xl:gap-0 xl:columns-1' : ''}`,
|
||||
`${isEnterprise && teamTierEnabled ? 'lg:gap-16 lg:columns-2' : ''}`,
|
||||
].join(' ')}
|
||||
>
|
||||
<ul role="list">
|
||||
{planMeta.features.map((feature) => (
|
||||
<li key={feature} className="flex py-2">
|
||||
<div className="w-[12px]">
|
||||
@@ -116,35 +72,34 @@ const PlanCard: FC<Props> = ({ plan, currentPlan, onSelectPlan }) => {
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
{!isEnterprise && (
|
||||
<div className="space-y-4 !mt-20">
|
||||
<div className="space-y-2">
|
||||
<p className="text-xs text-scale-1000">{planMeta.scale}</p>
|
||||
|
||||
{planMeta.name === 'Pro' && currentPlan?.prod_id === STRIPE_PRODUCT_IDS.PRO && (
|
||||
<div>
|
||||
<p className="text-sm text-scale-1100">
|
||||
Need more? Turn off your spend cap to Pay As You Grow
|
||||
</p>
|
||||
<p className="text-xs text-scale-1000">
|
||||
Additional fees apply for spend beyond the limits above
|
||||
</p>{' '}
|
||||
</div>
|
||||
)}
|
||||
<div className="space-y-4 !mt-20">
|
||||
<div className="space-y-2">
|
||||
<p className="text-xs text-scale-1000">{planMeta.scale}</p>
|
||||
|
||||
{planMeta.name === 'Pro' && currentPlan?.prod_id === STRIPE_PRODUCT_IDS.PAYG && (
|
||||
<div>
|
||||
<p className="text-sm text-scale-1100">Spend cap is currently disabled</p>
|
||||
<p className="text-xs text-scale-1000">
|
||||
You will be charged for usage beyond the plan limits
|
||||
</p>{' '}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{planMeta.name === 'Pro' && currentPlan?.prod_id === STRIPE_PRODUCT_IDS.PRO && (
|
||||
<div>
|
||||
<p className="text-sm text-scale-1100">
|
||||
Need more? Turn off your spend cap to Pay As You Grow
|
||||
</p>
|
||||
<p className="text-xs text-scale-1000">
|
||||
Additional fees apply for spend beyond the limits above
|
||||
</p>{' '}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<PlanCTAButton plan={plan} currentPlan={currentPlan} onSelectPlan={onSelectPlan} />
|
||||
{planMeta.name === 'Pro' && currentPlan?.prod_id === STRIPE_PRODUCT_IDS.PAYG && (
|
||||
<div>
|
||||
<p className="text-sm text-scale-1100">Spend cap is currently disabled</p>
|
||||
<p className="text-xs text-scale-1000">
|
||||
You will be charged for usage beyond the plan limits
|
||||
</p>{' '}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<PlanCTAButton plan={plan} currentPlan={currentPlan} onSelectPlan={onSelectPlan} />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
import { STRIPE_PRODUCT_IDS } from 'lib/constants'
|
||||
import { FC } from 'react'
|
||||
import { IconCheck } from 'ui'
|
||||
|
||||
import PlanCTAButton from './PlanCTAButton'
|
||||
import { PRICING_META } from './Plans.Constants'
|
||||
|
||||
interface Props {
|
||||
plan: any
|
||||
currentPlan: any
|
||||
onSelectPlan: () => void
|
||||
}
|
||||
|
||||
const PlanCardEnterprise: FC<Props> = ({ plan, currentPlan, onSelectPlan }) => {
|
||||
const planMeta = PRICING_META[plan.id]
|
||||
const isCurrentPlan =
|
||||
plan.id === currentPlan?.prod_id ||
|
||||
(plan.id === STRIPE_PRODUCT_IDS.PRO && currentPlan?.prod_id === STRIPE_PRODUCT_IDS.PRO) ||
|
||||
(plan.id === STRIPE_PRODUCT_IDS.PRO && currentPlan?.prod_id === STRIPE_PRODUCT_IDS.PAYG)
|
||||
|
||||
return (
|
||||
<div className="px-6 py-6 border rounded flex-col bg-scale-200 flex">
|
||||
<div className="space-y-8">
|
||||
<div className=" grid grid-cols-3 space-x-8">
|
||||
<div className="space-y-4 pr-14">
|
||||
<div className="flex items-center ">
|
||||
<p className="text-brand-900 text-sm">{planMeta.name}</p>
|
||||
{isCurrentPlan ? (
|
||||
<div className="text-xs bg-scale-500 text-scale-1000 rounded px-2 py-0.5">
|
||||
Current plan
|
||||
</div>
|
||||
) : planMeta.new ? (
|
||||
<div className="text-xs bg-brand-400 text-brand-900 rounded px-2 py-0.5">New</div>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-sm text-scale-1100">{planMeta.description}</p>
|
||||
<div className="space-y-2 border-t pb-6 lg:pt-6 xl:pt-0 xl:pb-6">
|
||||
<PlanCTAButton plan={plan} currentPlan={currentPlan} onSelectPlan={onSelectPlan} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-4 lg:col-span-2">
|
||||
<p className="text-xs text-scale-1000">{planMeta.preface}</p>
|
||||
<ul role="list" className="text-xs text-scale-1000 md:grid md:grid-cols-2 md:gap-x-10">
|
||||
{planMeta.features.map((feature) => (
|
||||
<li key={feature} className="flex items-center py-2 first:mt-0">
|
||||
<IconCheck
|
||||
className="text-brand-900 h-4 w-4"
|
||||
aria-hidden="true"
|
||||
strokeWidth={3}
|
||||
/>
|
||||
<span className="dark:text-scale-1200 mb-0 ml-3 ">{feature}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default PlanCardEnterprise
|
||||
@@ -1,11 +1,7 @@
|
||||
import { STRIPE_PRODUCT_IDS } from 'lib/constants'
|
||||
|
||||
/**
|
||||
* @mildtomato same plan metadata is also in /www website
|
||||
* would be good if this constant was shared across apps
|
||||
*
|
||||
* https://github.com/supabase/supabase/blob/master/studio/components/interfaces/Billing/PlanSelection/Plans/Plans.Constants.ts
|
||||
*/
|
||||
// This file will get deleted soon and superseded by components in BillingV2/*
|
||||
|
||||
export const PRICING_META = {
|
||||
[STRIPE_PRODUCT_IDS.FREE]: {
|
||||
id: 'tier_free',
|
||||
@@ -13,18 +9,20 @@ export const PRICING_META = {
|
||||
name: 'Free',
|
||||
description: 'Perfect for passion projects & simple websites.',
|
||||
priceMonthly: 0,
|
||||
priceUnit: 'per month per project',
|
||||
priceUnit: '/ month',
|
||||
warning: 'Limit of 2 free projects',
|
||||
preface: 'Get started with:',
|
||||
features: [
|
||||
'Unlimited API requests',
|
||||
'Social OAuth providers',
|
||||
'Up to 500MB database & 1GB file storage',
|
||||
'Up to 500MB database space',
|
||||
'Up to 1GB file storage',
|
||||
'Up to 2GB bandwidth',
|
||||
'Up to 50MB file uploads',
|
||||
'50,000 monthly active users',
|
||||
'Up to 50,000 monthly active users',
|
||||
'Up to 500K Edge Function invocations',
|
||||
'200 concurrent Realtime connections',
|
||||
'2 million Realtime messages',
|
||||
'Up to 200 concurrent Realtime connections',
|
||||
'Up to 2 million Realtime messages',
|
||||
'1-day log retention',
|
||||
'Community support',
|
||||
],
|
||||
@@ -36,21 +34,22 @@ export const PRICING_META = {
|
||||
name: 'Pro',
|
||||
description: 'For production applications with the option to scale.',
|
||||
priceMonthly: 25,
|
||||
priceUnit: 'per month per project',
|
||||
warning: '+ any additional usage',
|
||||
priceUnit: '/ month',
|
||||
warning: 'Usage based plan',
|
||||
preface: 'Everything in the Free plan, plus:',
|
||||
features: [
|
||||
'No project pausing',
|
||||
'8GB database & 100GB file storage',
|
||||
'50GB bandwidth',
|
||||
'5GB file uploads',
|
||||
'100,000 monthly active users',
|
||||
'2M Edge Function invocations',
|
||||
'500 concurrent Realtime connections',
|
||||
'5 million Realtime messages',
|
||||
'Daily backups stored for 7 days',
|
||||
'8GB database space included',
|
||||
'100GB file storage included',
|
||||
'50GB bandwidth included',
|
||||
'5GB file uploads included',
|
||||
'100,000 monthly active users included',
|
||||
'2M Edge Function invocations included',
|
||||
'500 concurrent Realtime connections included',
|
||||
'5 million Realtime messages included',
|
||||
'7-day log retention',
|
||||
'Email support',
|
||||
'Daily backups',
|
||||
],
|
||||
},
|
||||
[STRIPE_PRODUCT_IDS.TEAM]: {
|
||||
@@ -59,20 +58,17 @@ export const PRICING_META = {
|
||||
name: 'Team',
|
||||
description: 'Collaborate with different permissions and access patterns.',
|
||||
priceMonthly: 599,
|
||||
priceUnit: 'per month per organization',
|
||||
warning: '+ usage',
|
||||
preface: 'The following features will apply to all projects within the organization:',
|
||||
priceUnit: '/ month',
|
||||
warning: 'Usage based plan',
|
||||
preface: 'Everything in the Pro plan, plus:',
|
||||
features: [
|
||||
'Usage-based pricing',
|
||||
'100,000 monthly active users included',
|
||||
'Organization member roles (ABAC)',
|
||||
'Additional Organization member roles',
|
||||
'Daily backups stored for 14 days',
|
||||
'Standardised Security Questionnaire',
|
||||
'SOC2',
|
||||
'SSO for Supabase Dashboard',
|
||||
'Priority email support & SLAs',
|
||||
'1 XS compute instance',
|
||||
'14 day backups',
|
||||
'28 day log retention',
|
||||
'28-day log retention',
|
||||
],
|
||||
},
|
||||
Enterprise: {
|
||||
@@ -85,14 +81,10 @@ export const PRICING_META = {
|
||||
features: [
|
||||
`Designated Support manager & SLAs`,
|
||||
`Enterprise OAuth providers`,
|
||||
`SSO/ SAML`,
|
||||
`SOC2`,
|
||||
`Custom contracts & invoicing`,
|
||||
`SSO/SAML`,
|
||||
`On-premise support`,
|
||||
`24×7×365 premium enterprise support`,
|
||||
`Custom Security questionnaires`,
|
||||
`Private Slack channel`,
|
||||
`Uptime SLA`,
|
||||
],
|
||||
},
|
||||
}
|
||||
@@ -48,10 +48,8 @@ const TeamUpgrade = ({
|
||||
const { app, ui } = useStore()
|
||||
const router = useRouter()
|
||||
|
||||
// Team tier is enabled when the flag is turned on OR the user is already on the team tier (manually assigned by us)
|
||||
const userIsOnTeamTier =
|
||||
currentSubscription?.tier?.supabase_prod_id === PRICING_TIER_PRODUCT_IDS.TEAM
|
||||
const teamTierEnabled = useFlag('teamTier') || userIsOnTeamTier
|
||||
// Team tier is enabled when the user is already on the team tier (manually assigned by us)
|
||||
const teamTierEnabled = currentSubscription?.tier?.supabase_prod_id === PRICING_TIER_PRODUCT_IDS.TEAM
|
||||
|
||||
const [captchaToken, setCaptchaToken] = useState<string | null>(null)
|
||||
const captchaRef = useRef<HCaptcha>(null)
|
||||
|
||||
@@ -1 +1,18 @@
|
||||
export const USAGE_APPROACHING_THRESHOLD = 0.8
|
||||
|
||||
export const CANCELLATION_REASONS = [
|
||||
'Pricing',
|
||||
"My project isn't getting traction",
|
||||
'Poor customer service',
|
||||
'Missing feature',
|
||||
"I didn't see the value",
|
||||
"Supabase didn't meet my needs",
|
||||
'Dashboard is too complicated',
|
||||
'Postgres is too complicated',
|
||||
'Problem not solved',
|
||||
'Bug issues',
|
||||
'I decided to use something else',
|
||||
'My work has finished/discontinued',
|
||||
'I’m migrating to/starting a new project',
|
||||
'None of the above',
|
||||
]
|
||||
@@ -1,66 +1,60 @@
|
||||
import clsx from 'clsx'
|
||||
import { Button, IconCheck } from 'ui'
|
||||
import { PricingInformation } from './Tier.constants'
|
||||
import Link from 'next/link'
|
||||
import { PricingInformation } from 'shared-data'
|
||||
|
||||
export interface EnterpriseCardProps {
|
||||
plan: PricingInformation
|
||||
isCurrentPlan: boolean
|
||||
isTeamTierEnabled: boolean
|
||||
}
|
||||
|
||||
const EnterpriseCard = ({ plan, isCurrentPlan, isTeamTierEnabled }: EnterpriseCardProps) => {
|
||||
const EnterpriseCard = ({ plan, isCurrentPlan }: EnterpriseCardProps) => {
|
||||
return (
|
||||
<div
|
||||
key={plan.id}
|
||||
className={clsx(
|
||||
'flex border rounded-md bg-scale-200',
|
||||
isTeamTierEnabled
|
||||
? 'px-12 py-8 col-span-12 justify-between'
|
||||
: 'px-6 py-6 col-span-4 flex-col'
|
||||
'grid grid-cols-1 md:grid-cols-3 border rounded-md bg-scale-200',
|
||||
'py-4 col-span-12 justify-between gap-x-8'
|
||||
)}
|
||||
>
|
||||
<div>
|
||||
<div className="flex flex-col justify-center px-4">
|
||||
<div className="flex items-center space-x-2">
|
||||
<p className={clsx('text-brand-900 text-sm uppercase')}>{plan.name}</p>
|
||||
{isCurrentPlan ? (
|
||||
<div className="text-xs bg-scale-500 text-scale-1000 rounded px-2 py-0.5">
|
||||
Current plan
|
||||
</div>
|
||||
) : plan.new ? (
|
||||
<div className="text-xs bg-brand-400 text-brand-900 rounded px-2 py-0.5">New</div>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
) : plan.nameBadge ? (
|
||||
<div className="text-xs bg-brand-400 text-brand-900 rounded px-2 py-0.5">
|
||||
{plan.nameBadge}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
<p className="text-sm mt-2 mb-4">{plan.preface}</p>
|
||||
<p className="text-sm mt-2 mb-4">{plan.description}</p>
|
||||
|
||||
<Link href="https://supabase.com/contact/enterprise">
|
||||
<a target="blank" rel="noreferrer">
|
||||
<Button block={!isTeamTierEnabled} type="default">
|
||||
Contact Sales Team
|
||||
</Button>
|
||||
</a>
|
||||
</Link>
|
||||
<a href={plan.href} className="hidden md:block" target="_blank">
|
||||
<Button block type="default" size="tiny">
|
||||
{plan.cta}
|
||||
</Button>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
{!isTeamTierEnabled && <div className="border-t my-6" />}
|
||||
<div className="flex flex-col justify-center col-span-2 px-4 md:px-0">
|
||||
<ul role="list" className="text-xs text-scale-1000 md:grid md:grid-cols-2 md:gap-x-10">
|
||||
{plan.features.map((feature) => (
|
||||
<li key={feature} className="flex items-center py-2 first:mt-0">
|
||||
<IconCheck className="text-brand-900 h-4 w-4" aria-hidden="true" strokeWidth={3} />
|
||||
<span className="dark:text-scale-1200 mb-0 ml-3 ">{feature}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
|
||||
<ul role="list" className={clsx(isTeamTierEnabled && 'pr-12')}>
|
||||
{plan.features.map((feature) => (
|
||||
<li key={feature} className="flex py-2">
|
||||
<div className="w-[12px]">
|
||||
<IconCheck
|
||||
className="h-3 w-3 text-brand-900 translate-y-[2.5px]"
|
||||
aria-hidden="true"
|
||||
strokeWidth={3}
|
||||
/>
|
||||
</div>
|
||||
<p className="ml-3 text-xs text-scale-1100">{feature}</p>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<a href={plan.href} className="visible md:hidden mt-8" target="_blank">
|
||||
<Button block type="default" size="tiny">
|
||||
{plan.cta}
|
||||
</Button>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -10,8 +10,8 @@ import { includes, without } from 'lodash'
|
||||
import { useRouter } from 'next/router'
|
||||
import { useReducer, useRef, useState } from 'react'
|
||||
import { Button, Input, Modal } from 'ui'
|
||||
import { CANCELLATION_REASONS } from './Tier.constants'
|
||||
import ProjectUpdateDisabledTooltip from '../../ProjectUpdateDisabledTooltip'
|
||||
import { CANCELLATION_REASONS } from '../../Billing.constants'
|
||||
|
||||
export interface ExitSurveyModalProps {
|
||||
visible: boolean
|
||||
|
||||
@@ -1,94 +0,0 @@
|
||||
export interface PricingInformation {
|
||||
id: string
|
||||
new: boolean
|
||||
name: string
|
||||
preface?: string
|
||||
features: string[]
|
||||
footer?: string
|
||||
}
|
||||
|
||||
// This only contains the meta information of plans like description and features, prices are from the API
|
||||
// Also ideally shift this to common folder (e.g packages/data)
|
||||
export const SUBSCRIPTION_PLANS: PricingInformation[] = [
|
||||
{
|
||||
id: 'tier_free',
|
||||
new: false,
|
||||
name: 'Free',
|
||||
preface: undefined,
|
||||
features: [
|
||||
'Social OAuth providers',
|
||||
'Up to 500MB database & 1GB file storage',
|
||||
'Up to 2GB bandwidth',
|
||||
'Up to 50MB file uploads',
|
||||
'50,000 monthly active users',
|
||||
'Up to 500K Edge Function invocations',
|
||||
'200 concurrent Realtime connections',
|
||||
'2 million Realtime messages',
|
||||
'1-day log retention',
|
||||
'Community support',
|
||||
],
|
||||
footer: undefined,
|
||||
},
|
||||
{
|
||||
id: 'tier_pro',
|
||||
new: false,
|
||||
name: 'Pro',
|
||||
preface: undefined,
|
||||
features: [
|
||||
'No project pausing',
|
||||
'8GB database & 100GB file storage',
|
||||
'50GB bandwidth',
|
||||
'5GB file uploads',
|
||||
'100,000 monthly active users',
|
||||
'2M Edge Function invocations',
|
||||
'500 concurrent Realtime connections',
|
||||
'5 million Realtime messages',
|
||||
'7-day log retention',
|
||||
'Email support',
|
||||
'Daily backups',
|
||||
],
|
||||
footer:
|
||||
'Your cost control settings determine if you wish to pay for anything more than the above.',
|
||||
},
|
||||
{
|
||||
id: 'tier_team',
|
||||
new: true,
|
||||
name: 'Team',
|
||||
preface: 'Everything in Pro, plus:',
|
||||
features: [
|
||||
'Organization member roles (ABAC)',
|
||||
'Standardised Security Questionnaire',
|
||||
'SOC2',
|
||||
'SSO for Supabase Dashboard',
|
||||
'Priority email support & SLAs',
|
||||
'14 day backups',
|
||||
'28 day log retention',
|
||||
],
|
||||
footer: undefined,
|
||||
},
|
||||
{
|
||||
id: 'tier_enterprise',
|
||||
new: false,
|
||||
name: 'Enterprise',
|
||||
preface: 'For large-scale applications managing serious workloads',
|
||||
features: [`Designated Support manager & SLAs`, `SSO/ SAML`, `Custom contracts & invoicing`],
|
||||
footer: undefined,
|
||||
},
|
||||
]
|
||||
|
||||
export const CANCELLATION_REASONS = [
|
||||
'Pricing',
|
||||
"My project isn't getting traction",
|
||||
'Poor customer service',
|
||||
'Missing feature',
|
||||
"I didn't see the value",
|
||||
"Supabase didn't meet my needs",
|
||||
'Dashboard is too complicated',
|
||||
'Postgres is too complicated',
|
||||
'Problem not solved',
|
||||
'Bug issues',
|
||||
'I decided to use something else',
|
||||
'My work has finished/discontinued',
|
||||
'I’m migrating to/starting a new project',
|
||||
'None of the above',
|
||||
]
|
||||
@@ -5,9 +5,7 @@ import { useFreeProjectLimitCheckQuery } from 'data/organizations/free-project-l
|
||||
import { useProjectAddonsQuery } from 'data/subscriptions/project-addons-query'
|
||||
import { useProjectPlansQuery } from 'data/subscriptions/project-plans-query'
|
||||
import { useProjectSubscriptionUpdateMutation } from 'data/subscriptions/project-subscription-update-mutation'
|
||||
import { useProjectSubscriptionV2Query } from 'data/subscriptions/project-subscription-v2-query'
|
||||
import { useFlag, useStore } from 'hooks'
|
||||
import { PRICING_TIER_PRODUCT_IDS } from 'lib/constants'
|
||||
import { useStore } from 'hooks'
|
||||
import Link from 'next/link'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useSubscriptionPageStateSnapshot } from 'state/subscription-page'
|
||||
@@ -16,7 +14,7 @@ import EnterpriseCard from './EnterpriseCard'
|
||||
import ExitSurveyModal from './ExitSurveyModal'
|
||||
import MembersExceedLimitModal from './MembersExceedLimitModal'
|
||||
import PaymentMethodSelection from './PaymentMethodSelection'
|
||||
import { SUBSCRIPTION_PLANS } from './Tier.constants'
|
||||
import { plans as subscriptionsPlans } from 'shared-data/plans'
|
||||
|
||||
const TierUpdateSidePanel = () => {
|
||||
const { ui } = useStore()
|
||||
@@ -36,16 +34,13 @@ const TierUpdateSidePanel = () => {
|
||||
const { data: plans, isLoading: isLoadingPlans } = useProjectPlansQuery({ projectRef })
|
||||
const { data: addons } = useProjectAddonsQuery({ projectRef })
|
||||
const { data: membersExceededLimit } = useFreeProjectLimitCheckQuery({ slug })
|
||||
const { data: subscription, isLoading } = useProjectSubscriptionV2Query({ projectRef })
|
||||
const { mutateAsync: updateSubscriptionTier } = useProjectSubscriptionUpdateMutation()
|
||||
|
||||
const availablePlans = plans ?? []
|
||||
const subscriptionAddons = addons?.selected_addons ?? []
|
||||
const userIsOnTeamTier = subscription?.plan?.id === 'team'
|
||||
|
||||
const teamTierEnabled = useFlag('teamTier') || userIsOnTeamTier
|
||||
const hasMembersExceedingFreeTierLimit = (membersExceededLimit || []).length > 0
|
||||
const selectedTierMeta = SUBSCRIPTION_PLANS.find((tier) => tier.id === selectedTier)
|
||||
const selectedTierMeta = subscriptionsPlans.find((tier) => tier.id === selectedTier)
|
||||
const selectedPlanMeta = availablePlans.find(
|
||||
(plan) => plan.id === selectedTier?.split('tier_')[1]
|
||||
)
|
||||
@@ -118,22 +113,15 @@ const TierUpdateSidePanel = () => {
|
||||
>
|
||||
<SidePanel.Content>
|
||||
<div className="py-6 grid grid-cols-12 gap-3">
|
||||
{SUBSCRIPTION_PLANS.map((plan) => {
|
||||
{subscriptionsPlans.map((plan) => {
|
||||
const planMeta = availablePlans.find((p) => p.id === plan.id.split('tier_')[1])
|
||||
const tierMeta = subscriptionsPlans.find((it) => it.id === plan.id)
|
||||
const price = planMeta?.price ?? 0
|
||||
const isDowngradeOption = planMeta?.change_type === 'downgrade'
|
||||
const isCurrentPlan = planMeta?.is_current ?? false
|
||||
|
||||
if (plan.id === 'tier_team' && !teamTierEnabled) return null
|
||||
if (plan.id === 'tier_enterprise') {
|
||||
return (
|
||||
<EnterpriseCard
|
||||
key={plan.id}
|
||||
plan={plan}
|
||||
isCurrentPlan={isCurrentPlan}
|
||||
isTeamTierEnabled={teamTierEnabled}
|
||||
/>
|
||||
)
|
||||
return <EnterpriseCard key={plan.id} plan={plan} isCurrentPlan={isCurrentPlan} />
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -141,7 +129,7 @@ const TierUpdateSidePanel = () => {
|
||||
key={plan.id}
|
||||
className={clsx(
|
||||
'border rounded-md px-4 py-4 flex flex-col items-start justify-between',
|
||||
teamTierEnabled && plan.id === 'tier_enterprise' ? 'col-span-12' : 'col-span-4',
|
||||
plan.id === 'tier_enterprise' ? 'col-span-12' : 'col-span-12 md:col-span-4',
|
||||
plan.id === 'tier_enterprise' ? 'bg-scale-200' : 'bg-scale-300'
|
||||
)}
|
||||
>
|
||||
@@ -152,9 +140,9 @@ const TierUpdateSidePanel = () => {
|
||||
<div className="text-xs bg-scale-500 text-scale-1000 rounded px-2 py-0.5">
|
||||
Current plan
|
||||
</div>
|
||||
) : plan.new ? (
|
||||
) : plan.nameBadge ? (
|
||||
<div className="text-xs bg-brand-400 text-brand-900 rounded px-2 py-0.5">
|
||||
New
|
||||
{plan.nameBadge}
|
||||
</div>
|
||||
) : (
|
||||
<></>
|
||||
@@ -169,32 +157,33 @@ const TierUpdateSidePanel = () => {
|
||||
) : (
|
||||
<p className="text-scale-1200 text-lg">${price}</p>
|
||||
)}
|
||||
<p className="text-scale-1000 text-sm">per month</p>
|
||||
<p className="text-scale-1000 text-sm">{tierMeta?.costUnit}</p>
|
||||
</div>
|
||||
<div
|
||||
className={clsx(
|
||||
'flex mt-1 mb-4',
|
||||
plan.id !== PRICING_TIER_PRODUCT_IDS.TEAM && 'opacity-0'
|
||||
)}
|
||||
>
|
||||
<div className="text-xs bg-brand-400 text-brand-900 rounded px-2 py-0.5">
|
||||
Usage-based plan
|
||||
<div className={clsx('flex mt-1 mb-4', !tierMeta?.warning && 'opacity-0')}>
|
||||
<div className="bg-scale-200 text-brand-1100 border shadow-sm rounded-md bg-opacity-30 py-0.5 px-2 text-xs">
|
||||
{tierMeta?.warning}
|
||||
</div>
|
||||
</div>
|
||||
{isCurrentPlan ? (
|
||||
<Button block disabled type="default">
|
||||
Current plan
|
||||
</Button>
|
||||
) : (
|
||||
) : plan.id !== 'tier_team' ? (
|
||||
<Button
|
||||
block
|
||||
type={isDowngradeOption ? 'default' : 'primary'}
|
||||
loading={isLoading}
|
||||
disabled={isLoading}
|
||||
onClick={() => setSelectedTier(plan.id as any)}
|
||||
>
|
||||
{isDowngradeOption ? 'Downgrade' : 'Upgrade'} to {plan.name}
|
||||
</Button>
|
||||
) : (
|
||||
<Link href={plan.href} passHref className="hidden md:block">
|
||||
<a target="_blank">
|
||||
<Button block type="primary">
|
||||
Contact Us
|
||||
</Button>
|
||||
</a>
|
||||
</Link>
|
||||
)}
|
||||
|
||||
<div className="border-t my-6" />
|
||||
|
||||
+1
-1
@@ -8,7 +8,7 @@ import { useStore, checkPermissions } from 'hooks'
|
||||
import { API_URL, PRICING_TIER_PRODUCT_IDS } from 'lib/constants'
|
||||
import { delete_, post } from 'lib/common/fetch'
|
||||
import TextConfirmModal from 'components/ui/Modals/TextConfirmModal'
|
||||
import { CANCELLATION_REASONS } from 'components/interfaces/Billing/Billing.constants'
|
||||
import { CANCELLATION_REASONS } from 'components/interfaces/BillingV2/Billing.constants'
|
||||
|
||||
interface Props {
|
||||
type?: 'danger' | 'default'
|
||||
|
||||
@@ -17,10 +17,6 @@ const UpgradePrompt: React.FC<Props> = ({ show, setShowUpgradePrompt, subscripti
|
||||
const router = useRouter()
|
||||
const { ref } = router.query
|
||||
|
||||
// Team plan is enabled when the flag is turned on OR the user is already on the team plan (manually assigned by us)
|
||||
const userIsOnTeamTier = subscription?.tier?.supabase_prod_id === PRICING_TIER_PRODUCT_IDS.TEAM
|
||||
const teamTierEnabled = useFlag('teamTier') || userIsOnTeamTier
|
||||
|
||||
return (
|
||||
<Modal
|
||||
hideFooter
|
||||
@@ -51,12 +47,10 @@ const UpgradePrompt: React.FC<Props> = ({ show, setShowUpgradePrompt, subscripti
|
||||
<p className="w-[40%] text-sm">Pro</p>
|
||||
<p className="w-[60%] text-sm">{TIER_QUERY_LIMITS.PRO.text}</p>
|
||||
</div>
|
||||
{teamTierEnabled && (
|
||||
<div className="flex items-center px-4 py-1">
|
||||
<p className="w-[40%] text-sm">Team</p>
|
||||
<p className="w-[60%] text-sm">{TIER_QUERY_LIMITS.TEAM.text}</p>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex items-center px-4 py-1">
|
||||
<p className="w-[40%] text-sm">Team</p>
|
||||
<p className="w-[60%] text-sm">{TIER_QUERY_LIMITS.TEAM.text}</p>
|
||||
</div>
|
||||
<div className="flex items-center px-4 py-1">
|
||||
<p className="w-[40%] text-sm">Enterprise</p>
|
||||
<p className="w-[60%] text-sm">{TIER_QUERY_LIMITS.ENTERPRISE.text}</p>
|
||||
|
||||
@@ -4,7 +4,7 @@ const withBundleAnalyzer = require('@next/bundle-analyzer')({
|
||||
})
|
||||
|
||||
// this is required to use shared packages in the packages directory
|
||||
const withTM = require('next-transpile-modules')(['ui', 'common'])
|
||||
const withTM = require('next-transpile-modules')(['ui', 'common', 'shared-data'])
|
||||
|
||||
// Required for nextjs standalone build
|
||||
const path = require('path')
|
||||
|
||||
@@ -3,7 +3,7 @@ import { useRouter } from 'next/router'
|
||||
import { observer } from 'mobx-react-lite'
|
||||
import { Button, Modal } from 'ui'
|
||||
|
||||
import { useFlag, useStore } from 'hooks'
|
||||
import { useStore } from 'hooks'
|
||||
import { useFreeProjectLimitCheckQuery } from 'data/organizations/free-project-limit-check-query'
|
||||
import { get } from 'lib/common/fetch'
|
||||
import { API_URL, PRICING_TIER_PRODUCT_IDS, STRIPE_PRODUCT_IDS } from 'lib/constants'
|
||||
@@ -103,10 +103,6 @@ const BillingUpdate: NextPageWithLayout = () => {
|
||||
}
|
||||
}
|
||||
|
||||
// Team tier is enabled when the flag is turned on OR the user is already on the team tier (manually assigned by us)
|
||||
const userIsOnTeamTier = subscription?.tier?.supabase_prod_id === PRICING_TIER_PRODUCT_IDS.TEAM
|
||||
const teamTierEnabled = useFlag('teamTier') || userIsOnTeamTier
|
||||
|
||||
if (isLoadingProducts || isEnterprise) {
|
||||
return (
|
||||
<div className="flex items-center justify-center w-full h-full">
|
||||
@@ -115,13 +111,11 @@ const BillingUpdate: NextPageWithLayout = () => {
|
||||
)
|
||||
}
|
||||
|
||||
const productTiers = (products?.tiers ?? []).filter(
|
||||
(tier) => teamTierEnabled || tier.id !== STRIPE_PRODUCT_IDS.TEAM
|
||||
)
|
||||
const productTiers = (products?.tiers ?? [])
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={`mx-auto my-10 ${teamTierEnabled ? 'max-w-[90vw]' : 'max-w-[80vw]'}`}>
|
||||
<div className={`mx-auto my-10 max-w-[90vw] xl:max-w-[80vw]`}>
|
||||
<PlanSelection
|
||||
visible={!selectedPlan || (selectedPlan && showConfirmDowngrade)}
|
||||
tiers={productTiers}
|
||||
|
||||
Reference in new issue
Block a user