From 42e2e88fba3567002c047111394d9025c7093c09 Mon Sep 17 00:00:00 2001 From: Pamela Chia Date: Wed, 13 Aug 2025 17:28:32 +0800 Subject: [PATCH 1/3] update fair use policy (#37889) --- apps/docs/content/guides/platform/billing-faq.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/docs/content/guides/platform/billing-faq.mdx b/apps/docs/content/guides/platform/billing-faq.mdx index 719cf942f4b4d..a167dc55cf17a 100644 --- a/apps/docs/content/guides/platform/billing-faq.mdx +++ b/apps/docs/content/guides/platform/billing-faq.mdx @@ -128,7 +128,7 @@ The Fair Use Policy is generally applied to all projects of the restricted organ To remove restrictions, you will need to address the issue that caused the restriction. This could be reducing your usage, paying overdue invoices, updating your payment method, or any other issue that caused the restriction. Once the issue is resolved, the restriction will be lifted. -Restrictions due to usage limits are lifted with the next billing cycle as your quota refills at the beginning of each cycle. You can see when your current billing cycle ends on the [billing page](https://supabase.com/dashboard/org/_/billing) under "Upcoming Invoice". If your organization is on the Free Plan, you can also lift restrictions immediately by [upgrading](https://supabase.com/dashboard/org/_/billing?panel=subscriptionPlan) to Pro. +Restrictions due to usage limits are lifted with the next billing cycle as your quota refills at the beginning of each cycle. You can see when your current billing cycle ends on the [billing page](https://supabase.com/dashboard/org/_/billing) under "Upcoming Invoice". You can also lift restrictions immediately by [upgrading](https://supabase.com/dashboard/org/_/billing?panel=subscriptionPlan) to Pro (if on Free Plan) or by [disabling spend cap](https://supabase.com/dashboard/org/_/billing?panel=costControl) (if on Pro Plan with spend cap enabled). ## Reports and invoices From 90b22881227aa9ccbb46f40ac14e9b5122ab102a Mon Sep 17 00:00:00 2001 From: Thomas <31189692+ecktoteckto@users.noreply.github.com> Date: Wed, 13 Aug 2025 11:31:14 +0200 Subject: [PATCH 2/3] feat(billing): add onboarding page for AWS Marketplace --- .../AwsMarketplaceAutoRenewalWarning.tsx | 37 ++ .../AwsMarketplaceCreateNewOrg.tsx | 94 +++++ .../AwsMarketplaceLinkExistingOrg.tsx | 342 ++++++++++++++++++ .../AwsMarketplaceOnboardingPlaceholder.tsx | 29 ++ .../AwsMarketplaceOnboardingSuccessModal.tsx | 42 +++ .../NewAwsMarketplaceOrgForm.tsx | 159 ++++++++ .../NewAwsMarketplaceOrgModal.tsx | 84 +++++ .../cloud-marketplace-query.ts | 50 +++ .../Organization/CloudMarketplace/keys.ts | 3 + .../layouts/LinkAwsMarketplaceLayout.tsx | 51 +++ .../organization-create-mutation.ts | 79 ++++ ...anization-link-aws-marketplace-mutation.ts | 51 +++ .../pages/aws-marketplace-onboarding.tsx | 52 +++ packages/api-types/types/platform.d.ts | 111 +++--- 14 files changed, 1135 insertions(+), 49 deletions(-) create mode 100644 apps/studio/components/interfaces/Organization/CloudMarketplace/AwsMarketplaceAutoRenewalWarning.tsx create mode 100644 apps/studio/components/interfaces/Organization/CloudMarketplace/AwsMarketplaceCreateNewOrg.tsx create mode 100644 apps/studio/components/interfaces/Organization/CloudMarketplace/AwsMarketplaceLinkExistingOrg.tsx create mode 100644 apps/studio/components/interfaces/Organization/CloudMarketplace/AwsMarketplaceOnboardingPlaceholder.tsx create mode 100644 apps/studio/components/interfaces/Organization/CloudMarketplace/AwsMarketplaceOnboardingSuccessModal.tsx create mode 100644 apps/studio/components/interfaces/Organization/CloudMarketplace/NewAwsMarketplaceOrgForm.tsx create mode 100644 apps/studio/components/interfaces/Organization/CloudMarketplace/NewAwsMarketplaceOrgModal.tsx create mode 100644 apps/studio/components/interfaces/Organization/CloudMarketplace/cloud-marketplace-query.ts create mode 100644 apps/studio/components/interfaces/Organization/CloudMarketplace/keys.ts create mode 100644 apps/studio/components/layouts/LinkAwsMarketplaceLayout.tsx create mode 100644 apps/studio/data/organizations/organization-link-aws-marketplace-mutation.ts create mode 100644 apps/studio/pages/aws-marketplace-onboarding.tsx diff --git a/apps/studio/components/interfaces/Organization/CloudMarketplace/AwsMarketplaceAutoRenewalWarning.tsx b/apps/studio/components/interfaces/Organization/CloudMarketplace/AwsMarketplaceAutoRenewalWarning.tsx new file mode 100644 index 0000000000000..e8056bd2bd466 --- /dev/null +++ b/apps/studio/components/interfaces/Organization/CloudMarketplace/AwsMarketplaceAutoRenewalWarning.tsx @@ -0,0 +1,37 @@ +import { Alert_Shadcn_, AlertDescription_Shadcn_, AlertTitle_Shadcn_ } from 'ui' +import dayjs from 'dayjs' +import Link from 'next/link' + +interface Props { + awsContractEndDate: string + awsContractSettingsUrl: string +} + +const AwsMarketplaceAutoRenewalWarning = ({ + awsContractEndDate, + awsContractSettingsUrl, +}: Props) => { + return ( +
+ + + “Auto Renewal” is turned OFF for your AWS Marketplace subscription + + +
+ As a result, your Supabase organization will be downgraded to the Free Plan on{' '} + {dayjs(awsContractEndDate).format('MMMM DD')}. If you have more than 2 projects running, + all your projects will be paused. To ensure uninterrupted service, enable “Auto Renewal” + in your {''} + + AWS Marketplace subscription settings + + . +
+
+
+
+ ) +} + +export default AwsMarketplaceAutoRenewalWarning diff --git a/apps/studio/components/interfaces/Organization/CloudMarketplace/AwsMarketplaceCreateNewOrg.tsx b/apps/studio/components/interfaces/Organization/CloudMarketplace/AwsMarketplaceCreateNewOrg.tsx new file mode 100644 index 0000000000000..e9c9a2b7a9f6f --- /dev/null +++ b/apps/studio/components/interfaces/Organization/CloudMarketplace/AwsMarketplaceCreateNewOrg.tsx @@ -0,0 +1,94 @@ +import { useAwsManagedOrganizationCreateMutation } from '../../../../data/organizations/organization-create-mutation' +import { toast } from 'sonner' +import { SubmitHandler } from 'react-hook-form' +import NewAwsMarketplaceOrgForm, { + CREATE_AWS_MANAGED_ORG_FORM_ID, + NewMarketplaceOrgForm, +} from './NewAwsMarketplaceOrgForm' +import { + ScaffoldSection, + ScaffoldSectionContent, + ScaffoldSectionDetail, +} from '../../../layouts/Scaffold' +import Link from 'next/link' +import { Button } from 'ui' +import { useRouter } from 'next/router' +import AwsMarketplaceAutoRenewalWarning from './AwsMarketplaceAutoRenewalWarning' +import { CloudMarketplaceOnboardingInfo } from './cloud-marketplace-query' + +interface Props { + onboardingInfo?: CloudMarketplaceOnboardingInfo | undefined +} + +const AwsMarketplaceCreateNewOrg = ({ onboardingInfo }: Props) => { + const router = useRouter() + const { + query: { buyer_id: buyerId }, + } = router + + const { mutate: createOrganization, isLoading: isCreatingOrganization } = + useAwsManagedOrganizationCreateMutation({ + onSuccess: (org) => { + //TODO(thomas): send tracking event? + router.push(`/org/${org.slug}`) + }, + onError: (res) => { + toast.error(res.message, { + duration: 7_000, + }) + }, + }) + + const onSubmit: SubmitHandler = async (values) => { + createOrganization({ ...values, buyerId: buyerId as string }) + } + + return ( + <> + {onboardingInfo && !onboardingInfo.aws_contract_auto_renewal && ( + + )} + + +

+ You’ve subscribed to the Supabase {onboardingInfo?.plan_name_selected_on_marketplace}{' '} + Plan via the AWS Marketplace. As a final step, you need to create a Supabase + organization. That organization will be managed and billed through AWS Marketplace. +

+

+ You can read more on billing through AWS in our {''} + {/*TODO(thomas): Update docs link once the new docs exist*/} + + Billing Docs. + +

+
+ +
+ + +
+ +
+
+
+
+ + ) +} + +export default AwsMarketplaceCreateNewOrg diff --git a/apps/studio/components/interfaces/Organization/CloudMarketplace/AwsMarketplaceLinkExistingOrg.tsx b/apps/studio/components/interfaces/Organization/CloudMarketplace/AwsMarketplaceLinkExistingOrg.tsx new file mode 100644 index 0000000000000..c00b2b3fa0eb4 --- /dev/null +++ b/apps/studio/components/interfaces/Organization/CloudMarketplace/AwsMarketplaceLinkExistingOrg.tsx @@ -0,0 +1,342 @@ +import { + Button, + Collapsible_Shadcn_, + CollapsibleContent_Shadcn_, + CollapsibleTrigger_Shadcn_, + Form_Shadcn_, + FormField_Shadcn_, + Skeleton, +} from 'ui' +import { RadioGroupCard, RadioGroupCardItem } from '@ui/components/radio-group-card' +import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout' +import { cn } from '@ui/lib/utils' +import { ActionCard } from '../../../ui/ActionCard' +import { Boxes, ChevronRight } from 'lucide-react' +import { + ScaffoldSection, + ScaffoldSectionContent, + ScaffoldSectionDetail, +} from '../../../layouts/Scaffold' +import { ButtonTooltip } from '../../../ui/ButtonTooltip' +import { z } from 'zod' +import { SubmitHandler, useForm } from 'react-hook-form' +import { zodResolver } from '@hookform/resolvers/zod' +import { useMemo, useState } from 'react' +import { Organization } from '../../../../types' +import { useProjectsQuery } from '../../../../data/projects/projects-query' +import { useOrganizationLinkAwsMarketplaceMutation } from '../../../../data/organizations/organization-link-aws-marketplace-mutation' +import { toast } from 'sonner' +import AwsMarketplaceOnboardingSuccessModal from './AwsMarketplaceOnboardingSuccessModal' +import NewAwsMarketplaceOrgModal from './NewAwsMarketplaceOrgModal' +import { useRouter } from 'next/router' +import AwsMarketplaceAutoRenewalWarning from './AwsMarketplaceAutoRenewalWarning' +import { CloudMarketplaceOnboardingInfo } from './cloud-marketplace-query' +import Link from 'next/link' + +interface Props { + organizations?: Organization[] | undefined + onboardingInfo?: CloudMarketplaceOnboardingInfo | undefined + isLoadingOnboardingInfo: boolean +} + +const FormSchema = z.object({ + orgSlug: z.string(), +}) + +export type LinkExistingOrgForm = z.infer + +const AwsMarketplaceLinkExistingOrg = ({ + organizations, + onboardingInfo, + isLoadingOnboardingInfo, +}: Props) => { + const router = useRouter() + const { + query: { buyer_id: buyerId }, + } = router + + const form = useForm({ + resolver: zodResolver(FormSchema), + defaultValues: { + orgSlug: undefined, + }, + mode: 'onBlur', + reValidateMode: 'onChange', + }) + + const isDirty = !!Object.keys(form.formState.dirtyFields).length + + // Sort organizations by name ascending + const sortedOrganizations = useMemo(() => { + return organizations?.slice().sort((a, b) => a.name.localeCompare(b.name)) + }, [organizations]) + + const { orgsLinkable, orgsNotLinkable } = useMemo(() => { + const orgQualifiesForLinking = (org: Organization) => { + const validationResult = onboardingInfo?.organization_linking_eligibility.find( + (result) => result.slug === org.slug + ) + + return validationResult?.is_eligible ?? false + } + + const linkable: Organization[] = [] + const notLinkable: Organization[] = [] + sortedOrganizations?.forEach((org) => { + if (orgQualifiesForLinking(org)) { + linkable.push(org) + } else { + notLinkable.push(org) + } + }) + return { orgsLinkable: linkable, orgsNotLinkable: notLinkable } + }, [sortedOrganizations, onboardingInfo?.organization_linking_eligibility]) + + const { data: projects = [] } = useProjectsQuery() + + const [isNotLinkableOrgListOpen, setIsNotLinkableOrgListOpen] = useState(false) + const [orgLinkedSuccessfully, setOrgLinkedSuccessfully] = useState(false) + const [showOrgCreationDialog, setShowOrgCreationDialog] = useState(false) + const [orgToRedirectTo, setOrgToRedirectTo] = useState('') + + const { mutate: linkOrganization, isLoading: isLinkingOrganization } = + useOrganizationLinkAwsMarketplaceMutation({ + onSuccess: (_) => { + //TODO(thomas): send tracking event? + setOrgLinkedSuccessfully(true) + setOrgToRedirectTo(form.getValues('orgSlug')) + }, + onError: (res) => { + toast.error(res.message, { + duration: 7_000, + }) + }, + }) + + const onSubmit: SubmitHandler = async (values) => { + linkOrganization({ slug: values.orgSlug, buyerId: buyerId as string }) + } + + return ( + <> + {onboardingInfo && !onboardingInfo.aws_contract_auto_renewal && ( + + )} + + + <> +

+ You’ve subscribed to the Supabase {onboardingInfo?.plan_name_selected_on_marketplace}{' '} + Plan via the AWS Marketplace. As a final step, you need to link a Supabase + organization to that subscription. Select the organization you want to be managed and + billed through AWS. +

+ +

+ You can read more on billing through AWS in our {''} + {/*TODO(thomas): Update docs link once the new docs exist*/} + + Billing Docs. + +

+ +

+ Want to start fresh? Create a + new organization and it will be linked automatically. +

+ + +
+ + + +
+ ( + { + form.setValue('orgSlug', value, { + shouldDirty: true, + shouldValidate: false, + }) + }} + > + +
+ {isLoadingOnboardingInfo ? ( + Array(3) + .fill(0) + .map((_, i) => ( + + )) + ) : ( + <> +

+ Organizations that can be linked +

+ {orgsLinkable.length === 0 ? ( +

+ None of your organizations can be linked to your AWS Marketplace + subscription at the moment. +

+ ) : ( + <> + {orgsLinkable.map((org) => { + const numProjects = projects.filter( + (p) => p.organization_slug === org.slug + ).length + return ( + + } + title={org.name} + description={`${org.plan.name} Plan • ${numProjects > 0 ? `${numProjects} Project${numProjects > 1 ? 's' : ''}` : '0 Projects'}`} + /> + } + /> + ) + })} + + )} + + )} +
+
+
+ )} + /> + +
+ + {orgsNotLinkable.length > 0 && !isLoadingOnboardingInfo && ( + setIsNotLinkableOrgListOpen((prev) => !prev)} + > + +

+ Organizations that can't be linked +

+ +
+ +

+ The following organizations can’t be linked to your AWS Marketplace subscription + at the moment. This may be due to missing permissions, outstanding invoices, or an + existing marketplace link. If you'd like to link one of these organizations, + please review the organization settings. You need to be Owner or Administrator of + the organization to link it. +

+
+ {orgsNotLinkable.map((org) => { + const numProjects = projects.filter( + (p) => p.organization_slug === org.slug + ).length + return ( + } + title={org.name} + description={`${org.plan.name} Plan • ${numProjects > 0 ? `${numProjects} Project${numProjects > 1 ? 's' : ''}` : '0 Projects'}`} + /> + ) + })} +
+
+
+ )} + +
+ { + await onSubmit(form.getValues()) + }} + loading={isLinkingOrganization} + disabled={!isDirty || isLinkingOrganization || isLoadingOnboardingInfo} + tooltip={{ + content: { + side: 'top', + text: !isDirty ? 'No organization selected' : undefined, + }, + }} + > + Link organization + +
+
+
+ + { + setOrgLinkedSuccessfully(false) + router.push(`/org/${orgToRedirectTo}`) + }} + /> + + setShowOrgCreationDialog(false)} + buyerId={buyerId as string} + onSuccess={(newlyCreatedOrgSlug) => { + setShowOrgCreationDialog(false) + setOrgToRedirectTo(newlyCreatedOrgSlug) + setOrgLinkedSuccessfully(true) + }} + /> + + ) +} + +export default AwsMarketplaceLinkExistingOrg diff --git a/apps/studio/components/interfaces/Organization/CloudMarketplace/AwsMarketplaceOnboardingPlaceholder.tsx b/apps/studio/components/interfaces/Organization/CloudMarketplace/AwsMarketplaceOnboardingPlaceholder.tsx new file mode 100644 index 0000000000000..07f9c13abe427 --- /dev/null +++ b/apps/studio/components/interfaces/Organization/CloudMarketplace/AwsMarketplaceOnboardingPlaceholder.tsx @@ -0,0 +1,29 @@ +import { + ScaffoldSection, + ScaffoldSectionContent, + ScaffoldSectionDetail, +} from '../../../layouts/Scaffold' +import { Skeleton } from '@ui/components/shadcn/ui/skeleton' + +const AwsMarketplaceOnboardingPlaceholder = () => { + return ( + + + {Array(1) + .fill(0) + .map((_, i) => ( + + ))} + + + {Array(3) + .fill(0) + .map((_, i) => ( + + ))} + + + ) +} + +export default AwsMarketplaceOnboardingPlaceholder diff --git a/apps/studio/components/interfaces/Organization/CloudMarketplace/AwsMarketplaceOnboardingSuccessModal.tsx b/apps/studio/components/interfaces/Organization/CloudMarketplace/AwsMarketplaceOnboardingSuccessModal.tsx new file mode 100644 index 0000000000000..1402382228b41 --- /dev/null +++ b/apps/studio/components/interfaces/Organization/CloudMarketplace/AwsMarketplaceOnboardingSuccessModal.tsx @@ -0,0 +1,42 @@ +import { Dialog, DialogContent, DialogFooter, DialogSection } from '@ui/components/shadcn/ui/dialog' +import { Button } from 'ui' + +interface Props { + visible: boolean + onClose: () => void +} + +const AwsMarketplaceOnboardingSuccessModal = ({ visible, onClose }: Props) => { + return ( + { + if (!open) onClose() + }} + > + event.preventDefault()} + size="xlarge" + hideClose={true} + onEscapeKeyDown={(e) => e.preventDefault()} + onPointerDownOutside={(e) => e.preventDefault()} + > + +
+

