From 48b372d29be0f11a86edf1c42a60054fdcd34b24 Mon Sep 17 00:00:00 2001 From: Stephen Morgan Date: Thu, 18 Dec 2025 07:28:38 +1300 Subject: [PATCH] Feature/private link (#41094) * privatelink integration * mock queries * add integration endpoints * poll for updates on transition states * prevent ui for multiple posts * changes to copy for existing accounts * update privatelink documentation * hide privatelink for customer under Team plan * added connected state tip * linting fixes * fixed issue due to merge * Add cc feature flag * Nit clean up * Small revert * Nudge UI a little * Update react queries to fit new structure * Fix * Clean up * nit * revert padding * Fix * fix: tanstack changes * added feature to pricing page * Update apps/docs/content/guides/platform/privatelink.mdx Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> * fix: dropdown copy change * Update apps/docs/content/guides/platform/privatelink.mdx Co-authored-by: Chris Chinchilla * Update apps/docs/content/guides/platform/privatelink.mdx Co-authored-by: Chris Chinchilla * Smol clean up --------- Co-authored-by: Saxon Fletcher Co-authored-by: Joshen Lim Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Chris Chinchilla --- .../content/guides/platform/privatelink.mdx | 33 ++- .../IntegrationSettings.tsx | 2 +- .../SidePanelVercelProjectLinker.tsx | 2 +- .../AWSPrivateLinkAccountItem.tsx | 82 ++++++ .../AWSPrivateLink/AWSPrivateLinkForm.tsx | 237 ++++++++++++++++++ .../AWSPrivateLink/AWSPrivateLinkSection.tsx | 141 +++++++++++ .../GithubIntegration/GithubSection.tsx | 6 +- .../Integrations/IntegrationsSettings.tsx | 19 +- .../VercelIntegration/VercelSection.tsx | 6 +- apps/studio/components/layouts/Scaffold.tsx | 2 +- .../components/ui/Resource/ResourceItem.tsx | 50 +++- .../components/ui/UpgradePlanButton.tsx | 16 +- .../aws-account-create-mutation.ts | 63 +++++ .../aws-account-delete-mutation.ts | 57 +++++ .../data/aws-accounts/aws-accounts-query.ts | 64 +++++ apps/studio/data/aws-accounts/keys.ts | 3 + .../img/integrations/covers/aws-cover.png | Bin 0 -> 8571 bytes .../components/Pricing/PricingTableRow.tsx | 12 + .../enabled-features/enabled-features.json | 1 + .../enabled-features.schema.json | 5 + packages/shared-data/pricing.ts | 12 + 21 files changed, 774 insertions(+), 39 deletions(-) create mode 100644 apps/studio/components/interfaces/Settings/Integrations/AWSPrivateLink/AWSPrivateLinkAccountItem.tsx create mode 100644 apps/studio/components/interfaces/Settings/Integrations/AWSPrivateLink/AWSPrivateLinkForm.tsx create mode 100644 apps/studio/components/interfaces/Settings/Integrations/AWSPrivateLink/AWSPrivateLinkSection.tsx create mode 100644 apps/studio/data/aws-accounts/aws-account-create-mutation.ts create mode 100644 apps/studio/data/aws-accounts/aws-account-delete-mutation.ts create mode 100644 apps/studio/data/aws-accounts/aws-accounts-query.ts create mode 100644 apps/studio/data/aws-accounts/keys.ts create mode 100644 apps/studio/public/img/integrations/covers/aws-cover.png diff --git a/apps/docs/content/guides/platform/privatelink.mdx b/apps/docs/content/guides/platform/privatelink.mdx index a95717b01f9..7e1f7a85752 100644 --- a/apps/docs/content/guides/platform/privatelink.mdx +++ b/apps/docs/content/guides/platform/privatelink.mdx @@ -6,7 +6,8 @@ description: 'Secure private network connectivity to your Supabase database usin -PrivateLink is currently in alpha and available exclusively to Enterprise customers. Contact your account manager or [reach out to our team](/contact/enterprise) to enable this feature. +PrivateLink is currently in beta and available only to Team and Enterprise customers. +Contact support if you would like to create a PrivateLink connection for a read-only replica. @@ -31,19 +32,25 @@ Supabase PrivateLink is currently just for direct database and PgBouncer connect To use PrivateLink with your Supabase project: -- Enterprise Supabase subscription +- Team or Enterprise Supabase subscription - AWS VPC in the same region as your Supabase project - Appropriate permissions to accept Resource Shares, and create and manage endpoints ## Getting started -#### Step 1: Contact Supabase support +#### Step 1: Add AWS account -Reach out to your Enterprise account manager or [contact our team](/contact/enterprise) to initiate PrivateLink setup. During this initial contact, be prepared to provide: +Navigate to your project's Integrations section to set up PrivateLink: -- Your Supabase organization slug -- The specific projects you want to enable PrivateLink for (optional) -- Your AWS Account ID(s) +1. Go to your Supabase project dashboard +2. Navigate to [**Settings** > **Integrations**](/dashboard/project/_/settings/integrations) +3. Find the **AWS PrivateLink** section +4. Click **Add Account** +5. Enter your AWS Account ID +6. Provide a description for the account (recommended) +7. Click **Add Account** to submit + +After submission, Supabase creates a VPC Lattice Resource Configuration for your project and sends an AWS Resource Share to the specified AWS Account ID. This process may take a few moments. Once complete, the account will show a "Ready" status, indicating that the resource share has been sent to your AWS account and is ready to be accepted. #### Step 2: Accept resource share @@ -54,7 +61,7 @@ Supabase will send you an AWS Resource Share containing the VPC Lattice Resource {/* supa-mdx-lint-disable-next-line Rule004ExcludeWords */} 3. Go to [Shared with me > Resource shares](https://console.aws.amazon.com/ram/home#SharedResourceShares) 4. Locate the resource share from Supabase. - - The resource share will have the format `cust-prod-[region]-pl-[organisation]-rc-share` + - The resource share has the format `sspl-[project_ref]-[random alphanumeric string]` 5. Click on the resource share name to view details. Review the list of resource shares - it should only include resources of type vpc-lattice:ResourceConfiguration. 6. Click **Accept resource share** 7. Confirm the acceptance in the dialog box @@ -115,7 +122,7 @@ In your AWS account, you have two options to establish connectivity: Verify the private connection is working correctly from your VPC: -1. Launch an EC2 instance or use an existing instance in your VPC +1. Launch an EC2 instance or use an existing instance within your VPC 2. Install a Postgres client (e.g., `psql`) 3. Test the connection using the private endpoint: @@ -144,7 +151,7 @@ postgresql://user:pass@db.[project-ref].supabase.co:5432/postgres postgresql://user:pass@your-private-endpoint.vpce.amazonaws.com:5432/postgres ``` -#### Step 8: Disable public connectivity (optional) +#### Step 7: Disable public connectivity (optional) For maximum security, you can disable public internet access for your database: @@ -152,11 +159,11 @@ For maximum security, you can disable public internet access for your database: 2. Ensure all applications are successfully using the private connection 3. Update any monitoring or backup tools to use the private endpoint -## Alpha limitations +## Beta limitations -During the alpha phase: +During the beta phase: -- **Setup Coordination**: Configuration requires direct coordination with Supabase support team +- **Read Replicas**: PrivateLink does not currently support read replicas - **Feature Evolution**: The setup process and capabilities may evolve as we refine the offering ## Compatibility diff --git a/apps/studio/components/interfaces/Organization/IntegrationSettings/IntegrationSettings.tsx b/apps/studio/components/interfaces/Organization/IntegrationSettings/IntegrationSettings.tsx index 35672b1744b..72b34b486f8 100644 --- a/apps/studio/components/interfaces/Organization/IntegrationSettings/IntegrationSettings.tsx +++ b/apps/studio/components/interfaces/Organization/IntegrationSettings/IntegrationSettings.tsx @@ -4,7 +4,7 @@ import { toast } from 'sonner' import { EmptyIntegrationConnection } from 'components/interfaces/Integrations/VercelGithub/IntegrationPanels' import { Markdown } from 'components/interfaces/Markdown' -import VercelSection from 'components/interfaces/Settings/Integrations/VercelIntegration/VercelSection' +import { VercelSection } from 'components/interfaces/Settings/Integrations/VercelIntegration/VercelSection' import { ScaffoldContainer, ScaffoldContainerLegacy, diff --git a/apps/studio/components/interfaces/Organization/IntegrationSettings/SidePanelVercelProjectLinker.tsx b/apps/studio/components/interfaces/Organization/IntegrationSettings/SidePanelVercelProjectLinker.tsx index f514df7c345..4897a23b5cb 100644 --- a/apps/studio/components/interfaces/Organization/IntegrationSettings/SidePanelVercelProjectLinker.tsx +++ b/apps/studio/components/interfaces/Organization/IntegrationSettings/SidePanelVercelProjectLinker.tsx @@ -24,7 +24,7 @@ const VERCEL_ICON = ( ) -const SidePanelVercelProjectLinker = () => { +export const SidePanelVercelProjectLinker = () => { const { ref } = useParams() const { data: selectedOrganization } = useSelectedOrganizationQuery() const sidePanelStateSnapshot = useSidePanelsStateSnapshot() diff --git a/apps/studio/components/interfaces/Settings/Integrations/AWSPrivateLink/AWSPrivateLinkAccountItem.tsx b/apps/studio/components/interfaces/Settings/Integrations/AWSPrivateLink/AWSPrivateLinkAccountItem.tsx new file mode 100644 index 00000000000..25faef9cdd6 --- /dev/null +++ b/apps/studio/components/interfaces/Settings/Integrations/AWSPrivateLink/AWSPrivateLinkAccountItem.tsx @@ -0,0 +1,82 @@ +import { Edit, MoreVertical, Trash } from 'lucide-react' + +import { + Badge, + Button, + CardContent, + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from 'ui' + +interface AWSPrivateLinkAccountItemProps { + aws_account_id: string + account_name?: string + status: + | 'CREATING' + | 'READY' + | 'ASSOCIATION_REQUEST_EXPIRED' + | 'ASSOCIATION_ACCEPTED' + | 'CREATION_FAILED' + | 'DELETING' + shared_at: string | null + onEdit: () => void + onDelete: () => void +} + +export const AWSPrivateLinkAccountItem = ({ + aws_account_id, + account_name, + status, + onEdit, + onDelete, +}: AWSPrivateLinkAccountItemProps) => { + const getStatusBadge = () => { + switch (status) { + case 'ASSOCIATION_ACCEPTED': + return Connected + case 'READY': + return Ready + case 'CREATING': + return Creating + case 'DELETING': + return Deleting + case 'ASSOCIATION_REQUEST_EXPIRED': + return Expired + case 'CREATION_FAILED': + return Failed + default: + return Unknown + } + } + + return ( + +
+
{aws_account_id}
+
{account_name || 'No description'}
+
+ + {getStatusBadge()} + + + + + ) + } + /> + + )} + ( + + + { + if (!isNew) { + e.target.blur() + } + }} + /> + + + )} + /> + ( + + + { + if (!isNew) { + e.target.blur() + } + }} + /> + + + )} + /> + + + + + {isNew && ( + + )} + + + + + + ) +} diff --git a/apps/studio/components/interfaces/Settings/Integrations/AWSPrivateLink/AWSPrivateLinkSection.tsx b/apps/studio/components/interfaces/Settings/Integrations/AWSPrivateLink/AWSPrivateLinkSection.tsx new file mode 100644 index 00000000000..95176c33441 --- /dev/null +++ b/apps/studio/components/interfaces/Settings/Integrations/AWSPrivateLink/AWSPrivateLinkSection.tsx @@ -0,0 +1,141 @@ +import { useState } from 'react' +import { toast } from 'sonner' + +import { + ScaffoldContainer, + ScaffoldSection, + ScaffoldSectionContent, + ScaffoldSectionDetail, +} from 'components/layouts/Scaffold' +import { ResourceList } from 'components/ui/Resource/ResourceList' +import { UpgradeToPro } from 'components/ui/UpgradeToPro' +import { useAWSAccountDeleteMutation } from 'data/aws-accounts/aws-account-delete-mutation' +import { useAWSAccountsQuery } from 'data/aws-accounts/aws-accounts-query' +import { useSelectedOrganizationQuery } from 'hooks/misc/useSelectedOrganization' +import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject' +import { IS_PLATFORM } from 'lib/constants' +import { Button, Card, CardContent, cn } from 'ui' +import { ConfirmationModal } from 'ui-patterns/Dialogs/ConfirmationModal' +import { IntegrationImageHandler } from '../IntegrationsSettings' +import { AWSPrivateLinkAccountItem } from './AWSPrivateLinkAccountItem' +import { AWSPrivateLinkForm } from './AWSPrivateLinkForm' + +export const AWSPrivateLinkSection = () => { + const { data: project } = useSelectedProjectQuery() + const { data: organization } = useSelectedOrganizationQuery() + const { data: accounts } = useAWSAccountsQuery({ projectRef: project?.ref }) + + const [selectedAccount, setSelectedAccount] = useState(null) + const [showForm, setShowForm] = useState(false) + const [showDeleteModal, setShowDeleteModal] = useState(false) + + const { mutate: deleteAccount, isPending: isDeleting } = useAWSAccountDeleteMutation({ + onSuccess: () => { + toast.success('Account will be deleted shortly') + setShowDeleteModal(false) + setSelectedAccount(null) + }, + }) + + const isTeamsOrEnterpriseAndUp = + organization?.plan?.id === 'enterprise' || organization?.plan?.id === 'team' + const promptPlanUpgrade = IS_PLATFORM && !isTeamsOrEnterpriseAndUp + + const onAddAccount = () => { + setSelectedAccount(null) + setShowForm(true) + } + + const onEditAccount = (account: any) => { + setSelectedAccount(account) + setShowForm(true) + } + + const onDeleteAccount = (account: any) => { + setSelectedAccount(account) + setShowDeleteModal(true) + } + + const onConfirmDelete = () => { + if (selectedAccount && project) { + deleteAccount({ projectRef: project.ref, awsAccountId: selectedAccount.aws_account_id }) + } + } + + return ( + <> + + + +

