From c9ed51c99ea088b9cafbddcf4d0881445ffc7985 Mon Sep 17 00:00:00 2001
From: Danny White <3104761+dnywh@users.noreply.github.com>
Date: Mon, 27 Jul 2026 18:14:01 -0400
Subject: [PATCH] fix(studio): add return to Vercel escape hatch (#48311)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
## I have read the
[CONTRIBUTING.md](https://github.com/supabase/supabase/blob/master/CONTRIBUTING.md)
file.
YES
## What kind of change does this PR introduce?
Bug fix / UX improvement for the Vercel Deploy Button create-project
interstitial.
## What is the current behavior?
On the Vercel create-project step, the organization picker is locked
(correct — the integration is bound to that org) and Cancel is hidden.
If the org can't create a free project (member free-project limits),
users hit a dead end: Upgrade may not help, and there's no way out of
the popup.
Also includes a small capitalisation nit on the Vercel install page.
| Before |
| --- |
| |
## What is the new behavior?
- Replaces `hideCancelButton` with `cancelAction: 'studio' | 'vercel' |
'hidden'`
- Vercel create flow shows **Return to Vercel**, which redirects to the
install `next` URL (closing the popup cleanly)
- Free-project-limit admonition adds a Vercel-only hint pointing at that
button: “Or return to Vercel and restart with a different organization.”
- Main `/new` Cancel behaviour is unchanged
- Org picker stays disabled
## Additional context
Org switching mid-create is intentionally not allowed. That would orphan
the Vercel install. Returning to Vercel is the safe escape hatch so
users can restart Deploy Button with another org, or free a project slot
/ upgrade and try again.
## To test
As far as I can tell, this is impossible to test on prod. Shortly after
merge though, you could test the following:
- [ ] Happy path: create still works; Return to Vercel is secondary and
does not block submit
- [ ] Free-limit blocked org: Create disabled, Return to Vercel visible
and redirects to `next`
- [ ] Main `/new`: Cancel still goes to last org / organizations
## Summary by CodeRabbit
- **New Features**
- Enhanced project creation flow for Vercel: when a valid return
destination is available, users can choose **“Return to Vercel”**.
- Added additional messaging in the free-project-limit warning to guide
users back to Vercel and restart with a different organization (when
applicable).
- **Bug Fixes**
- Improved cancel behavior and routing consistency by only enabling
Vercel return when the destination is valid.
- **Style**
- Updated the Vercel integration interstitial title capitalization for
consistency.
---------
Co-authored-by: Joshen Lim
---
.../Vercel/VercelIntegration.utils.test.ts | 32 ++++++++++++++++++
.../Vercel/VercelIntegration.utils.ts | 6 ++++
.../FreeProjectLimitWarning.tsx | 11 ++++++-
.../ProjectCreation/ProjectCreationFooter.tsx | 33 ++++++++++++++-----
.../ProjectCreation/ProjectCreationForm.tsx | 13 +++++---
.../pages/integrations/vercel/install.tsx | 2 +-
6 files changed, 82 insertions(+), 15 deletions(-)
create mode 100644 apps/studio/components/interfaces/Integrations/Vercel/VercelIntegration.utils.test.ts
diff --git a/apps/studio/components/interfaces/Integrations/Vercel/VercelIntegration.utils.test.ts b/apps/studio/components/interfaces/Integrations/Vercel/VercelIntegration.utils.test.ts
new file mode 100644
index 00000000000..00cd23ecbf7
--- /dev/null
+++ b/apps/studio/components/interfaces/Integrations/Vercel/VercelIntegration.utils.test.ts
@@ -0,0 +1,32 @@
+import { describe, expect, test } from 'vitest'
+
+import {
+ getValidVercelReturnUrl,
+ isVercelUrl,
+} from '@/components/interfaces/Integrations/Vercel/VercelIntegration.utils'
+
+describe('isVercelUrl', () => {
+ test('accepts https vercel.com urls', () => {
+ expect(isVercelUrl('https://vercel.com/callback')).toBe(true)
+ })
+
+ test('rejects non-vercel and invalid urls', () => {
+ expect(isVercelUrl('https://example.com')).toBe(false)
+ expect(isVercelUrl('http://vercel.com')).toBe(false)
+ expect(isVercelUrl('not-a-url')).toBe(false)
+ })
+})
+
+describe('getValidVercelReturnUrl', () => {
+ test('returns the url when it is a valid vercel return url', () => {
+ expect(getValidVercelReturnUrl('https://vercel.com/callback')).toBe(
+ 'https://vercel.com/callback'
+ )
+ })
+
+ test('returns undefined for missing or invalid next values', () => {
+ expect(getValidVercelReturnUrl(undefined)).toBeUndefined()
+ expect(getValidVercelReturnUrl('https://example.com')).toBeUndefined()
+ expect(getValidVercelReturnUrl('not-a-url')).toBeUndefined()
+ })
+})
diff --git a/apps/studio/components/interfaces/Integrations/Vercel/VercelIntegration.utils.ts b/apps/studio/components/interfaces/Integrations/Vercel/VercelIntegration.utils.ts
index a6802341ab7..05db949804d 100644
--- a/apps/studio/components/interfaces/Integrations/Vercel/VercelIntegration.utils.ts
+++ b/apps/studio/components/interfaces/Integrations/Vercel/VercelIntegration.utils.ts
@@ -10,6 +10,12 @@ export function isVercelUrl(url: string): boolean {
}
}
+/** Returns `next` when it is a safe Vercel return URL; otherwise undefined. */
+export function getValidVercelReturnUrl(next: string | undefined): string | undefined {
+ if (typeof next === 'string' && isVercelUrl(next)) return next
+ return undefined
+}
+
export function findVercelIntegrationByConfigurationId(
integrations: Integration[] | undefined,
configurationId: string | undefined
diff --git a/apps/studio/components/interfaces/ProjectCreation/FreeProjectLimitWarning.tsx b/apps/studio/components/interfaces/ProjectCreation/FreeProjectLimitWarning.tsx
index e3b8389232c..ba554365f5a 100644
--- a/apps/studio/components/interfaces/ProjectCreation/FreeProjectLimitWarning.tsx
+++ b/apps/studio/components/interfaces/ProjectCreation/FreeProjectLimitWarning.tsx
@@ -6,9 +6,13 @@ import type { MemberWithFreeProjectLimit } from '@/data/organizations/free-proje
interface FreeProjectLimitWarningProps {
membersExceededLimit: MemberWithFreeProjectLimit[]
+ showVercelReturnHint?: boolean
}
-export const FreeProjectLimitWarning = ({ membersExceededLimit }: FreeProjectLimitWarningProps) => {
+export const FreeProjectLimitWarning = ({
+ membersExceededLimit,
+ showVercelReturnHint = false,
+}: FreeProjectLimitWarningProps) => {
return (
+ {showVercelReturnHint && (
+
+ Or return to Vercel and restart with a different organization.
+
+ )}
canCreateProject: boolean
@@ -29,7 +32,7 @@ interface ProjectCreationFooterProps {
organizationProjects: OrgProject[]
isCreatingNewProject: boolean
isSuccessNewProject: boolean
- hideCancelButton: boolean
+ cancelAction?: ProjectCreationCancelAction
}
export const ProjectCreationFooter = ({
@@ -39,9 +42,10 @@ export const ProjectCreationFooter = ({
organizationProjects,
isCreatingNewProject,
isSuccessNewProject,
- hideCancelButton,
+ cancelAction = 'studio',
}: ProjectCreationFooterProps) => {
const router = useRouter()
+ const { next } = useParams()
const { data: currentOrg } = useSelectedOrganizationQuery()
const isFreePlan = currentOrg?.plan?.id === 'free'
const { lastVisitedOrganization } = useLastVisitedOrganization()
@@ -53,6 +57,9 @@ export const ProjectCreationFooter = ({
? 0
: monthlyInstancePrice(instanceSize) - availableComputeCredits
+ const vercelReturnUrl = getValidVercelReturnUrl(next)
+ const canReturnToVercel = cancelAction === 'vercel' && vercelReturnUrl !== undefined
+
// [kevin] This will eventually all be provided by a new API endpoint to preview and validate project creation, this is just for kaizen now
const monthlyComputeCosts =
// current project costs
@@ -66,6 +73,17 @@ export const ProjectCreationFooter = ({
// compute credits
10
+ const onCancel = () => {
+ if (canReturnToVercel && vercelReturnUrl) {
+ window.location.href = vercelReturnUrl
+ return
+ }
+
+ // Fall back to Studio when cancelAction is studio, or when vercel next is missing/invalid
+ if (!!lastVisitedOrganization) router.push(`/org/${lastVisitedOrganization}`)
+ else router.push('/organizations')
+ }
+
return (