AWS Marketplace Setup completed

+

+ The organization is now managed and billed through AWS Marketplace. +

+
+
+ + + +
+
+ ) +} + +export default AwsMarketplaceOnboardingSuccessModal diff --git a/apps/studio/components/interfaces/Organization/CloudMarketplace/NewAwsMarketplaceOrgForm.tsx b/apps/studio/components/interfaces/Organization/CloudMarketplace/NewAwsMarketplaceOrgForm.tsx new file mode 100644 index 0000000000000..cc27b6389c3a8 --- /dev/null +++ b/apps/studio/components/interfaces/Organization/CloudMarketplace/NewAwsMarketplaceOrgForm.tsx @@ -0,0 +1,159 @@ +import { z } from 'zod' +import { + Form_Shadcn_, + FormControl_Shadcn_, + FormField_Shadcn_, + Input_Shadcn_, + Label_Shadcn_, + Select_Shadcn_, + SelectContent_Shadcn_, + SelectItem_Shadcn_, + SelectTrigger_Shadcn_, + SelectValue_Shadcn_, +} from 'ui' +import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout' +import { useForm } from 'react-hook-form' +import { zodResolver } from '@hookform/resolvers/zod' + +const ORG_KIND_TYPES = { + PERSONAL: 'Personal', + EDUCATIONAL: 'Educational', + STARTUP: 'Startup', + AGENCY: 'Agency', + COMPANY: 'Company', + UNDISCLOSED: 'N/A', +} + +const ORG_KIND_DEFAULT = 'PERSONAL' + +const ORG_SIZE_TYPES = { + '1': '1 - 10', + '10': '10 - 49', + '50': '50 - 99', + '100': '100 - 299', + '300': 'More than 300', +} + +interface Props { + onSubmit: (values: NewMarketplaceOrgForm) => void +} + +export const CREATE_AWS_MANAGED_ORG_FORM_ID = 'create-aws-managed-org-form' + +const FormSchema = z.object({ + name: z.string().trim().min(1, 'Please provide an organization name'), + kind: z.string(), + size: z.string().optional(), +}) + +export type NewMarketplaceOrgForm = z.infer + +const NewAwsMarketplaceOrgForm = ({ onSubmit }: Props) => { + const form = useForm({ + resolver: zodResolver(FormSchema), + defaultValues: { + name: '', + kind: ORG_KIND_DEFAULT, + }, + }) + + const kind = form.watch('kind') + + return ( + +
+
+ ( + + + <> + +
+ + What's the name of your company or team? + +
+ +
+
+ )} + /> + ( + + + <> + + + + + + {Object.entries(ORG_KIND_TYPES).map(([k, v]) => ( + + {v} + + ))} + + +
+ + What would best describe your organization? + +
+ +
+
+ )} + /> + {kind == 'COMPANY' && ( + ( + + + <> + + + + + + {Object.entries(ORG_SIZE_TYPES).map(([k, v]) => ( + + {v} + + ))} + + +
+ + How many people are in your company? + +
+ +
+
+ )} + /> + )} +
+
+
+ ) +} + +export default NewAwsMarketplaceOrgForm diff --git a/apps/studio/components/interfaces/Organization/CloudMarketplace/NewAwsMarketplaceOrgModal.tsx b/apps/studio/components/interfaces/Organization/CloudMarketplace/NewAwsMarketplaceOrgModal.tsx new file mode 100644 index 0000000000000..13a31c4be6054 --- /dev/null +++ b/apps/studio/components/interfaces/Organization/CloudMarketplace/NewAwsMarketplaceOrgModal.tsx @@ -0,0 +1,84 @@ +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogSection, + DialogSectionSeparator, + DialogTitle, +} from '@ui/components/shadcn/ui/dialog' +import { Button } from 'ui' +import NewAwsMarketplaceOrgForm, { + CREATE_AWS_MANAGED_ORG_FORM_ID, + NewMarketplaceOrgForm, +} from './NewAwsMarketplaceOrgForm' +import { useAwsManagedOrganizationCreateMutation } from '../../../../data/organizations/organization-create-mutation' +import { toast } from 'sonner' +import { SubmitHandler } from 'react-hook-form' + +interface Props { + buyerId: string + visible: boolean + onSuccess: (newlyCreatedOrgSlug: string) => void + onClose: () => void +} + +const NewAwsMarketplaceOrgModal = ({ buyerId, visible, onSuccess, onClose }: Props) => { + const { mutate: createOrganization, isLoading: isCreatingOrganization } = + useAwsManagedOrganizationCreateMutation({ + onSuccess: (org) => { + //TODO(thomas): send tracking event? + onSuccess(org.slug) + }, + onError: (res) => { + toast.error(res.message, { + duration: 7_000, + }) + }, + }) + + const onSubmit: SubmitHandler = async (values) => { + createOrganization({ ...values, buyerId }) + } + + return ( + { + if (!open) onClose() + }} + > + event.preventDefault()} + size="xlarge" + onEscapeKeyDown={(e) => (isCreatingOrganization ? e.preventDefault() : onClose())} + onPointerDownOutside={(e) => (isCreatingOrganization ? e.preventDefault() : onClose())} + className="p-2" + > + + Create a new organization + + A new organization will be created and linked to your AWS Marketplace subscription + + + + + + + + + + + + ) +} + +export default NewAwsMarketplaceOrgModal diff --git a/apps/studio/components/interfaces/Organization/CloudMarketplace/cloud-marketplace-query.ts b/apps/studio/components/interfaces/Organization/CloudMarketplace/cloud-marketplace-query.ts new file mode 100644 index 0000000000000..0b9d924aa4c7e --- /dev/null +++ b/apps/studio/components/interfaces/Organization/CloudMarketplace/cloud-marketplace-query.ts @@ -0,0 +1,50 @@ +import { get, handleError } from '../../../../data/fetchers' +import type { ResponseError } from '../../../../types' +import { useQuery, UseQueryOptions } from '@tanstack/react-query' +import { useProfile } from '../../../../lib/profile' +import { cloudMarketplaceKeys } from './keys' + +export type CloudMarketplaceOnboardingInfoVariables = { + buyerId: string +} + +export async function getCloudMarketplaceOnboardingInfo( + { buyerId }: CloudMarketplaceOnboardingInfoVariables, + signal?: AbortSignal +) { + const { data, error } = await get( + '/platform/cloud-marketplace/buyers/{buyer_id}/onboarding-info', + { + params: { path: { buyer_id: buyerId } }, + signal, + } + ) + + if (error) handleError(error) + + return data +} + +export type CloudMarketplaceOnboardingInfo = Awaited< + ReturnType +> +export type CloudMarketplaceOnboardingInfoError = ResponseError + +export const useCloudMarketplaceOnboardingInfoQuery = ( + { buyerId }: CloudMarketplaceOnboardingInfoVariables, + { + enabled = true, + ...options + }: UseQueryOptions< + CloudMarketplaceOnboardingInfo, + CloudMarketplaceOnboardingInfoError, + TData + > = {} +) => { + const { profile } = useProfile() + return useQuery( + cloudMarketplaceKeys.onboardingInfo(buyerId), + ({ signal }) => getCloudMarketplaceOnboardingInfo({ buyerId }, signal), + { enabled: enabled && profile !== undefined, ...options, staleTime: 30 * 60 * 1000 } + ) +} diff --git a/apps/studio/components/interfaces/Organization/CloudMarketplace/keys.ts b/apps/studio/components/interfaces/Organization/CloudMarketplace/keys.ts new file mode 100644 index 0000000000000..790b1e1332914 --- /dev/null +++ b/apps/studio/components/interfaces/Organization/CloudMarketplace/keys.ts @@ -0,0 +1,3 @@ +export const cloudMarketplaceKeys = { + onboardingInfo: (buyerId: string) => ['cloud-marketplace', 'onboarding-info', buyerId], +} diff --git a/apps/studio/components/layouts/LinkAwsMarketplaceLayout.tsx b/apps/studio/components/layouts/LinkAwsMarketplaceLayout.tsx new file mode 100644 index 0000000000000..8baff25324022 --- /dev/null +++ b/apps/studio/components/layouts/LinkAwsMarketplaceLayout.tsx @@ -0,0 +1,51 @@ +import { BASE_PATH } from 'lib/constants' +import { useTheme } from 'next-themes' +import Head from 'next/head' +import Image from 'next/legacy/image' +import type { PropsWithChildren } from 'react' +import { Separator } from 'ui' +import { withAuth } from '../../hooks/misc/withAuth' + +export interface LinkAwsMarketplaceLayoutProps {} + +const LinkAwsMarketplaceLayout = ({ + children, +}: PropsWithChildren) => { + const { resolvedTheme } = useTheme() + return ( + <> + + AWS Marketplace Setup | Supabase + +
+
+
+
+
+
+ Supabase + Supabase Logo +
+
+
+
+
+ +
+ {children} +
+
+ + ) +} + +export default withAuth(LinkAwsMarketplaceLayout) diff --git a/apps/studio/data/organizations/organization-create-mutation.ts b/apps/studio/data/organizations/organization-create-mutation.ts index 5baffba7b9824..93024acbb3be4 100644 --- a/apps/studio/data/organizations/organization-create-mutation.ts +++ b/apps/studio/data/organizations/organization-create-mutation.ts @@ -93,3 +93,82 @@ export const useOrganizationCreateMutation = ({ } ) } + +export type AwsManagedOrganizationCreateVariables = { + name: string + kind?: string + size?: string + buyerId: string +} + +export async function createAwsManagedOrganization({ + name, + kind, + size, + buyerId, +}: AwsManagedOrganizationCreateVariables) { + const { data, error } = await post('/platform/organizations/cloud-marketplace', { + body: { + name, + kind, + size, + buyer_id: buyerId, + }, + }) + + if (error) handleError(error) + return data +} + +type AwsManagedOrganizationCreateData = Awaited> + +export const useAwsManagedOrganizationCreateMutation = ({ + onSuccess, + onError, + ...options +}: Omit< + UseMutationOptions< + AwsManagedOrganizationCreateData, + ResponseError, + AwsManagedOrganizationCreateVariables + >, + 'mutationFn' +> = {}) => { + const queryClient = useQueryClient() + + return useMutation< + AwsManagedOrganizationCreateData, + ResponseError, + AwsManagedOrganizationCreateVariables + >((vars) => createAwsManagedOrganization(vars), { + async onSuccess(data, variables, context) { + if (data) { + // [Joshen] We're manually updating the query client here as the org's subscription is + // created async, and the invalidation will happen too quick where the GET organizations + // endpoint will error out with a 500 since the subscription isn't created yet. + queryClient.setQueriesData( + { + queryKey: organizationKeys.list(), + exact: true, + }, + (prev: any) => { + if (!prev) return prev + return [...prev, castOrganizationResponseToOrganization(data)] + } + ) + + await queryClient.invalidateQueries(permissionKeys.list()) + } + + await onSuccess?.(data, variables, context) + }, + async onError(data, variables, context) { + if (onError === undefined) { + toast.error(`Failed to create organization: ${data.message}`) + } else { + onError(data, variables, context) + } + }, + ...options, + }) +} diff --git a/apps/studio/data/organizations/organization-link-aws-marketplace-mutation.ts b/apps/studio/data/organizations/organization-link-aws-marketplace-mutation.ts new file mode 100644 index 0000000000000..a8ac033a76499 --- /dev/null +++ b/apps/studio/data/organizations/organization-link-aws-marketplace-mutation.ts @@ -0,0 +1,51 @@ +import { useMutation, UseMutationOptions } from '@tanstack/react-query' +import { toast } from 'sonner' + +import { handleError, put } from '../fetchers' +import type { ResponseError } from '../../types' + +export type OrganizationLinkAwsMarketplaceVariables = { + buyerId: string + slug: string +} + +export async function linkOrganization({ buyerId, slug }: OrganizationLinkAwsMarketplaceVariables) { + const { data, error } = await put(`/platform/organizations/{slug}/cloud-marketplace/link`, { + params: { path: { slug } }, + body: { + buyer_id: buyerId, + }, + }) + + if (error) handleError(error) + + return data +} + +type LinkOrganizationData = Awaited> + +export const useOrganizationLinkAwsMarketplaceMutation = ({ + onSuccess, + onError, + ...options +}: Omit< + UseMutationOptions, + 'mutationFn' +> = {}) => { + return useMutation( + (vars) => linkOrganization(vars), + { + async onSuccess(data, variables, context) { + await onSuccess?.(data, variables, context) + }, + async onError(data, variables, context) { + if (onError === undefined) { + toast.error(`Failed to link organization to AWS Marketplace: ${data.message}`) + } else { + onError(data, variables, context) + } + }, + ...options, + } + ) +} diff --git a/apps/studio/pages/aws-marketplace-onboarding.tsx b/apps/studio/pages/aws-marketplace-onboarding.tsx new file mode 100644 index 0000000000000..0faea387ebf4a --- /dev/null +++ b/apps/studio/pages/aws-marketplace-onboarding.tsx @@ -0,0 +1,52 @@ +import { NextPageWithLayout } from '../types' +import { + ScaffoldContainer, + ScaffoldDivider, + ScaffoldHeader, + ScaffoldTitle, +} from '../components/layouts/Scaffold' +import LinkAwsMarketplaceLayout from '../components/layouts/LinkAwsMarketplaceLayout' +import { useOrganizationsQuery } from '../data/organizations/organizations-query' +import AwsMarketplaceLinkExistingOrg from '../components/interfaces/Organization/CloudMarketplace/AwsMarketplaceLinkExistingOrg' +import AwsMarketplaceCreateNewOrg from '../components/interfaces/Organization/CloudMarketplace/AwsMarketplaceCreateNewOrg' +import { useCloudMarketplaceOnboardingInfoQuery } from '../components/interfaces/Organization/CloudMarketplace/cloud-marketplace-query' +import { useRouter } from 'next/router' +import AwsMarketplaceOnboardingPlaceholder from '../components/interfaces/Organization/CloudMarketplace/AwsMarketplaceOnboardingPlaceholder' + +const AwsMarketplaceOnboarding: NextPageWithLayout = () => { + const { + query: { buyer_id: buyerId }, + } = useRouter() + + const { data: organizations, isFetched: isOrganizationsFetched } = useOrganizationsQuery() + const { data: onboardingInfo, isLoading: isLoadingOnboardingInfo } = + useCloudMarketplaceOnboardingInfoQuery({ + buyerId: buyerId as string, + }) + + return ( + + + AWS Marketplace Setup + + + {!isOrganizationsFetched ? ( + + ) : organizations?.length ? ( + + ) : ( + + )} + + ) +} + +AwsMarketplaceOnboarding.getLayout = (page) => ( + {page} +) + +export default AwsMarketplaceOnboarding diff --git a/packages/api-types/types/platform.d.ts b/packages/api-types/types/platform.d.ts index b84082af171d7..c5e541ff155a0 100644 --- a/packages/api-types/types/platform.d.ts +++ b/packages/api-types/types/platform.d.ts @@ -227,6 +227,23 @@ export interface paths { patch?: never trace?: never } + '/platform/cloud-marketplace/buyers/{buyer_id}/onboarding-info': { + parameters: { + query?: never + header?: never + path?: never + cookie?: never + } + /** Get info needed for AWS Marketplace onboarding */ + get: operations['ClazarController_getCloudMarketplaceOnboardingInfo'] + put?: never + post?: never + delete?: never + options?: never + head?: never + patch?: never + trace?: never + } '/platform/database/{ref}/backups': { parameters: { query?: never @@ -1640,23 +1657,6 @@ export interface paths { patch?: never trace?: never } - '/platform/organizations/cloud-marketplace/check-eligibility': { - parameters: { - query?: never - header?: never - path?: never - cookie?: never - } - /** Check whether given organizations are eligible for AWS billing */ - get: operations['OrganizationsController_checkCloudMarketplaceEligibility'] - put?: never - post?: never - delete?: never - options?: never - head?: never - patch?: never - trace?: never - } '/platform/organizations/confirm-subscription': { parameters: { query?: never @@ -4438,11 +4438,6 @@ export interface components { ChangeMFAEnforcementStateRequest: { enforced: boolean } - CheckCloudMarketplaceEligibilityResponse: { - is_eligible: boolean - reasons: string[] - slug: string - }[] CloneBackupsResponse: { backups: { id: number @@ -4471,6 +4466,22 @@ export interface components { /** @default 0 */ recoveryTimeTarget?: number } + CloudMarketplaceOnboardingInfoResponse: { + aws_contract_auto_renewal: boolean + aws_contract_end_date: string + aws_contract_settings_url: string + aws_contract_start_date: string + organization_linking_eligibility: { + is_eligible: boolean + reasons: ( + | 'ALREADY_BILLED_BY_PARTNER' + | 'ALREADY_BILLED_BY_PARTNER_AWS' + | 'OVERDUE_INVOICES' + )[] + slug: string + }[] + plan_name_selected_on_marketplace: string + } ConfirmCreateSubscriptionChangeBody: { kind?: string name: string @@ -9772,6 +9783,34 @@ export interface operations { } } } + ClazarController_getCloudMarketplaceOnboardingInfo: { + parameters: { + query?: never + header?: never + path: { + buyer_id: string + } + cookie?: never + } + requestBody?: never + responses: { + 200: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['CloudMarketplaceOnboardingInfoResponse'] + } + } + /** @description Failed to get info for AWS Marketplace onboarding */ + 500: { + headers: { + [name: string]: unknown + } + content?: never + } + } + } BackupsController_getBackups: { parameters: { query?: never @@ -13256,7 +13295,7 @@ export interface operations { [name: string]: unknown } content: { - 'application/json': components['schemas']['CreateOrganizationResponse'] + 'application/json': components['schemas']['OrganizationResponse'] } } /** @description Failed to create organization billed by AWS Marketplace */ @@ -13268,32 +13307,6 @@ export interface operations { } } } - OrganizationsController_checkCloudMarketplaceEligibility: { - parameters: { - query?: never - header?: never - path?: never - cookie?: never - } - requestBody?: never - responses: { - 200: { - headers: { - [name: string]: unknown - } - content: { - 'application/json': components['schemas']['CheckCloudMarketplaceEligibilityResponse'] - } - } - /** @description Failed to check whether organizations are eligible for AWS billing */ - 500: { - headers: { - [name: string]: unknown - } - content?: never - } - } - } OrganizationsController_confirmSubscription: { parameters: { query?: never From d60b3e3b9ecaa7436f51e022025563d45f941d21 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kevin=20Gr=C3=BCneberg?= Date: Wed, 13 Aug 2025 17:36:48 +0800 Subject: [PATCH 3/3] feat: cached egress rollout (#37827) --- .../database/extensions/wrappers/overview.mdx | 2 +- .../content/guides/platform/billing-faq.mdx | 6 +- .../platform/manage-your-usage/egress.mdx | 44 +- .../guides/storage/production/scaling.mdx | 4 + .../guides/storage/serving/bandwidth.mdx | 50 +- .../storage/serving/image-transformations.mdx | 2 +- .../docs/content/guides/telemetry/reports.mdx | 4 +- .../all-about-supabase-egress-a_Sg_e.mdx | 8 +- .../docs/Reference.navigation.client.tsx | 2 +- .../guides/platform/cached-egress--light.png | Bin 0 -> 64705 bytes .../img/guides/platform/cached-egress.png | Bin 0 -> 64259 bytes .../BillingBreakdown.constants.ts | 8 + .../BillingBreakdown/UpcomingInvoice.tsx | 2 + .../Subscription/EnterpriseCard.tsx | 6 +- .../Subscription/PlanUpdateSidePanel.tsx | 15 +- .../SubscriptionPlanUpdateDialog.tsx | 4 +- .../Organization/BillingSettings/helpers.ts | 1 + .../Usage/{Bandwidth.tsx => Egress.tsx} | 24 +- .../Organization/Usage/Usage.constants.tsx | 520 ++++++++++-------- .../interfaces/Organization/Usage/Usage.tsx | 4 +- .../Settings/Logs/Logs.constants.ts | 33 +- .../data/analytics/org-daily-stats-query.ts | 1 + apps/studio/lib/constants/metrics.tsx | 8 + .../[slug]/billing/subscription.ts | 1 + apps/www/components/Pricing/PricingPlans.tsx | 6 +- .../components/Pricing/PricingTableRow.tsx | 10 +- apps/www/data/features.tsx | 12 +- packages/api-types/types/api.d.ts | 2 + packages/api-types/types/platform.d.ts | 7 + packages/shared-data/plans.ts | 123 ++--- packages/shared-data/pricing.ts | 24 +- 31 files changed, 495 insertions(+), 438 deletions(-) create mode 100644 apps/docs/public/img/guides/platform/cached-egress--light.png create mode 100644 apps/docs/public/img/guides/platform/cached-egress.png rename apps/studio/components/interfaces/Organization/Usage/{Bandwidth.tsx => Egress.tsx} (71%) diff --git a/apps/docs/content/guides/database/extensions/wrappers/overview.mdx b/apps/docs/content/guides/database/extensions/wrappers/overview.mdx index 210d86d9bced3..a524a0d16b7ed 100644 --- a/apps/docs/content/guides/database/extensions/wrappers/overview.mdx +++ b/apps/docs/content/guides/database/extensions/wrappers/overview.mdx @@ -92,7 +92,7 @@ This approach has several benefits: 1. **On-demand:** analytical data is immediately available within your application with no additional infrastructure. 1. **Always in sync:** since the data is queried directly from the remote server, it's always up-to-date. 1. **Integrated:** large datasets are available within your application, and can be joined with your operational/transactional data. -1. **Save on bandwidth:** only extract/load what you need. +1. **Save on egress:** only extract/load what you need. ### Batch ETL with Wrappers diff --git a/apps/docs/content/guides/platform/billing-faq.mdx b/apps/docs/content/guides/platform/billing-faq.mdx index a167dc55cf17a..fa0dcde3872d0 100644 --- a/apps/docs/content/guides/platform/billing-faq.mdx +++ b/apps/docs/content/guides/platform/billing-faq.mdx @@ -64,9 +64,11 @@ If you upgrade your project to a larger instance for 10 hours and then downgrade Read more about [Compute usage](/docs/guides/platform/manage-your-usage/compute). -#### What is unified egress and how is it billed? +#### What is egress and how is it billed? -Unified egress refers to the total egress quota available to each organization. This quota can be utilized for various purposes such as Storage, Realtime, Auth, Functions, Supavisor, Log Drains and Database. Each plan includes a specific egress quota, and any additional usage beyond that quota is billed accordingly. +Egress refers to the total bandwidth (network traffic) quota available to each organization. This quota can be utilized for various purposes such as Storage, Realtime, Auth, Functions, Supavisor, Log Drains and Database. Each plan includes a specific egress quota, and any additional usage beyond that quota is billed accordingly. + +We differentiate between cached (served via our CDN from cache hits) and uncached egress and give quotas for each type and have varying pricing (cached egress is cheaper). Read more about [Egress usage](/docs/guides/platform/manage-your-usage/egress). diff --git a/apps/docs/content/guides/platform/manage-your-usage/egress.mdx b/apps/docs/content/guides/platform/manage-your-usage/egress.mdx index 71a893036af18..b5819a762343e 100644 --- a/apps/docs/content/guides/platform/manage-your-usage/egress.mdx +++ b/apps/docs/content/guides/platform/manage-your-usage/egress.mdx @@ -55,25 +55,29 @@ Data pushed to the connected log drain. **Example:** You set up a log drain, each log sent to the log drain is considered egress. You can toggle the GZIP option to reduce egress, in case your provider supports it. +### Cached Egress + +Cached and uncached egress have independent quotas and independent pricing. Cached egress is egress that is served from our CDN via cache hits. Cached egress is typically incurred for storage through our [Smart CDN](/docs/guides/storage/cdn/smart-cdn). + ## How charges are calculated Egress is charged by gigabyte. Charges apply only for usage exceeding your subscription plan's quota. This quota is called the Unified Egress Quota because it can be used across all services (Database, Auth, Storage etc.). ### Usage on your invoice -Usage is shown as "Egress GB" on your invoice. +Usage is shown as "Egress GB" and "Cached Egress GB" on your invoice. ## Pricing - per GB per month. You are only charged for usage exceeding your subscription plan's -quota. + per GB per month for uncached egress, per GB per month +for cached egress. You are only charged for usage exceeding your subscription plan's quota. -| Plan | Unified Egress Quota | Over-Usage per month | -| ---------- | -------------------- | ----------------------------- | -| Free | 5 GB | - | -| Pro | 250 GB | per GB | -| Team | 250 GB | per GB | -| Enterprise | Custom | Custom | +| Plan | Egress Quota (Uncached / Cached) | Over-Usage per month (Uncached / Cached) | +| ---------- | -------------------------------- | ------------------------------------------------------------- | +| Free | 5 GB / 5 GB | - | +| Pro | 250 GB / 250 GB | per GB / per GB | +| Team | 250 GB / 250 GB | per GB / per GB | +| Enterprise | Custom | Custom | ## Billing examples @@ -86,22 +90,24 @@ The organization's Egress usage is within the quota, so no charges for Egress ap | Pro Plan | 1 | | | Compute Hours Micro | 744 hours | | | Egress | 200 GB | | +| Cached Egress | 230 GB | | | **Subtotal** | | **** | | Compute Credits | | - | | **Total** | | **** | ### Exceeding quota -The organization's Egress usage exceeds the quota by 50 GB, incurring charges for this additional usage. +The organization's Egress usage exceeds the uncached egress quota by 50 GB and the cached egress quota by 550 GB, incurring charges for this additional usage. | Line Item | Units | Costs | | ------------------- | --------- | -------------------------- | | Pro Plan | 1 | | | Compute Hours Micro | 744 hours | | | Egress | 300 GB | | -| **Subtotal** | | **** | +| Cached Egress | 800 GB | | +| **Subtotal** | | **** | | Compute Credits | | - | -| **Total** | | **** | +| **Total** | | **** | ## View usage @@ -118,7 +124,7 @@ You can view Egress usage on the [organization's usage page](https://supabase.co zoomable /> -In the Total Egress section, you can see the usage for the selected time period. Hover over a specific date to view a breakdown by service. +In the Total Egress section, you can see the usage for the selected time period. Hover over a specific date to view a breakdown by service. Note that this includes the cached egress. Unified Egress +Separately, you can see the cached egress right below: + +Unified Egress + ### Custom report 1. On the [reports page](https://supabase.com/dashboard/project/_/reports), click **New custom report** in the left navigation menu @@ -144,7 +160,7 @@ In the Total Egress section, you can see the usage for the selected time period. ## Debug usage -To better understand your Egress usage, identify what’s driving the most traffic. Check the most frequent database queries, or analyze the most requested API paths to pinpoint high-bandwidth endpoints. +To better understand your Egress usage, identify what’s driving the most traffic. Check the most frequent database queries, or analyze the most requested API paths to pinpoint high-egress endpoints. ### Frequent database queries diff --git a/apps/docs/content/guides/storage/production/scaling.mdx b/apps/docs/content/guides/storage/production/scaling.mdx index c5f38e72eae1b..031ae6a62f97b 100644 --- a/apps/docs/content/guides/storage/production/scaling.mdx +++ b/apps/docs/content/guides/storage/production/scaling.mdx @@ -24,6 +24,10 @@ Using the browser cache can effectively lower your egress since the asset remain You have the option to set a maximum upload size for your bucket. Doing this can prevent users from uploading and then downloading excessively large files. You can control the maximum file size by configuring this option at the [bucket level](/docs/guides/storage/buckets/creating-buckets). +#### Smart CDN + +By leveraging our [Smart CDN](/docs/guides/storage/cdn/smart-cdn), you can achieve a higher cache hit rate and therefore lower your egress cached, as we charge less for cached egress (see [egress pricing](/docs/guides/platform/manage-your-usage/egress#pricing)). + ## Optimize listing objects Once you have a substantial number of objects, you might observe that the `supabase.storage.list()` method starts to slow down. This occurs because the endpoint is quite generic and attempts to retrieve both folders and objects in a single query. While this approach is very useful for building features like the Storage viewer on the Supabase dashboard, it can impact performance with a large number of objects. diff --git a/apps/docs/content/guides/storage/serving/bandwidth.mdx b/apps/docs/content/guides/storage/serving/bandwidth.mdx index 740931c242df7..021810276c495 100644 --- a/apps/docs/content/guides/storage/serving/bandwidth.mdx +++ b/apps/docs/content/guides/storage/serving/bandwidth.mdx @@ -8,44 +8,52 @@ sidebar_label: 'Bandwidth & Storage Egress' ## Bandwidth & Storage egress -Free Plan Organizations in Supabase have a limit of 5 GB of bandwidth. This limit is calculated by the sum of all the data transferred from the Supabase servers to the client. This includes all the data transferred from the database, storage, and functions. +Free Plan Organizations in Supabase have a limit of 10 GB of bandwidth (5 GB cached + 5 GB uncached). This limit is calculated by the sum of all the data transferred from the Supabase servers to the client. This includes all the data transferred from the database, storage, and functions. -### Checking Storage egress requests in Logs Explorer: +### Checking Storage egress requests in Logs Explorer We have a template query that you can use to get the number of requests for each object in [Logs Explorer](/dashboard/project/_/logs/explorer/templates). ```sql select - r.method as http_verb, - r.path as filepath, + request.method as http_verb, + request.path as filepath, + (responseHeaders.cf_cache_status = 'HIT') as cached, count(*) as num_requests from edge_logs - cross join unnest(metadata) as m - cross join unnest(m.request) as r - cross join unnest(r.headers) as h -where (path like '%storage/v1/object/%' or path like '%storage/v1/render/%') and r.method = 'GET' -group by r.path, r.method + cross join unnest(metadata) as metadata + cross join unnest(metadata.request) as request + cross join unnest(metadata.response) as response + cross join unnest(response.headers) as responseHeaders +where + (path like '%storage/v1/object/%' or path like '%storage/v1/render/%') + and request.method = 'GET' +group by 1, 2, 3 order by num_requests desc limit 100; ``` Example of the output: -``` +```json [ - {"filepath":"/storage/v1/object/sign/large%20bucket/20230902_200037.gif", - "http_verb":"GET", - "num_requests":100 - }, - {"filepath":"/storage/v1/object/public/demob/Sports/volleyball.png", - "http_verb":"GET", - "num_requests":168 - } + { + "filepath": "/storage/v1/object/sign/large%20bucket/20230902_200037.gif", + "http_verb": "GET", + "cached": true, + "num_requests": 100 + }, + { + "filepath": "/storage/v1/object/public/demob/Sports/volleyball.png", + "http_verb": "GET", + "cached": false, + "num_requests": 168 + } ] ``` -### Calculating egress: +### Calculating egress If you already know the size of those files, you can calculate the egress by multiplying the number of requests by the size of the file. You can also get the size of the file with the following cURL: @@ -67,6 +75,6 @@ Total Egress = 395.76MB You can see that these values can get quite large, so it's important to keep track of the egress and optimize the files. -### Optimizing egress: +### Optimizing egress -If you are on the Pro Plan, you can use the [Supabase Image Transformations](/docs/guides/storage/image-transformations) to optimize the images and reduce the egress. +See our [scaling tips for egress](/docs/guides/storage/production/scaling#egress). diff --git a/apps/docs/content/guides/storage/serving/image-transformations.mdx b/apps/docs/content/guides/storage/serving/image-transformations.mdx index 52a2ad73d2caf..952d6a79b11b0 100644 --- a/apps/docs/content/guides/storage/serving/image-transformations.mdx +++ b/apps/docs/content/guides/storage/serving/image-transformations.mdx @@ -259,7 +259,7 @@ response = supabase.storage.from_('bucket').download( When using the image transformation API, Storage will automatically find the best format supported by the client and return that to the client, without any code change. For instance, if you use Chrome when viewing a JPEG image and using transformation options, you'll see that images are automatically optimized as `webp` images. -As a result, this will lower the bandwidth that you send to your users and your application will load much faster. +As a result, this will lower the egress that you send to your users and your application will load much faster. diff --git a/apps/docs/content/guides/telemetry/reports.mdx b/apps/docs/content/guides/telemetry/reports.mdx index 34fbd117671a2..49ab17a59aa76 100644 --- a/apps/docs/content/guides/telemetry/reports.mdx +++ b/apps/docs/content/guides/telemetry/reports.mdx @@ -270,7 +270,7 @@ The Storage report provides visibility into how your Supabase Storage is being u | --------------- | ------------------------------------------ | ------------------------------------------------------ | | Total Requests | Overall request volume to Storage | Traffic patterns and usage trends | | Response Speed | Average response time for storage requests | Performance bottlenecks and optimization opportunities | -| Network Traffic | Ingress and egress bandwidth usage | Data transfer costs and CDN effectiveness | +| Network Traffic | Ingress and egress usage | Data transfer costs and CDN effectiveness | | Request Caching | Cache hit rates and miss patterns | CDN performance and cost optimization | | Top Routes | Most frequently accessed storage paths | Popular content and usage patterns | @@ -305,5 +305,5 @@ The API Gateway report analyzes traffic patterns and performance characteristics | Total Requests | Overall API request volume | Traffic patterns and growth trends | | Response Errors | Error rates with 4XX and 5XX status codes | API reliability and user experience issues | | Response Speed | Average API response times | Performance bottlenecks and optimization targets | -| Network Traffic | Request and response bandwidth usage | Data transfer patterns and cost implications | +| Network Traffic | Request and response egress usage | Data transfer patterns and cost implications | | Top Routes | Most frequently accessed API endpoints | Usage patterns and optimization priorities | diff --git a/apps/docs/content/troubleshooting/all-about-supabase-egress-a_Sg_e.mdx b/apps/docs/content/troubleshooting/all-about-supabase-egress-a_Sg_e.mdx index 78cd4f9dde736..974ce7498ac4d 100644 --- a/apps/docs/content/troubleshooting/all-about-supabase-egress-a_Sg_e.mdx +++ b/apps/docs/content/troubleshooting/all-about-supabase-egress-a_Sg_e.mdx @@ -31,4 +31,10 @@ While pointing out the exact cause for egress may not be straightforward, there - Reduce the number of queries/calls by optimising client code or use caches to reduce the number of requests/queries being done: https://github.com/psteinroe/supabase-cache-helpers/ - In case of update/insert queries, if you don’t need the entire row to be returned, configure your ORM/queries to not return the entire row - In case of running manual backups through Supavisor, remove unneeded tables and/or reduce the frequency -- For Storage, if you start using the [Smart CDN](https://supabase.com/docs/guides/storage/cdn/smart-cdn) Storage Egress usage can be managed. You can also use the [Supabase Image Transformations](https://supabase.com/docs/guides/storage/image-transformations) to optimize the images and reduce the egress. +- For Storage, if you start using the [Smart CDN](https://supabase.com/docs/guides/storage/cdn/smart-cdn) Storage Egress usage can be reduced. You can also use the [Supabase Image Transformations](https://supabase.com/docs/guides/storage/image-transformations) to optimize the images and reduce the egress. + +**Cached vs uncached egress** + +We differentiate between cached and uncached egress. Cached egress refers to egress that is served via our CDN and hits the cache. Uncached egress, on the other hand, refers to egress that is not served from the cache and requires a fresh request to the origin server. + +Your plan includes a quota for both cached and uncached egress and these are independent. Cached egress is also cheaper in case you exceed your quota. diff --git a/apps/docs/features/docs/Reference.navigation.client.tsx b/apps/docs/features/docs/Reference.navigation.client.tsx index b714555561a34..485f6542c784d 100644 --- a/apps/docs/features/docs/Reference.navigation.client.tsx +++ b/apps/docs/features/docs/Reference.navigation.client.tsx @@ -214,7 +214,7 @@ export function RefLink({ 7m&`2qg60 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 TBBo6Mf3C}^Udz66=kfmn`{gcP literal 0 HcmV?d00001 diff --git a/apps/docs/public/img/guides/platform/cached-egress.png b/apps/docs/public/img/guides/platform/cached-egress.png new file mode 100644 index 0000000000000000000000000000000000000000..60ed3968ec4929e10962633e8af2a694c14b1418 GIT binary patch literal 64259 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 { +export const EnterpriseCard = ({ plan, isCurrentPlan }: EnterpriseCardProps) => { const { data: selectedOrganization } = useSelectedOrganizationQuery() const orgSlug = selectedOrganization?.slug - const features = pickFeatures(plan, billingPartner) + const features = plan.features const currentPlan = selectedOrganization?.plan.name const { mutate: sendEvent } = useSendEventMutation() diff --git a/apps/studio/components/interfaces/Organization/BillingSettings/Subscription/PlanUpdateSidePanel.tsx b/apps/studio/components/interfaces/Organization/BillingSettings/Subscription/PlanUpdateSidePanel.tsx index 4059c82a68251..f73603a57b5da 100644 --- a/apps/studio/components/interfaces/Organization/BillingSettings/Subscription/PlanUpdateSidePanel.tsx +++ b/apps/studio/components/interfaces/Organization/BillingSettings/Subscription/PlanUpdateSidePanel.tsx @@ -21,7 +21,7 @@ import { useSendEventMutation } from 'data/telemetry/send-event-mutation' import { useCheckPermissions } from 'hooks/misc/useCheckPermissions' import { useSelectedOrganizationQuery } from 'hooks/misc/useSelectedOrganization' import { formatCurrency } from 'lib/helpers' -import { pickFeatures, pickFooter, plans as subscriptionsPlans } from 'shared-data/plans' +import { plans as subscriptionsPlans } from 'shared-data/plans' import { useOrgSettingsPageStateSnapshot } from 'state/organization-settings' import { Button, SidePanel, cn } from 'ui' import DowngradeModal from './DowngradeModal' @@ -164,18 +164,11 @@ const PlanUpdateSidePanel = () => { const isDowngradeOption = getPlanChangeType(subscription?.plan.id, plan?.planId) === 'downgrade' const isCurrentPlan = planMeta?.id === subscription?.plan?.id - const features = pickFeatures(plan, billingPartner) - const footer = pickFooter(plan, billingPartner) + const features = plan.features + const footer = plan.footer if (plan.id === 'tier_enterprise') { - return ( - - ) + return } return ( diff --git a/apps/studio/components/interfaces/Organization/BillingSettings/Subscription/SubscriptionPlanUpdateDialog.tsx b/apps/studio/components/interfaces/Organization/BillingSettings/Subscription/SubscriptionPlanUpdateDialog.tsx index 3a5ff8dca18bc..46a5c8d02236f 100644 --- a/apps/studio/components/interfaces/Organization/BillingSettings/Subscription/SubscriptionPlanUpdateDialog.tsx +++ b/apps/studio/components/interfaces/Organization/BillingSettings/Subscription/SubscriptionPlanUpdateDialog.tsx @@ -187,11 +187,11 @@ export const SubscriptionPlanUpdateDialog = ({ }) } - const features = subscriptionPlanMeta?.features?.[0]?.features || [] + const features = subscriptionPlanMeta?.features || [] const topFeatures = features // Get current plan features for downgrade comparison - const currentPlanFeatures = currentPlanMeta?.features?.[0]?.features || [] + const currentPlanFeatures = currentPlanMeta?.features || [] // Features that will be lost when downgrading const featuresToLose = diff --git a/apps/studio/components/interfaces/Organization/BillingSettings/helpers.ts b/apps/studio/components/interfaces/Organization/BillingSettings/helpers.ts index 63c76f35d4d6f..9294e92daa14e 100644 --- a/apps/studio/components/interfaces/Organization/BillingSettings/helpers.ts +++ b/apps/studio/components/interfaces/Organization/BillingSettings/helpers.ts @@ -3,6 +3,7 @@ import { PricingMetric } from 'data/analytics/org-daily-stats-query' const pricingMetricBytes = [ PricingMetric.DATABASE_SIZE, PricingMetric.EGRESS, + PricingMetric.CACHED_EGRESS, PricingMetric.STORAGE_SIZE, ] diff --git a/apps/studio/components/interfaces/Organization/Usage/Bandwidth.tsx b/apps/studio/components/interfaces/Organization/Usage/Egress.tsx similarity index 71% rename from apps/studio/components/interfaces/Organization/Usage/Bandwidth.tsx rename to apps/studio/components/interfaces/Organization/Usage/Egress.tsx index f6bbf70669bcd..0ce4d8e2d9e00 100644 --- a/apps/studio/components/interfaces/Organization/Usage/Bandwidth.tsx +++ b/apps/studio/components/interfaces/Organization/Usage/Egress.tsx @@ -3,7 +3,7 @@ import { PricingMetric, useOrgDailyStatsQuery } from 'data/analytics/org-daily-s import type { OrgSubscription } from 'data/subscriptions/types' import UsageSection from './UsageSection/UsageSection' -export interface BandwidthProps { +export interface EgressProps { orgSlug: string projectRef?: string startDate: string | undefined @@ -12,14 +12,14 @@ export interface BandwidthProps { currentBillingCycleSelected: boolean } -const Bandwidth = ({ +const Egress = ({ orgSlug, projectRef, subscription, startDate, endDate, currentBillingCycleSelected, -}: BandwidthProps) => { +}: EgressProps) => { const { data: egressData, isLoading: isLoadingDbEgressData } = useOrgDailyStatsQuery({ orgSlug, projectRef, @@ -29,6 +29,15 @@ const Bandwidth = ({ endDate, }) + const { data: cachedEgressData, isLoading: isLoadingCachedEgress } = useOrgDailyStatsQuery({ + orgSlug, + projectRef, + metric: PricingMetric.CACHED_EGRESS, + interval: '1d', + startDate, + endDate, + }) + const chartMeta: { [key: string]: { data: DataPoint[]; margin: number; isLoading: boolean } } = { @@ -37,13 +46,18 @@ const Bandwidth = ({ margin: 16, isLoading: isLoadingDbEgressData, }, + [PricingMetric.CACHED_EGRESS]: { + data: cachedEgressData?.data ?? [], + margin: 16, + isLoading: isLoadingCachedEgress, + }, } return ( JSX.Element | null } -export type CategoryMetaKey = 'bandwidth' | 'sizeCount' | 'activity' | 'compute' +export type CategoryMetaKey = 'egress' | 'sizeCount' | 'activity' | 'compute' export interface CategoryMeta { key: CategoryMetaKey @@ -72,249 +72,285 @@ export interface CategoryMeta { export const USAGE_CATEGORIES: (subscription?: OrgSubscription) => CategoryMeta[] = ( subscription -) => [ - { - key: 'bandwidth', - name: 'Bandwidth', - description: 'Amount of data transmitted over all network connections', - attributes: [ - { - anchor: 'egress', - key: PricingMetric.EGRESS, - attributes: [ - { key: EgressType.AUTH, name: 'Auth Egress', color: 'yellow' }, - { key: EgressType.DATABASE, name: 'Database Egress', color: 'green' }, - { key: EgressType.STORAGE, name: 'Storage Egress', color: 'blue' }, - { key: EgressType.REALTIME, name: 'Realtime Egress', color: 'orange' }, - { key: EgressType.FUNCTIONS, name: 'Functions Egress', color: 'purple' }, - { key: EgressType.SUPAVISOR, name: 'Shared Pooler Egress', color: 'red' }, - { key: EgressType.LOGDRAIN, name: 'Logdrain Egress', color: 'teal' }, - ], - name: 'Total Egress', - unit: 'bytes', - description: - 'Contains any outgoing traffic including Database, Storage, Realtime, Auth, API, Edge Functions, Pooler and Log Drains.\nBilling is based on the total sum of egress in GB throughout your billing period.', - chartDescription: 'The data refreshes every 24 hours.', - }, - ], - }, - { - key: 'sizeCount', - name: 'Database & Storage Size', - description: 'Amount of resources your project is consuming', - attributes: [ - subscription?.plan.id === 'free' - ? { - anchor: 'dbSize', - key: PricingMetric.DATABASE_SIZE, - attributes: [{ key: PricingMetric.DATABASE_SIZE.toLowerCase(), color: 'white' }], - name: 'Database size', - chartPrefix: 'Average', - unit: 'bytes', - description: - 'Database size refers to the actual amount of space used by all your database objects, as reported by Postgres.', - links: [ - { - name: 'Documentation', - url: 'https://supabase.com/docs/guides/platform/database-size', - }, - ], - chartDescription: 'The data refreshes every 24 hours.', - additionalInfo: (usage?: OrgUsageResponse) => { - const usageMeta = usage?.usages.find((x) => x.metric === PricingMetric.DATABASE_SIZE) - const usageRatio = - typeof usageMeta !== 'number' - ? (usageMeta?.usage ?? 0) / (usageMeta?.pricing_free_units ?? 0) - : 0 - const hasLimit = usageMeta && (usageMeta?.pricing_free_units ?? 0) > 0 +) => { + const egressAttributes: CategoryAttribute[] = [ + { + anchor: 'egress', + key: PricingMetric.EGRESS, + attributes: [ + { key: EgressType.AUTH, name: 'Auth Egress', color: 'yellow' }, + { key: EgressType.DATABASE, name: 'Database Egress', color: 'green' }, + { key: EgressType.STORAGE, name: 'Storage Egress', color: 'blue' }, + { key: EgressType.REALTIME, name: 'Realtime Egress', color: 'orange' }, + { key: EgressType.FUNCTIONS, name: 'Functions Egress', color: 'purple' }, + { key: EgressType.SUPAVISOR, name: 'Shared Pooler Egress', color: 'red' }, + { key: EgressType.LOGDRAIN, name: 'Logdrain Egress', color: 'teal' }, + ], + name: 'Egress', + unit: 'bytes', + description: + subscription?.cached_egress_enabled === true + ? 'Contains any outgoing traffic including Database, Storage, Realtime, Auth, API, Edge Functions, Pooler and Log Drains.\nBilling is based on the total sum of uncached egress in GB throughout your billing period.\nEgress via cache hits is billed separately.' + : 'Contains any outgoing traffic including Database, Storage, Realtime, Auth, API, Edge Functions, Pooler and Log Drains.\nBilling is based on the total sum of uncached egress in GB throughout your billing period.', + chartDescription: + 'The breakdown of different egress types is inclusive of cached egress, even though it is billed separately. The data refreshes every 24 hours.', + links: [ + { + name: 'Documentation', + url: 'https://supabase.com/docs/guides/platform/manage-your-usage/egress', + }, + ], + }, + ] + + if (subscription?.cached_egress_enabled) { + egressAttributes.push({ + anchor: 'cachedEgress', + key: PricingMetric.CACHED_EGRESS, + attributes: [{ key: PricingMetric.CACHED_EGRESS.toLowerCase(), color: 'white' }], + name: 'Cached Egress', + unit: 'bytes', + description: + 'Contains any outgoing traffic that is served from a cache hit. Includes API, Storage and Edge Functions.\nBilling is based on the total sum of cached egress in GB throughout your billing period.', + chartDescription: 'The data refreshes every 24 hours.', + links: [ + { + name: 'Documentation', + url: 'https://supabase.com/docs/guides/platform/manage-your-usage/egress', + }, + ], + }) + } - const isApproachingLimit = hasLimit && usageRatio >= USAGE_APPROACHING_THRESHOLD - const isExceededLimit = hasLimit && usageRatio >= 1 - const isCapped = usageMeta?.capped + return [ + { + key: 'egress', + name: 'Egress', + description: 'Amount of data transmitted over all network connections', + attributes: egressAttributes, + }, + { + key: 'sizeCount', + name: 'Database & Storage Size', + description: 'Amount of resources your project is consuming', + attributes: [ + subscription?.plan.id === 'free' + ? { + anchor: 'dbSize', + key: PricingMetric.DATABASE_SIZE, + attributes: [{ key: PricingMetric.DATABASE_SIZE.toLowerCase(), color: 'white' }], + name: 'Database size', + chartPrefix: 'Average', + unit: 'bytes', + description: + 'Database size refers to the actual amount of space used by all your database objects, as reported by Postgres.', + links: [ + { + name: 'Documentation', + url: 'https://supabase.com/docs/guides/platform/database-size', + }, + ], + chartDescription: 'The data refreshes every 24 hours.', + additionalInfo: (usage?: OrgUsageResponse) => { + const usageMeta = usage?.usages.find( + (x) => x.metric === PricingMetric.DATABASE_SIZE + ) + const usageRatio = + typeof usageMeta !== 'number' + ? (usageMeta?.usage ?? 0) / (usageMeta?.pricing_free_units ?? 0) + : 0 + const hasLimit = usageMeta && (usageMeta?.pricing_free_units ?? 0) > 0 - const onFreePlan = subscription?.plan?.name === 'Free' + const isApproachingLimit = hasLimit && usageRatio >= USAGE_APPROACHING_THRESHOLD + const isExceededLimit = hasLimit && usageRatio >= 1 + const isCapped = usageMeta?.capped - return ( -
- {(isApproachingLimit || isExceededLimit) && isCapped && ( - -
-
- When you reach your database size limit, your project can go into - read-only mode.{' '} - {onFreePlan - ? 'Please upgrade your Plan.' - : "Disable your spend cap to scale seamlessly, and pay for over-usage beyond your Plan's quota."} + const onFreePlan = subscription?.plan?.name === 'Free' + + return ( +
+ {(isApproachingLimit || isExceededLimit) && isCapped && ( + +
+
+ When you reach your database size limit, your project can go into + read-only mode.{' '} + {onFreePlan + ? 'Please upgrade your Plan.' + : "Disable your spend cap to scale seamlessly, and pay for over-usage beyond your Plan's quota."} +
-
- - )} -
- ) - }, - } - : { - anchor: 'diskSize', - key: 'diskSize', - attributes: [], - name: 'Disk size', - chartPrefix: 'Average', - unit: 'bytes', - description: - "Each Supabase project comes with a dedicated disk. Each project gets 8 GB of disk for free. Billing is based on the provisioned disk size. Disk automatically scales up when you get close to it's size.\nEach hour your project is using more than 8 GB of GP3 disk, it incurs the overages in GB-Hrs, i.e. a 16 GB disk incurs 8 GB-Hrs every hour. Extra disk size costs $0.125/GB/month ($0.000171/GB-Hr).", - links: [ - { - name: 'Documentation', - url: 'https://supabase.com/docs/guides/platform/manage-your-usage/disk-size', + + )} +
+ ) }, - { - name: 'Disk Management', - url: 'https://supabase.com/docs/guides/platform/database-size#disk-management', - }, - ], - chartDescription: '', - }, - { - anchor: 'storageSize', - key: PricingMetric.STORAGE_SIZE, - attributes: [{ key: PricingMetric.STORAGE_SIZE.toLowerCase(), color: 'white' }], - name: 'Storage Size', - chartPrefix: 'Average', - unit: 'bytes', - description: - 'Sum of all objects in your storage buckets.\nBilling is prorated down to the hour and will be displayed GB-Hrs.', - chartDescription: 'The data refreshes every 24 hours.', - links: [ - { - name: 'Storage', - url: 'https://supabase.com/docs/guides/storage', - }, - ], - }, - ], - }, - { - key: 'activity', - name: 'Activity', - description: 'Usage statistics that reflect the activity of your project', - attributes: [ - { - anchor: 'mau', - key: PricingMetric.MONTHLY_ACTIVE_USERS, - attributes: [{ key: PricingMetric.MONTHLY_ACTIVE_USERS.toLowerCase(), color: 'white' }], - name: 'Monthly Active Users', - chartPrefix: 'Cumulative', - chartSuffix: 'in billing period', - unit: 'absolute', - description: - 'Users who log in or refresh their token count towards MAU.\nBilling is based on the sum of distinct users requesting your API throughout the billing period. Resets every billing cycle.', - chartDescription: - 'The data is refreshed over a period of 24 hours and resets at the beginning of every billing period.\nThe data points are relative to the beginning of your billing period and will reset with your billing period.', - links: [ - { - name: 'Auth', - url: 'https://supabase.com/docs/guides/auth', - }, - ], - }, - { - anchor: 'mauSso', - key: PricingMetric.MONTHLY_ACTIVE_SSO_USERS, - attributes: [{ key: PricingMetric.MONTHLY_ACTIVE_SSO_USERS.toLowerCase(), color: 'white' }], - name: 'Monthly Active SSO Users', - chartPrefix: 'Cumulative', - chartSuffix: 'in billing period', - unit: 'absolute', - description: - 'SSO users who log in or refresh their token count towards SSO MAU.\nBilling is based on the sum of distinct Single Sign-On users requesting your API throughout the billing period. Resets every billing cycle.', - chartDescription: - 'The data refreshes over a period of 24 hours and resets at the beginning of every billing period.\nThe data points are relative to the beginning of your billing period and will reset with your billing period.', - links: [ - { - name: 'SSO with SAML 2.0', - url: 'https://supabase.com/docs/guides/auth/sso/auth-sso-saml', - }, - ], - }, - { - anchor: 'storageImageTransformations', - key: PricingMetric.STORAGE_IMAGES_TRANSFORMED, - attributes: [ - { key: PricingMetric.STORAGE_IMAGES_TRANSFORMED.toLowerCase(), color: 'white' }, - ], - name: 'Storage Image Transformations', - chartPrefix: 'Cumulative', - chartSuffix: 'in billing period', - unit: 'absolute', - description: - 'We count all images that were transformed in the billing period, ignoring any transformations.\nUsage example: You transform one image with four different size transformations and another image with just a single transformation. It counts as two, as only two images were transformed.\nBilling is based on the count of (origin) images that used transformations throughout the billing period. Resets every billing cycle.', - chartDescription: - 'The data refreshes every 24 hours.\nThe data points are relative to the beginning of your billing period and will reset with your billing period.', - links: [ - { - name: 'Documentation', - url: 'https://supabase.com/docs/guides/storage/image-transformations', - }, - ], - }, - { - anchor: 'funcInvocations', - key: PricingMetric.FUNCTION_INVOCATIONS, - attributes: [{ key: PricingMetric.FUNCTION_INVOCATIONS.toLowerCase(), color: 'white' }], - name: 'Edge Function Invocations', - unit: 'absolute', - description: - 'Every serverless function invocation independent of response status is counted.\nBilling is based on the sum of all invocations throughout your billing period.', - chartDescription: 'The data refreshes every 24 hours.', - links: [ - { - name: 'Edge Functions', - url: 'https://supabase.com/docs/guides/functions', - }, - ], - }, - { - anchor: 'realtimeMessageCount', - key: PricingMetric.REALTIME_MESSAGE_COUNT, - attributes: [{ key: PricingMetric.REALTIME_MESSAGE_COUNT.toLowerCase(), color: 'white' }], - name: 'Realtime Messages', - unit: 'absolute', - description: - "Count of messages going through Realtime. Includes database changes, broadcast and presence. \nUsage example: If you do a database change and 5 clients listen to that change via Realtime, that's 5 messages. If you broadcast a message and 4 clients listen to that, that's 5 messages (1 message sent, 4 received).\nBilling is based on the total amount of messages throughout your billing period.", - chartDescription: 'The data refreshes every 24 hours.', - links: [ - { - name: 'Realtime Quotas', - url: 'https://supabase.com/docs/guides/realtime/quotas', - }, - ], - }, - { - anchor: 'realtimePeakConnections', - key: PricingMetric.REALTIME_PEAK_CONNECTIONS, - attributes: [ - { key: PricingMetric.REALTIME_PEAK_CONNECTIONS.toLowerCase(), color: 'white' }, - ], - name: 'Realtime Concurrent Peak Connections', - chartPrefix: 'Max', - unit: 'absolute', - description: - 'Total number of successful connections. Connections attempts are not counted towards usage.\nBilling is based on the maximum amount of concurrent peak connections throughout your billing period.', - chartDescription: 'The data refreshes every 24 hours.', - links: [ - { - name: 'Realtime Quotas', - url: 'https://supabase.com/docs/guides/realtime/quotas', - }, - ], - }, - ], - }, -] + } + : { + anchor: 'diskSize', + key: 'diskSize', + attributes: [], + name: 'Disk size', + chartPrefix: 'Average', + unit: 'bytes', + description: + "Each Supabase project comes with a dedicated disk. Each project gets 8 GB of disk for free. Billing is based on the provisioned disk size. Disk automatically scales up when you get close to it's size.\nEach hour your project is using more than 8 GB of GP3 disk, it incurs the overages in GB-Hrs, i.e. a 16 GB disk incurs 8 GB-Hrs every hour. Extra disk size costs $0.125/GB/month ($0.000171/GB-Hr).", + links: [ + { + name: 'Documentation', + url: 'https://supabase.com/docs/guides/platform/manage-your-usage/disk-size', + }, + { + name: 'Disk Management', + url: 'https://supabase.com/docs/guides/platform/database-size#disk-management', + }, + ], + chartDescription: '', + }, + { + anchor: 'storageSize', + key: PricingMetric.STORAGE_SIZE, + attributes: [{ key: PricingMetric.STORAGE_SIZE.toLowerCase(), color: 'white' }], + name: 'Storage Size', + chartPrefix: 'Average', + unit: 'bytes', + description: + 'Sum of all objects in your storage buckets.\nBilling is prorated down to the hour and will be displayed GB-Hrs.', + chartDescription: 'The data refreshes every 24 hours.', + links: [ + { + name: 'Storage', + url: 'https://supabase.com/docs/guides/storage', + }, + ], + }, + ], + }, + { + key: 'activity', + name: 'Activity', + description: 'Usage statistics that reflect the activity of your project', + attributes: [ + { + anchor: 'mau', + key: PricingMetric.MONTHLY_ACTIVE_USERS, + attributes: [{ key: PricingMetric.MONTHLY_ACTIVE_USERS.toLowerCase(), color: 'white' }], + name: 'Monthly Active Users', + chartPrefix: 'Cumulative', + chartSuffix: 'in billing period', + unit: 'absolute', + description: + 'Users who log in or refresh their token count towards MAU.\nBilling is based on the sum of distinct users requesting your API throughout the billing period. Resets every billing cycle.', + chartDescription: + 'The data is refreshed over a period of 24 hours and resets at the beginning of every billing period.\nThe data points are relative to the beginning of your billing period and will reset with your billing period.', + links: [ + { + name: 'Auth', + url: 'https://supabase.com/docs/guides/auth', + }, + ], + }, + { + anchor: 'mauSso', + key: PricingMetric.MONTHLY_ACTIVE_SSO_USERS, + attributes: [ + { key: PricingMetric.MONTHLY_ACTIVE_SSO_USERS.toLowerCase(), color: 'white' }, + ], + name: 'Monthly Active SSO Users', + chartPrefix: 'Cumulative', + chartSuffix: 'in billing period', + unit: 'absolute', + description: + 'SSO users who log in or refresh their token count towards SSO MAU.\nBilling is based on the sum of distinct Single Sign-On users requesting your API throughout the billing period. Resets every billing cycle.', + chartDescription: + 'The data refreshes over a period of 24 hours and resets at the beginning of every billing period.\nThe data points are relative to the beginning of your billing period and will reset with your billing period.', + links: [ + { + name: 'SSO with SAML 2.0', + url: 'https://supabase.com/docs/guides/auth/sso/auth-sso-saml', + }, + ], + }, + { + anchor: 'storageImageTransformations', + key: PricingMetric.STORAGE_IMAGES_TRANSFORMED, + attributes: [ + { key: PricingMetric.STORAGE_IMAGES_TRANSFORMED.toLowerCase(), color: 'white' }, + ], + name: 'Storage Image Transformations', + chartPrefix: 'Cumulative', + chartSuffix: 'in billing period', + unit: 'absolute', + description: + 'We count all images that were transformed in the billing period, ignoring any transformations.\nUsage example: You transform one image with four different size transformations and another image with just a single transformation. It counts as two, as only two images were transformed.\nBilling is based on the count of (origin) images that used transformations throughout the billing period. Resets every billing cycle.', + chartDescription: + 'The data refreshes every 24 hours.\nThe data points are relative to the beginning of your billing period and will reset with your billing period.', + links: [ + { + name: 'Documentation', + url: 'https://supabase.com/docs/guides/storage/image-transformations', + }, + ], + }, + { + anchor: 'funcInvocations', + key: PricingMetric.FUNCTION_INVOCATIONS, + attributes: [{ key: PricingMetric.FUNCTION_INVOCATIONS.toLowerCase(), color: 'white' }], + name: 'Edge Function Invocations', + unit: 'absolute', + description: + 'Every serverless function invocation independent of response status is counted.\nBilling is based on the sum of all invocations throughout your billing period.', + chartDescription: 'The data refreshes every 24 hours.', + links: [ + { + name: 'Edge Functions', + url: 'https://supabase.com/docs/guides/functions', + }, + ], + }, + { + anchor: 'realtimeMessageCount', + key: PricingMetric.REALTIME_MESSAGE_COUNT, + attributes: [{ key: PricingMetric.REALTIME_MESSAGE_COUNT.toLowerCase(), color: 'white' }], + name: 'Realtime Messages', + unit: 'absolute', + description: + "Count of messages going through Realtime. Includes database changes, broadcast and presence. \nUsage example: If you do a database change and 5 clients listen to that change via Realtime, that's 5 messages. If you broadcast a message and 4 clients listen to that, that's 5 messages (1 message sent, 4 received).\nBilling is based on the total amount of messages throughout your billing period.", + chartDescription: 'The data refreshes every 24 hours.', + links: [ + { + name: 'Realtime Quotas', + url: 'https://supabase.com/docs/guides/realtime/quotas', + }, + ], + }, + { + anchor: 'realtimePeakConnections', + key: PricingMetric.REALTIME_PEAK_CONNECTIONS, + attributes: [ + { key: PricingMetric.REALTIME_PEAK_CONNECTIONS.toLowerCase(), color: 'white' }, + ], + name: 'Realtime Concurrent Peak Connections', + chartPrefix: 'Max', + unit: 'absolute', + description: + 'Total number of successful connections. Connections attempts are not counted towards usage.\nBilling is based on the maximum amount of concurrent peak connections throughout your billing period.', + chartDescription: 'The data refreshes every 24 hours.', + links: [ + { + name: 'Realtime Quotas', + url: 'https://supabase.com/docs/guides/realtime/quotas', + }, + ], + }, + ], + }, + ] +} diff --git a/apps/studio/components/interfaces/Organization/Usage/Usage.tsx b/apps/studio/components/interfaces/Organization/Usage/Usage.tsx index 93796f2623efc..80a68493edf73 100644 --- a/apps/studio/components/interfaces/Organization/Usage/Usage.tsx +++ b/apps/studio/components/interfaces/Organization/Usage/Usage.tsx @@ -22,7 +22,7 @@ import { cn, Listbox } from 'ui' import { Admonition } from 'ui-patterns' import { Restriction } from '../BillingSettings/Restriction' import Activity from './Activity' -import Bandwidth from './Bandwidth' +import Egress from './Egress' import Compute from './Compute' import SizeAndCounts from './SizeAndCounts' import TotalUsage from './TotalUsage' @@ -234,7 +234,7 @@ const Usage = () => { /> )} - billing_partner: 'fly', scheduled_plan_change: null, customer_balance: 0, + cached_egress_enabled: false, } return res.status(200).json(response) diff --git a/apps/www/components/Pricing/PricingPlans.tsx b/apps/www/components/Pricing/PricingPlans.tsx index fade95855d958..fc5c42fb5a20e 100644 --- a/apps/www/components/Pricing/PricingPlans.tsx +++ b/apps/www/components/Pricing/PricingPlans.tsx @@ -1,7 +1,7 @@ import Link from 'next/link' import { Check } from 'lucide-react' -import { pickFeatures, pickFooter, plans } from 'shared-data/plans' +import { plans } from 'shared-data/plans' import { Button, cn } from 'ui' import { Organization } from '~/data/organizations' import { useSendTelemetryEvent } from '~/lib/telemetry' @@ -23,8 +23,8 @@ const PricingPlans = ({ organizations, hasExistingOrganizations }: PricingPlansP const isProPlan = plan.name === 'Pro' const isTeamPlan = plan.name === 'Team' const isUpgradablePlan = isProPlan || isTeamPlan - const features = pickFeatures(plan) - const footer = pickFooter(plan) + const features = plan.features + const footer = plan.footer const sendPricingEvent = () => { sendTelemetryEvent({ diff --git a/apps/www/components/Pricing/PricingTableRow.tsx b/apps/www/components/Pricing/PricingTableRow.tsx index 4ea9e4f01ded6..9e92602131832 100644 --- a/apps/www/components/Pricing/PricingTableRow.tsx +++ b/apps/www/components/Pricing/PricingTableRow.tsx @@ -35,9 +35,11 @@ export const pricingTooltips: PricingTooltips = { 'database.pausing': { main: 'Projects that have no activity or API requests will be paused. They can be reactivated via the dashboard.', }, - - 'database.bandwidth': { - main: 'Billing is based on the total sum of all outgoing traffic (includes Database, Storage, Realtime, Auth, API, Edge Functions, Supavisor, Log Drains) in GB throughout your billing period.', + 'database.egress': { + main: 'Billing is based on the total sum of all outgoing traffic (includes Database, Storage, Realtime, Auth, API, Edge Functions, Supavisor, Log Drains) in GB throughout your billing period. Excludes cache hits.', + }, + 'database.cachedEgress': { + main: 'Billing is based on the total sum of any outgoing traffic (includes Database, Storage, API, Edge Functions) in GB throughout your billing period that is served from our CDN cache.', }, 'auth.totalUsers': { main: 'The maximum number of users your project can have', @@ -84,7 +86,7 @@ export const pricingTooltips: PricingTooltips = { main: "Count of messages going through Realtime. Includes database changes, broadcast and presence. \nUsage example: If you do a database change and 5 clients listen to that change via Realtime, that's 5 messages. If you broadcast a message and 4 clients listen to that, that's 5 messages (1 message sent, 4 received).\nBilling is based on the total amount of messages throughout your billing period.", }, 'security.logDrain': { - main: 'Only events processed and sent to destinations are counted. Bandwidth required to export logs count towards usage.\nEgress through Log Drains is rolled up into the unified egress and benefits from the unified egress quota.', + main: 'Only events processed and sent to destinations are counted. Egress required to export logs count towards usage.\nEgress through Log Drains is rolled up into the unified egress and benefits from the unified egress quota.', }, 'security.hipaa': { main: 'Available as a paid add-on on Team Plan and above.', diff --git a/apps/www/data/features.tsx b/apps/www/data/features.tsx index f2907db1572bb..7ddd5894727fb 100644 --- a/apps/www/data/features.tsx +++ b/apps/www/data/features.tsx @@ -1189,7 +1189,7 @@ Supabase's Smart CDN automatically synchronizes asset metadata to the edge, ensu - Content freshness: Users always receive the most recent version of assets. - Reduced origin load: Minimize requests to the origin server by optimizing edge caching. - Improved user experience: Deliver fast-loading, up-to-date content globally. -- Cost optimization: Reduce bandwidth costs by serving more content from the edge. +- Cost optimization: Reduce egress costs by serving more content from the edge. ## The Smart CDN feature is valuable for: - Dynamic websites with frequently updated content @@ -1224,14 +1224,14 @@ Supabase’s Image Transformations feature enables developers to dynamically man 1. Dynamic resizing: Adjust image dimensions using width and height parameters to suit various display requirements. 2. Quality control: Set image quality on a scale from 20 to 100 to balance visual fidelity and file size. 3. Resize modes: Choose from ‘cover’, ‘contain’, or ‘fill’ to control how images fit within specified dimensions. -4. Automatic format optimization: Automatically convert images to WebP format for supported browsers, enhancing load times and reducing bandwidth usage. +4. Automatic format optimization: Automatically convert images to WebP format for supported browsers, enhancing load times and reducing egress usage. 5. Flexible implementation: Utilize with public URLs, signed URLs, or direct downloads to fit various access control needs ([Server-side Auth](/features/server-side-auth)). 6. [Next.js integration](/nextjs): Leverage a custom loader for optimized image handling in Next.js applications. 7. Self-hosting option: Deploy your own image transformation service using Imgproxy for greater control and customization. ## Benefits: -- Performance optimization: Reduce bandwidth usage and improve load times with optimized images. +- Performance optimization: Reduce egress usage and improve load times with optimized images. - Storage efficiency: Store a single high-quality version and generate variants as needed. - Responsive design support: Serve appropriately sized images for different devices and layouts. - Simplified workflow: Automate image processing tasks, reducing the need for manual intervention and third-party tools. @@ -1240,7 +1240,7 @@ Supabase’s Image Transformations feature enables developers to dynamically man - Responsive web applications: Deliver images optimized for various screen sizes and resolutions. - Ecommerce platforms: Showcase product images in multiple sizes without storing redundant files. - Content management systems (CMS): Adapt images for different layouts and templates dynamically. -- Mobile applications: Optimize images for devices with varying bandwidth and display capabilities. +- Mobile applications: Optimize images for devices with varying egress and display capabilities. - High-volume image handling: Efficiently manage and serve large quantities of images in diverse contexts with [resumable uploads](/features/resumable-uploads). Supabase's Image Transformations feature enables you to efficiently manage and serve optimized images, improving your application's performance and user experience while saving time and resources. @@ -1274,11 +1274,11 @@ Supabase provides a custom loader for Next.js, allowing seamless integration of ### Are there any limitations on image size or dimensions? -While Supabase does not impose strict limits on image sizes, it’s recommended to optimize images for web use to ensure faster load times and better performance. Large images may consume more bandwidth and affect loading speeds. +While Supabase does not impose strict limits on image sizes, it’s recommended to optimize images for web use to ensure faster load times and better performance. Large images may consume more egress and affect loading speeds. ### How does automatic format optimization work? -Automatic format optimization detects the capabilities of the user’s browser and serves the most efficient image format supported, such as WebP. This enhances loading times and reduces bandwidth usage without compromising image quality. +Automatic format optimization detects the capabilities of the user’s browser and serves the most efficient image format supported, such as WebP. This enhances loading times and reduces egress usage without compromising image quality. `, icon: Image, diff --git a/packages/api-types/types/api.d.ts b/packages/api-types/types/api.d.ts index 810894d44726c..2dbe7af567647 100644 --- a/packages/api-types/types/api.d.ts +++ b/packages/api-types/types/api.d.ts @@ -1773,6 +1773,7 @@ export interface components { external_twitter_client_id: string | null external_twitter_enabled: boolean | null external_twitter_secret: string | null + external_web3_ethereum_enabled: boolean | null external_web3_solana_enabled: boolean | null external_workos_client_id: string | null external_workos_enabled: boolean | null @@ -2925,6 +2926,7 @@ export interface components { external_twitter_client_id?: string | null external_twitter_enabled?: boolean | null external_twitter_secret?: string | null + external_web3_ethereum_enabled?: boolean | null external_web3_solana_enabled?: boolean | null external_workos_client_id?: string | null external_workos_enabled?: boolean | null diff --git a/packages/api-types/types/platform.d.ts b/packages/api-types/types/platform.d.ts index c5e541ff155a0..f574a4d98f5b3 100644 --- a/packages/api-types/types/platform.d.ts +++ b/packages/api-types/types/platform.d.ts @@ -5811,6 +5811,7 @@ export interface components { /** @enum {string} */ billing_partner?: 'fly' | 'aws' | 'vercel_marketplace' billing_via_partner: boolean + cached_egress_enabled: boolean current_period_end: number current_period_start: number customer_balance: number @@ -6089,6 +6090,7 @@ export interface components { EXTERNAL_TWITTER_CLIENT_ID: string EXTERNAL_TWITTER_ENABLED: boolean EXTERNAL_TWITTER_SECRET: string + EXTERNAL_WEB3_ETHEREUM_ENABLED: boolean EXTERNAL_WEB3_SOLANA_ENABLED: boolean EXTERNAL_WORKOS_CLIENT_ID: string EXTERNAL_WORKOS_ENABLED: boolean @@ -6720,6 +6722,7 @@ export interface components { /** @enum {string} */ metric: | 'EGRESS' + | 'CACHED_EGRESS' | 'DATABASE_SIZE' | 'STORAGE_SIZE' | 'MONTHLY_ACTIVE_USERS' @@ -8287,6 +8290,7 @@ export interface components { /** @enum {string} */ usage_metric?: | 'EGRESS' + | 'CACHED_EGRESS' | 'DATABASE_SIZE' | 'STORAGE_SIZE' | 'MONTHLY_ACTIVE_USERS' @@ -8550,6 +8554,7 @@ export interface components { EXTERNAL_TWITTER_CLIENT_ID?: string | null EXTERNAL_TWITTER_ENABLED?: boolean | null EXTERNAL_TWITTER_SECRET?: string | null + EXTERNAL_WEB3_ETHEREUM_ENABLED?: boolean | null EXTERNAL_WEB3_SOLANA_ENABLED?: boolean | null EXTERNAL_WORKOS_CLIENT_ID?: string | null EXTERNAL_WORKOS_ENABLED?: boolean | null @@ -11893,6 +11898,7 @@ export interface operations { interval?: string metric: | 'EGRESS' + | 'CACHED_EGRESS' | 'DATABASE_SIZE' | 'STORAGE_SIZE' | 'MONTHLY_ACTIVE_USERS' @@ -17744,6 +17750,7 @@ export interface operations { | 'total_realtime_egress' | 'total_ingress' | 'total_egress' + | 'total_cached_egress' | 'total_requests' | 'total_get_requests' | 'total_patch_requests' diff --git a/packages/shared-data/plans.ts b/packages/shared-data/plans.ts index d0b7d9d61ec4d..475160225c563 100644 --- a/packages/shared-data/plans.ts +++ b/packages/shared-data/plans.ts @@ -13,8 +13,8 @@ export interface PricingInformation { warningTooltip?: string description: string preface: string - features: { partners: string[]; features: (string | string[])[] }[] - footer?: { partners: string[]; footer: string }[] + features: (string | string[])[] + footer?: string cta: string } @@ -31,38 +31,15 @@ export const plans: PricingInformation[] = [ description: 'Perfect for passion projects & simple websites.', preface: 'Get started with:', features: [ - { - partners: [], - features: [ - 'Unlimited API requests', - '50,000 monthly active users', - ['500 MB database size', 'Shared CPU • 500 MB RAM'], - '5 GB bandwidth', - '1 GB file storage', - 'Community support', - ], - }, - { - partners: ['fly'], - features: [ - 'Unlimited API requests', - '50,000 monthly active users', - ['500 MB database size', 'Shared CPU • 500 MB RAM'], - '5 GB bandwidth', - 'Community support', - ], - }, - ], - footer: [ - { - partners: [], - footer: 'Free projects are paused after 1 week of inactivity. Limit of 2 active projects.', - }, - { - partners: ['fly'], - footer: 'Free projects are paused after 1 week of inactivity. Limit of 1 active project.', - }, + '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', + 'Community support', ], + footer: 'Free projects are paused after 1 week of inactivity. Limit of 2 active projects.', cta: 'Start for Free', }, { @@ -77,28 +54,14 @@ export const plans: PricingInformation[] = [ priceMonthly: 25, description: 'For production applications with the power to scale.', features: [ - { - partners: [], - features: [ - ['100,000 monthly active users', 'then $0.00325 per MAU'], - ['8 GB disk size per project', 'then $0.125 per GB'], - ['250 GB bandwidth', 'then $0.09 per GB'], - ['100 GB file storage', 'then $0.021 per GB'], - 'Email support', - 'Daily backups stored for 7 days', - '7-day log retention', - ], - }, - { - partners: ['fly'], - features: [ - ['8 GB disk size per project', 'then $0.125 per GB'], - ['250 GB bandwidth', 'then $0.09 per GB'], - 'Email support', - 'Daily backups stored for 7 days', - '7-day log retention', - ], - }, + ['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.021 per GB'], + 'Email support', + 'Daily backups stored for 7 days', + '7-day log retention', ], preface: 'Everything in the Free Plan, plus:', cta: 'Get Started', @@ -115,18 +78,13 @@ export const plans: PricingInformation[] = [ priceMonthly: 599, description: 'Add features such as SSO, control over backups, and industry certifications.', features: [ - { - partners: [], - features: [ - 'SOC2', - 'Project-scoped and read-only access', - 'HIPAA available as paid add-on', - 'SSO for Supabase Dashboard', - 'Priority email support & SLAs', - 'Daily backups stored for 14 days', - '28-day log retention', - ], - }, + 'SOC2', + 'Project-scoped and read-only access', + 'HIPAA available as paid add-on', + 'SSO for Supabase Dashboard', + 'Priority email support & SLAs', + 'Daily backups stored for 14 days', + '28-day log retention', ], preface: 'Everything in the Pro Plan, plus:', cta: 'Get Started', @@ -138,17 +96,12 @@ export const plans: PricingInformation[] = [ href: 'https://forms.supabase.com/enterprise', description: 'For large-scale applications running Internet scale workloads.', features: [ - { - partners: [], - features: [ - 'Designated Support manager', - 'Uptime SLAs', - 'BYO Cloud supported', - '24×7×365 premium enterprise support', - 'Private Slack channel', - 'Custom Security Questionnaires', - ], - }, + 'Designated Support manager', + 'Uptime SLAs', + 'BYO Cloud supported', + '24×7×365 premium enterprise support', + 'Private Slack channel', + 'Custom Security Questionnaires', ], priceLabel: '', priceMonthly: 'Custom', @@ -156,17 +109,3 @@ export const plans: PricingInformation[] = [ cta: 'Contact Us', }, ] as const - -export function pickFeatures(plan: PricingInformation, billingPartner: string = '') { - return ( - plan.features.find((f) => f.partners.includes(billingPartner))?.features || - plan.features.find((f) => f.partners.length === 0)!.features - ) -} - -export function pickFooter(plan: PricingInformation, billingPartner: string = '') { - return ( - plan.footer?.find((f) => f.partners.includes(billingPartner))?.footer || - plan.footer?.find((f) => f.partners.length === 0)!.footer - ) -} diff --git a/packages/shared-data/pricing.ts b/packages/shared-data/pricing.ts index 8f5603aa739ce..c7507541c637f 100644 --- a/packages/shared-data/pricing.ts +++ b/packages/shared-data/pricing.ts @@ -36,7 +36,8 @@ export type FeatureKey = | 'database.pitr' | 'database.pausing' | 'database.branching' - | 'database.bandwidth' + | 'database.egress' + | 'database.cachedEgress' | 'auth.totalUsers' | 'auth.maus' | 'auth.userDataOwnership' @@ -182,8 +183,8 @@ export const pricing: Pricing = { usage_based: true, }, { - key: 'database.bandwidth', - title: 'Bandwidth', + key: 'database.egress', + title: 'Egress', plans: { free: '5 GB included', pro: ['250 GB included', 'then $0.09 per GB'], @@ -192,6 +193,17 @@ export const pricing: Pricing = { }, usage_based: true, }, + { + key: 'database.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'], + enterprise: 'Custom', + }, + usage_based: true, + }, ], }, auth: { @@ -589,11 +601,7 @@ export const pricing: Pricing = { plans: { free: false, pro: false, - team: [ - '$60 per drain per month', - '+ $0.20 per million events', - '+ $0.09 per GB bandwidth', - ], + team: ['$60 per drain per month', '+ $0.20 per million events', '+ $0.09 per GB egress'], enterprise: 'Custom', }, usage_based: true,