Connect to your Supabase project from your AWS VPC using AWS PrivateLink.

+ +
+ +
+
+
+ How does the AWS PrivateLink integration work? +
+

+ Connecting to AWS PrivateLink allows you to create a private connection between + your AWS VPC and your Supabase project. +

+ {promptPlanUpgrade && ( +
+ +
+ )} +
+
+
+

AWS Accounts

+ +
+ {(accounts?.length ?? 0) > 0 ? ( + + {accounts?.map((account) => ( + onEditAccount(account)} + onDelete={() => onDeleteAccount(account)} + /> + ))} + + ) : ( + + +

No accounts connected

+
+
+ )} +
+
+
+
+
+ + + + setShowDeleteModal(false)} + onConfirm={onConfirmDelete} + > +

+ Are you sure you want to delete the AWS account connection for{' '} + {selectedAccount?.aws_account_id}? +

+
+ + ) +} diff --git a/apps/studio/components/interfaces/Settings/Integrations/GithubIntegration/GithubSection.tsx b/apps/studio/components/interfaces/Settings/Integrations/GithubIntegration/GithubSection.tsx index fd74b7ada5c..985726ca307 100644 --- a/apps/studio/components/interfaces/Settings/Integrations/GithubIntegration/GithubSection.tsx +++ b/apps/studio/components/interfaces/Settings/Integrations/GithubIntegration/GithubSection.tsx @@ -28,7 +28,7 @@ const IntegrationImageHandler = ({ title }: { title: 'vercel' | 'github' }) => { ) } -const GitHubSection = () => { +export const GitHubSection = () => { const { ref: projectRef } = useParams() const { data: organization } = useSelectedOrganizationQuery() @@ -52,7 +52,7 @@ const GitHubSection = () => { return ( - +

Connect any of your GitHub repositories to a project.

@@ -93,5 +93,3 @@ const GitHubSection = () => {
) } - -export default GitHubSection diff --git a/apps/studio/components/interfaces/Settings/Integrations/IntegrationsSettings.tsx b/apps/studio/components/interfaces/Settings/Integrations/IntegrationsSettings.tsx index 85b6e5d1569..4ad23997216 100644 --- a/apps/studio/components/interfaces/Settings/Integrations/IntegrationsSettings.tsx +++ b/apps/studio/components/interfaces/Settings/Integrations/IntegrationsSettings.tsx @@ -1,16 +1,18 @@ import Link from 'next/link' -import SidePanelVercelProjectLinker from 'components/interfaces/Organization/IntegrationSettings/SidePanelVercelProjectLinker' +import { useFlag } from 'common' +import { SidePanelVercelProjectLinker } from 'components/interfaces/Organization/IntegrationSettings/SidePanelVercelProjectLinker' import { ScaffoldContainer, ScaffoldDivider } from 'components/layouts/Scaffold' import { useProjectDetailQuery } from 'data/projects/project-detail-query' import { useIsFeatureEnabled } from 'hooks/misc/useIsFeatureEnabled' import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject' import { BASE_PATH } from 'lib/constants' import { AlertDescription_Shadcn_, AlertTitle_Shadcn_, Alert_Shadcn_, WarningIcon } from 'ui' -import GitHubSection from './GithubIntegration/GithubSection' -import VercelSection from './VercelIntegration/VercelSection' +import { AWSPrivateLinkSection } from './AWSPrivateLink/AWSPrivateLinkSection' +import { GitHubSection } from './GithubIntegration/GithubSection' +import { VercelSection } from './VercelIntegration/VercelSection' -export const IntegrationImageHandler = ({ title }: { title: 'vercel' | 'github' }) => { +export const IntegrationImageHandler = ({ title }: { title: 'vercel' | 'github' | 'aws' }) => { return ( { const isBranch = project?.parent_project_ref !== undefined const showVercelIntegration = useIsFeatureEnabled('integrations:vercel') + const showAWSPrivateLinkFeature = useIsFeatureEnabled('integrations:aws_private_link') + const showAWSPrivateLinkConfigCat = useFlag('awsPrivateLinkIntegration') + const showAWSPrivateLink = showAWSPrivateLinkFeature && showAWSPrivateLinkConfigCat return ( <> @@ -54,6 +59,12 @@ const IntegrationSettings = () => { )} + {showAWSPrivateLink && ( + <> + + + + )} ) } diff --git a/apps/studio/components/interfaces/Settings/Integrations/VercelIntegration/VercelSection.tsx b/apps/studio/components/interfaces/Settings/Integrations/VercelIntegration/VercelSection.tsx index 5c9d4371a32..548b152720c 100644 --- a/apps/studio/components/interfaces/Settings/Integrations/VercelIntegration/VercelSection.tsx +++ b/apps/studio/components/interfaces/Settings/Integrations/VercelIntegration/VercelSection.tsx @@ -37,7 +37,7 @@ import { GenericSkeletonLoader } from 'ui-patterns/ShimmeringLoader' import { IntegrationImageHandler } from '../IntegrationsSettings' import VercelIntegrationConnectionForm from './VercelIntegrationConnectionForm' -const VercelSection = ({ isProjectScoped }: { isProjectScoped: boolean }) => { +export const VercelSection = ({ isProjectScoped }: { isProjectScoped: boolean }) => { const { data: project } = useSelectedProjectQuery() const { data: org } = useSelectedOrganizationQuery() const { data } = useOrgIntegrationsQuery({ orgSlug: org?.slug }) @@ -165,7 +165,7 @@ You can change the scope of the access for Supabase by configuring return ( - + @@ -263,5 +263,3 @@ You can change the scope of the access for Supabase by configuring ) } - -export default VercelSection diff --git a/apps/studio/components/layouts/Scaffold.tsx b/apps/studio/components/layouts/Scaffold.tsx index b529cee0a22..cdc1ca4243c 100644 --- a/apps/studio/components/layouts/Scaffold.tsx +++ b/apps/studio/components/layouts/Scaffold.tsx @@ -105,7 +105,7 @@ export const ScaffoldSection = forwardRef< */ export const ScaffoldDivider = forwardRef>( ({ className, ...props }, ref) => { - return
+ return
} ) diff --git a/apps/studio/components/ui/Resource/ResourceItem.tsx b/apps/studio/components/ui/Resource/ResourceItem.tsx index 3e85724e2f9..20b35b33e48 100644 --- a/apps/studio/components/ui/Resource/ResourceItem.tsx +++ b/apps/studio/components/ui/Resource/ResourceItem.tsx @@ -1,17 +1,31 @@ -import { ChevronRight } from 'lucide-react' +import { ChevronRight, MoreVertical } from 'lucide-react' import { forwardRef, HTMLAttributes, ReactNode } from 'react' -import { CardContent, cn } from 'ui' +import { + Button, + CardContent, + cn, + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from 'ui' + +export interface ResourceAction { + label: string + onClick: () => void +} export interface ResourceItemProps extends HTMLAttributes { media?: ReactNode meta?: ReactNode onClick?: () => void children?: ReactNode + actions?: ResourceAction[] } export const ResourceItem = forwardRef( - ({ media, meta, onClick, children, className, ...props }, ref) => { + ({ media, meta, onClick, children, className, actions, ...props }, ref) => { return ( ( )}
{children}
{meta &&
{meta}
} - {onClick && } + {actions && actions.length > 0 ? ( + + +