Files
Danny WhiteandJoshen Lim 6b1fc3d11d recover failed Vercel deploy connections (#48474)
## What kind of change does this PR introduce?

Bug fix.

## What is the current behavior?

A failed Vercel connection after project creation is only logged,
leaving the project-creation screen in its loading state.

## What is the new behavior?

The flow preserves the created project and shows the connection error
with retry and open-project actions in the standard project-creation
footer.

Project-creation failures remain ordinary inline form errors. Connection
failures are owned by this flow without a duplicate toast or a no-op
error handler.

| Before | After |
| --- | --- |
| ![Create Vercel Project
Supabase](https://github.com/user-attachments/assets/18dfb6d1-614a-4298-bf28-8399d86c7bca)
| <img width="1024" height="563" alt="Create Vercel Project Supabase"
src="https://github.com/user-attachments/assets/b7d3fdca-d7a0-4b50-9231-3cf7dc371a87"
/> |

## To test

### Before on master

1. Switch to `master`.
2. With local Studio running and while signed in, open an organisation
you can access. Copy its slug from
`http://localhost:8082/org/<YOUR_ORG_SLUG>`.
3. Open
`apps/studio/components/interfaces/ProjectCreation/ProjectCreationForm.tsx`.
4. Find `isSuccessNewProject={isSuccessNewProject}` in the
`ProjectCreationFooter` props and temporarily change it to:
   ```tsx
   isSuccessNewProject={true}
   ```
5. Replace `<YOUR_ORG_SLUG>` in this URL with the slug from step 2, then
open it:
`http://localhost:8082/integrations/vercel/<YOUR_ORG_SLUG>/deploy-button/new-project`.
6. Confirm **Create new project** remains in its loading state and there
is no error, retry action, or route to the created project. This
represents the current stuck state.
7. Revert the temporary edit before switching branches.

### After on this branch

1. Switch to `dnywh/vercel-deploy-recovery`.
2. With local Studio running and while signed in, open an organisation
you can access. Copy its slug from
`http://localhost:8082/org/<YOUR_ORG_SLUG>`.
3. Open
`apps/studio/pages/integrations/vercel/[slug]/deploy-button/new-project.tsx`.
4. Find the conditional beginning with `newProjectRef === undefined`
inside `InterstitialLayout`.
5. Replace that whole conditional with:
   ```tsx
   <VercelConnectionError
     projectRef="abcdefghijklmnopqrst"
     message="Connection request failed"
     onRetry={() => undefined}
   />
   ```
6. Replace `<YOUR_ORG_SLUG>` in this URL with the slug from step 2, then
open it:
`http://localhost:8082/integrations/vercel/<YOUR_ORG_SLUG>/deploy-button/new-project`.
7. Confirm the admonition says **Unable to connect to Vercel** and
**Your Supabase project was still created. Error: Connection request
failed**.
8. Confirm **Open project** and **Retry connection** appear as compact,
right-aligned footer buttons. The retry action is intentionally inert in
this visual-only mock, and no project or Vercel connection is created.
9. Revert the temporary edit.

## Additional context

Follows #48473. The consistency follow-up #48640 is stacked on this PR.



<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

- **New Features**
- Added inline error messages to project creation forms for integration,
API, and validation failures.
- Added clear Vercel connection states, including waiting, connecting,
success, and error screens.
  - Added retry actions and links to open successfully created projects.

- **Bug Fixes**
- Improved error handling so Vercel connection issues remain visible in
context instead of appearing only as notifications.

- **Tests**
- Added coverage for partial-success messaging, project links, and retry
behavior.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Joshen Lim <joshenlimek@gmail.com>
2026-08-06 10:49:50 +10:00

823 lines
32 KiB
TypeScript

import { zodResolver } from '@hookform/resolvers/zod'
import { acceptUntrustedSql, joinSqlFragments, untrustedSql } from '@supabase/pg-meta'
import { PermissionAction } from '@supabase/shared-types/out/constants'
import { useFeatureFlags, useFlag, useParams } from 'common'
import Link from 'next/link'
import { useRouter } from 'next/router'
import { useEffect, useMemo, useRef, useState } from 'react'
import { useForm, useFormState } from 'react-hook-form'
import { type CloudProvider } from 'shared-data'
import { toast } from 'sonner'
import { Button, cn, Form, useWatch } from 'ui'
import { Admonition } from 'ui-patterns/Admonition'
import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal'
import { z } from 'zod'
import { AdvancedConfiguration } from './AdvancedConfiguration'
import { ComputeSizeSelector } from './ComputeSizeSelector'
import { DatabasePasswordInput } from './DatabasePasswordInput'
import { DataSeeding } from './DataSeeding'
import { DisabledWarningDueToIncident } from './DisabledWarningDueToIncident'
import { FreeProjectLimitWarning } from './FreeProjectLimitWarning'
import { HighAvailabilityInput } from './HighAvailabilityInput'
import { InternalOnlyConfiguration } from './InternalOnlyConfiguration'
import { OrganizationSelector } from './OrganizationSelector'
import { extractPostgresVersionDetails } from './PostgresVersionSelector'
import {
HIGH_AVAILABILITY_POSTGRES_ENGINE,
HIGH_AVAILABILITY_RELEASE_CHANNEL,
sizes,
} from './ProjectCreation.constants'
import { FormSchema } from './ProjectCreation.schema'
import {
getHighAvailabilityRegionCode,
instanceLabel,
monthlyInstancePrice,
smartRegionToExactRegion,
} from './ProjectCreation.utils'
import { ProjectCreationFooter } from './ProjectCreationFooter'
import { ProjectNameInput } from './ProjectNameInput'
import { RegionSelector } from './RegionSelector'
import { SecurityOptions } from './SecurityOptions'
import { AUTO_ENABLE_RLS_EVENT_TRIGGER_SQL } from '@/components/interfaces/Database/Triggers/EventTriggersList/EventTriggers.constants'
import {
GitHubRepositoryField,
useGitHubRepositoryOptions,
} from '@/components/interfaces/Settings/Integrations/GithubIntegration/GitHubRepositoryField'
import Panel from '@/components/ui/Panel'
import { useAvailableOrioleImageVersion } from '@/data/config/project-creation-postgres-versions-query'
import { useOverdueInvoicesQuery } from '@/data/invoices/invoices-overdue-query'
import { useDefaultRegionQuery } from '@/data/misc/get-default-region-query'
import { useAuthorizedAppsQuery } from '@/data/oauth/authorized-apps-query'
import { useFreeProjectLimitCheckQuery } from '@/data/organizations/free-project-limit-check-query'
import { useOrganizationAvailableRegionsQuery } from '@/data/organizations/organization-available-regions-query'
import { useOrganizationsQuery } from '@/data/organizations/organizations-query'
import { DesiredInstanceSize } from '@/data/projects/new-project.constants'
import {
OrgProject,
useOrgProjectsInfiniteQuery,
} from '@/data/projects/org-projects-infinite-query'
import {
ProjectCreateVariables,
useProjectCreateMutation,
} from '@/data/projects/project-create-mutation'
import { useCheckEntitlements } from '@/hooks/misc/useCheckEntitlements'
import { useAsyncCheckPermissions } from '@/hooks/misc/useCheckPermissions'
import {
isInDataApiRevokeTreatment,
useDataApiRevokeOnCreateDefaultEnabled,
} from '@/hooks/misc/useDataApiRevokeOnCreateDefault'
import { useIsFeatureEnabled } from '@/hooks/misc/useIsFeatureEnabled'
import { useLastVisitedOrganization } from '@/hooks/misc/useLastVisitedOrganization'
import { useSelectedOrganizationQuery } from '@/hooks/misc/useSelectedOrganization'
import { usePHFlag } from '@/hooks/ui/useFlag'
import { DOCS_URL, PROJECT_STATUS, PROVIDERS, useDefaultProvider } from '@/lib/constants'
import { getInitialMigrationSQLFromGitHubRepo } from '@/lib/integration-utils'
import { useProfile } from '@/lib/profile'
import { trimSafeSqlFragment } from '@/lib/sql'
import { classifyApiError, classifyValidationError } from '@/lib/telemetry/funnel-errors'
import { useTrack } from '@/lib/telemetry/track'
import { useTrackFunnelError } from '@/lib/telemetry/use-track-funnel-error'
const sizesWithNoCostConfirmationRequired: DesiredInstanceSize[] = ['micro', 'small']
interface ProjectCreationFormProps {
isVercelIntegrationFlow?: boolean
onCreateSuccess?: (ref: string) => void
}
/**
* [Joshen] JFYI am only adding the `isVercelIntegrationFlow` flag to keep the existing
* behaviour for project creation via Vercel integration as similar to keep current state
* for now, what it controls if `true`:
* - Disables organization selection
* - Hides the following:
* - "Internal configuration" section
* - "GitHub repository" field
* - "Free project info" at the bottom
* - Cancel closes the popup window instead of navigating into Studio
* - Shows the following:
* - "Data seeding" section
* - When embedded in the Vercel interstitial, flattens Panel chrome so the shared
* form fields sit inside InterstitialLayout without a nested card
* Eventually we could looking into reducing the differences more, e.g having data seeding
* for both ways, and showing GitHub repository field for Vercel integration
*/
export const ProjectCreationForm = ({
isVercelIntegrationFlow = false,
onCreateSuccess,
}: ProjectCreationFormProps) => {
const track = useTrack()
const router = useRouter()
const { profile } = useProfile()
const { slug, projectName, externalId } = useParams()
const trackFunnelError = useTrackFunnelError()
const defaultProvider = useDefaultProvider()
const surface = isVercelIntegrationFlow ? 'vercel' : 'main'
const { data: currentOrg } = useSelectedOrganizationQuery()
const isFreePlan = currentOrg?.plan?.id === 'free'
const canChooseInstanceSize = !isFreePlan
const { lastVisitedOrganization } = useLastVisitedOrganization()
const { can: isAdmin } = useAsyncCheckPermissions(PermissionAction.CREATE, 'projects')
const { can: canCreateGitHubConnection } = useAsyncCheckPermissions(
PermissionAction.CREATE,
'integrations.github_connections'
)
const showAdvancedConfig = useIsFeatureEnabled('project_creation:show_advanced_config')
const { hasAccess: hasAccessToGitHubIntegration } = useCheckEntitlements(
'integrations.github_connections'
)
const { hasLoaded: flagsLoaded } = useFeatureFlags()
const projectCreationDisabled = useFlag('disableProjectCreationAndUpdate')
const showInternalOnlyConfiguration =
useFlag('newProjectInternalOnlyConfiguration') && !isVercelIntegrationFlow
// Read the raw flag for telemetry — coerce-undefined-to-false would record false for
// users whose flags haven't loaded yet. The raw value preserves undefined (omitted from
// PostHog) so we only record an actual value (boolean true/false, or a variant string
// like 'test'/'control' post-multivariate migration) once the flag has resolved.
const dataApiRevokeOnCreateDefaultFlag = usePHFlag<boolean | string>(
'dataApiRevokeOnCreateDefault'
)
const isDataApiRevokeOnCreateDefault = useDataApiRevokeOnCreateDefaultEnabled()
const isNotOnHigherPlan = !['team', 'enterprise', 'platform'].includes(currentOrg?.plan.id ?? '')
const [allProjects, setAllProjects] = useState<OrgProject[] | undefined>(undefined)
const [isComputeCostsConfirmationModalVisible, setIsComputeCostsConfirmationModalVisible] =
useState(false)
const [projectCreationError, setProjectCreationError] = useState<string>()
const form = useForm<z.infer<typeof FormSchema>>({
resolver: zodResolver(FormSchema),
mode: 'onChange',
defaultValues: {
organization: slug,
projectName: projectName || '',
highAvailability: false,
postgresVersion: '',
instanceType: '',
cloudProvider: PROVIDERS[defaultProvider].id,
dbPass: '',
dbPassStrength: 0,
dbPassStrengthMessage: '',
dbRegion: undefined,
githubRepositoryId: '',
githubInstallationId: undefined,
githubRepositoryName: '',
instanceSize: canChooseInstanceSize ? sizes[0] : undefined,
dataApi: true,
dataApiDefaultPrivileges: !isDataApiRevokeOnCreateDefault,
enableRlsEventTrigger: false,
postgresVersionSelection: '',
useOrioleDb: false,
shouldRunMigrations: true,
},
})
const { getFieldState, resetField, setValue } = form
const {
instanceSize: watchedInstanceSize,
cloudProvider,
dbRegion,
githubRepositoryName,
organization,
projectName: watchedProjectName,
highAvailability,
} = useWatch({ control: form.control })
const { dirtyFields } = useFormState(form)
const isDbRegionDirty = dirtyFields.dbRegion
const smartRegionEnabled = cloudProvider !== 'AWS_NIMBUS'
const highAvailabilityRegionCode = getHighAvailabilityRegionCode()
// Read dirty state during render rather than depending on form.formState in the
// effect — form.formState is a Proxy that gets a new reference every render, which
// would re-fire this effect after each setValue and trigger an infinite loop.
const isDataApiDefaultPrivilegesDirty = getFieldState(
'dataApiDefaultPrivileges',
form.formState
).isDirty
// [Charis] Since the form is updated in a useEffect, there is an edge case
// when switching from free to paid, where canChooseInstanceSize is true for
// an in-between render, but watchedInstanceSize is still undefined from the
// form state carried over from the free plan. To avoid this, we set a
// default instance size in this case.
const instanceSize = canChooseInstanceSize ? (watchedInstanceSize ?? sizes[0]) : undefined
const { data: membersExceededLimit = [] } = useFreeProjectLimitCheckQuery(
{ slug },
{ enabled: isFreePlan }
)
const hasMembersExceedingFreeTierLimit = membersExceededLimit.length > 0
const freePlanWithExceedingLimits = isFreePlan && hasMembersExceedingFreeTierLimit
const { data: organizations = [], isSuccess: isOrganizationsSuccess } = useOrganizationsQuery()
const isEmptyOrganizations = isOrganizationsSuccess && organizations.length <= 0
const { data: approvedOAuthApps = [] } = useAuthorizedAppsQuery(
{ slug },
{ enabled: !isFreePlan && slug !== '_' }
)
const hasOAuthApps = approvedOAuthApps.length > 0
const { data: allOverdueInvoices = [] } = useOverdueInvoicesQuery({
enabled: isNotOnHigherPlan,
})
const overdueInvoices = allOverdueInvoices.filter((x) => x.organization_id === currentOrg?.id)
const hasOutstandingInvoices = isNotOnHigherPlan && overdueInvoices.length > 0
const { data: orgProjectsFromApi } = useOrgProjectsInfiniteQuery({ slug: currentOrg?.slug })
const allOrgProjects = useMemo(
() => orgProjectsFromApi?.pages.flatMap((page) => page.projects),
[orgProjectsFromApi?.pages]
)
const organizationProjects =
allProjects?.filter((project) => project.status !== PROJECT_STATUS.INACTIVE) ?? []
const availableComputeCredits = organizationProjects.length === 0 ? 10 : 0
const additionalMonthlySpend = isFreePlan
? 0
: monthlyInstancePrice(instanceSize) - availableComputeCredits
const selectedCloudProvider = cloudProvider as CloudProvider
const { data: autoDefaultRegion, error: defaultRegionError } = useDefaultRegionQuery(
{
cloudProvider: selectedCloudProvider,
},
{
enabled: flagsLoaded && !smartRegionEnabled,
refetchOnMount: false,
refetchOnWindowFocus: false,
refetchInterval: false,
refetchOnReconnect: false,
retry: false,
}
)
const { data: availableRegionsData, error: availableRegionsError } =
useOrganizationAvailableRegionsQuery(
{
slug: slug,
cloudProvider: PROVIDERS[cloudProvider as CloudProvider].id,
desiredInstanceSize: instanceSize as DesiredInstanceSize,
},
{
enabled: flagsLoaded && smartRegionEnabled,
refetchOnMount: false,
refetchOnWindowFocus: false,
refetchInterval: false,
refetchOnReconnect: false,
}
)
const highAvailabilityRegion =
highAvailability && highAvailabilityRegionCode !== undefined
? availableRegionsData?.all.specific.find(
(region) => region.code === highAvailabilityRegionCode
)
: undefined
const recommendedSmartRegion = smartRegionEnabled
? availableRegionsData?.recommendations.smartGroup.name
: ''
const fixedDefaultRegion = PROVIDERS[selectedCloudProvider].default_region.displayName
const regionError = smartRegionEnabled ? availableRegionsError : defaultRegionError
const defaultRegion =
highAvailability && highAvailabilityRegionCode !== undefined
? highAvailabilityRegion?.name
: smartRegionEnabled
? recommendedSmartRegion
: (autoDefaultRegion ?? fixedDefaultRegion)
const canCreateProject = isAdmin && !freePlanWithExceedingLimits && !hasOutstandingInvoices
const canConfigureGitHubOnCreate =
canCreateProject && hasAccessToGitHubIntegration && canCreateGitHubConnection
const dbRegionExact = smartRegionToExactRegion(dbRegion ?? '')
const availableOrioleVersion = useAvailableOrioleImageVersion(
{
cloudProvider: cloudProvider as CloudProvider,
dbRegion: smartRegionEnabled ? dbRegionExact : (dbRegion ?? ''),
organizationSlug: organization,
},
{ enabled: currentOrg !== null }
)
const userPrimaryEmail = profile?.primary_email?.toLowerCase()
const isUserAtFreeProjectLimit = userPrimaryEmail
? membersExceededLimit.some(
(member) => member.primary_email?.toLowerCase() === userPrimaryEmail
)
: false
const shouldShowFreeProjectInfo =
!!currentOrg && !isFreePlan && !isUserAtFreeProjectLimit && !isVercelIntegrationFlow
const {
gitHubAuthorization,
githubRepos,
hasPartialResponseDueToSSO,
isLoading: isLoadingRepositoryOptions,
refetch: refetchRepositoryOptions,
} = useGitHubRepositoryOptions()
const {
mutate: createProject,
isPending: isCreatingNewProject,
isSuccess: isSuccessNewProject,
} = useProjectCreateMutation({
onSuccess: (res) => {
setProjectCreationError(undefined)
track(
'project_creation_simple_version_submitted',
{
surface,
instanceSize: form.getValues('instanceSize'),
enableRlsEventTrigger: form.getValues('enableRlsEventTrigger'),
dataApiEnabled: form.getValues('dataApi'),
dataApiDefaultPrivilegesGranted: form.getValues('dataApiDefaultPrivileges'),
useOrioleDb: form.getValues('useOrioleDb'),
...(dataApiRevokeOnCreateDefaultFlag !== undefined && {
dataApiRevokeOnCreateDefaultEnabled: dataApiRevokeOnCreateDefaultFlag,
}),
},
{
project: res.ref,
organization: res.organization_slug,
}
)
onCreateSuccess?.(res.ref)
if (surface === 'main') router.push(`/project/${res.ref}`)
},
onError: (error) => {
if (isVercelIntegrationFlow) {
setProjectCreationError(`Failed to create new project: ${error.message}`)
trackFunnelError('project_creation', classifyApiError('project_creation', error), 'form')
return
}
const toastId = toast.error(`Failed to create new project: ${error.message}`)
trackFunnelError(
'project_creation',
classifyApiError('project_creation', error),
'toast',
toastId
)
},
})
const onSubmitWithComputeCostsConfirmation = async (values: z.infer<typeof FormSchema>) => {
const launchingLargerInstance =
values.instanceSize &&
!sizesWithNoCostConfirmationRequired.includes(values.instanceSize as DesiredInstanceSize)
if (additionalMonthlySpend > 0 && (hasOAuthApps || launchingLargerInstance)) {
track('project_creation_simple_version_confirm_modal_opened', {
instanceSize: values.instanceSize,
})
setIsComputeCostsConfirmationModalVisible(true)
} else {
await onSubmit(values)
}
}
const onSubmit = async (values: z.infer<typeof FormSchema>) => {
if (!currentOrg) return console.error('Unable to retrieve current organization')
setProjectCreationError(undefined)
const {
cloudProvider,
projectName,
highAvailability,
dbPass,
dbRegion,
postgresVersion,
instanceType,
instanceSize,
dataApi,
dataApiDefaultPrivileges,
enableRlsEventTrigger,
postgresVersionSelection,
useOrioleDb,
githubInstallationId,
githubRepositoryId,
shouldRunMigrations,
} = values
// HA projects never take a custom version — the API resolves the image from
// postgresEngine + releaseChannel.
const customPostgresVersion = highAvailability ? undefined : postgresVersion
if (customPostgresVersion && !customPostgresVersion.match(/1[2-9]\..*/)) {
const message =
'Invalid Postgres version, should start with a number between 12-19, a dot and additional characters, i.e. 15.2 or 15.2.0-3'
if (isVercelIntegrationFlow) {
setProjectCreationError(message)
return
}
return toast.error(message)
}
if (useOrioleDb && !availableOrioleVersion) {
const message = 'No available OrioleDB image found, only Postgres is available'
if (isVercelIntegrationFlow) {
setProjectCreationError(message)
trackFunnelError(
'project_creation',
{ errorCategory: 'validation', errorReason: 'oriole_unavailable' },
'form'
)
return
}
const toastId = toast.error(message)
trackFunnelError(
'project_creation',
{ errorCategory: 'validation', errorReason: 'oriole_unavailable' },
'toast',
toastId
)
return
}
const { postgresEngine, releaseChannel } =
extractPostgresVersionDetails(postgresVersionSelection)
const { smartGroup = [], specific = [] } = availableRegionsData?.all ?? {}
const selectedRegion =
highAvailability && highAvailabilityRegionCode !== undefined
? specific.find((region) => region.code === highAvailabilityRegionCode)
: smartRegionEnabled
? (smartGroup.find((x) => x.name === dbRegion) ??
specific.find((x) => x.name === dbRegion))
: undefined
if (highAvailability && highAvailabilityRegionCode !== undefined && !selectedRegion) {
return toast.error(
`High Availability projects are not available in the required region (${highAvailabilityRegionCode})`
)
}
const parsedGitHubRepositoryId =
githubRepositoryId.length > 0 ? Number(githubRepositoryId) : undefined
const shouldIncludeGitHubFields =
githubInstallationId !== undefined && Number.isFinite(parsedGitHubRepositoryId)
let dbSql = enableRlsEventTrigger ? AUTO_ENABLE_RLS_EVENT_TRIGGER_SQL : undefined
if (isVercelIntegrationFlow && shouldRunMigrations && !!externalId) {
const id = toast.loading(`Fetching initial migrations from GitHub repository...`)
try {
const migrationSql = await getInitialMigrationSQLFromGitHubRepo(externalId)
if (migrationSql) {
const safeMigrationSql = trimSafeSqlFragment(
acceptUntrustedSql(untrustedSql(migrationSql))
)
dbSql = dbSql
? joinSqlFragments([trimSafeSqlFragment(dbSql), safeMigrationSql], ';\n')
: safeMigrationSql
toast.loading(`Migrations fetched! Creating project...`, { id })
} else {
toast.loading('No migrations found, creating project...')
}
} catch (error) {
toast.loading(
`Failed to fetch migrations: ${error instanceof Error ? error.message : ''}. Proceeding to create project...`,
{ id }
)
}
}
const data: ProjectCreateVariables = {
dbSql,
dbPass,
cloudProvider,
organizationSlug: currentOrg.slug,
name: projectName,
highAvailability,
// gets ignored due to org billing subscription anyway
dbPricingTierId: 'tier_free',
// only set the compute size on pro+ plans. Free plans always use micro (nano in the future) size.
dbInstanceSize: isFreePlan ? undefined : (instanceSize as DesiredInstanceSize),
dataApiExposedSchemas: !dataApi ? [] : undefined,
dataApiUseApiSchema: false,
dataApiRevokeDefaultPrivileges: dataApi && !dataApiDefaultPrivileges,
postgresEngine: highAvailability
? HIGH_AVAILABILITY_POSTGRES_ENGINE
: useOrioleDb
? availableOrioleVersion?.postgres_engine
: postgresEngine,
releaseChannel: highAvailability
? HIGH_AVAILABILITY_RELEASE_CHANNEL
: useOrioleDb
? availableOrioleVersion?.release_channel
: releaseChannel,
...(smartRegionEnabled ? { regionSelection: selectedRegion } : { dbRegion }),
...(shouldIncludeGitHubFields
? {
githubInstallationId,
githubRepositoryId: parsedGitHubRepositoryId,
}
: {}),
}
if (customPostgresVersion || instanceType) {
data['customSupabaseRequest'] = {
ami: {
...(customPostgresVersion && {
search_tags: { 'tag:postgresVersion': customPostgresVersion },
}),
...(instanceType && { instance_type: instanceType }),
},
}
}
createProject(data)
}
const hasTrackedFormExposed = useRef(false)
useEffect(() => {
if (hasTrackedFormExposed.current) return
if (!isOrganizationsSuccess || !canCreateProject || !currentOrg) return
hasTrackedFormExposed.current = true
track('project_creation_form_exposed', { surface })
}, [isOrganizationsSuccess, canCreateProject, currentOrg, track, surface])
useEffect(() => {
// Only set once to ensure compute credits dont change while project is being created
if (allOrgProjects && allOrgProjects.length > 0 && !allProjects) {
setAllProjects(allOrgProjects)
}
}, [allOrgProjects, allProjects, setAllProjects])
useEffect(() => {
// Handle no org: redirect to new org route
if (isEmptyOrganizations && !isVercelIntegrationFlow) {
router.push(`/new`)
}
}, [isEmptyOrganizations, isVercelIntegrationFlow, router])
useEffect(() => {
// [Joshen] Cause slug depends on router which doesnt load immediately on render
// While the form data does load immediately
if (slug && slug !== '_') setValue('organization', slug)
if (projectName) setValue('projectName', projectName || '')
}, [slug, setValue, projectName])
useEffect(() => {
if (!isDbRegionDirty && defaultRegion) {
setValue('dbRegion', defaultRegion)
}
}, [defaultRegion, isDbRegionDirty, setValue])
useEffect(() => {
if (regionError && fixedDefaultRegion) {
resetField('dbRegion', { defaultValue: fixedDefaultRegion })
}
}, [regionError, resetField, fixedDefaultRegion])
useEffect(() => {
if (watchedInstanceSize !== instanceSize) {
setValue('instanceSize', instanceSize, {
shouldDirty: false,
shouldValidate: false,
shouldTouch: false,
})
}
}, [instanceSize, watchedInstanceSize, setValue])
useEffect(() => {
if (!githubRepositoryName) return
if ((watchedProjectName ?? '').trim().length > 0) return
const repoName = githubRepositoryName.split('/').at(-1) ?? githubRepositoryName
setValue('projectName', repoName.trim(), {
shouldValidate: true,
})
}, [githubRepositoryName, watchedProjectName, setValue])
useEffect(() => {
if (dataApiRevokeOnCreateDefaultFlag === undefined) return
if (isDataApiDefaultPrivilegesDirty) return
setValue(
'dataApiDefaultPrivileges',
!isInDataApiRevokeTreatment(dataApiRevokeOnCreateDefaultFlag),
{
shouldDirty: false,
}
)
}, [dataApiRevokeOnCreateDefaultFlag, isDataApiDefaultPrivilegesDirty, setValue])
useEffect(() => {
// This is to make the database.new redirect work correctly. The database.new redirect should be set to supabase.com/dashboard/new/last-visited-org
if (slug === 'last-visited-org') {
if (lastVisitedOrganization) {
router.replace(`/new/${lastVisitedOrganization}`, undefined, { shallow: true })
} else {
router.replace(`/new/_`, undefined, { shallow: true })
}
}
}, [slug, lastVisitedOrganization, router])
return (
<Form {...form}>
<form
onSubmit={form.handleSubmit(onSubmitWithComputeCostsConfirmation, (errors) =>
trackFunnelError(
'project_creation',
classifyValidationError('project_creation', errors),
'form'
)
)}
>
<Panel
loading={!isOrganizationsSuccess}
noMargin={isVercelIntegrationFlow}
className={cn(
isVercelIntegrationFlow && 'border-0 shadow-none rounded-none bg-transparent'
)}
title={
!isVercelIntegrationFlow && (
<div key="panel-title">
<h3>Create a new project</h3>
<p className="text-sm text-foreground-lighter text-balance">
Your project will have its own dedicated instance and full Postgres database. An
API will be set up so you can easily interact with your new database.
</p>
</div>
)
}
footer={
<ProjectCreationFooter
form={form}
canCreateProject={canCreateProject}
instanceSize={instanceSize}
organizationProjects={organizationProjects}
isCreatingNewProject={isCreatingNewProject}
isSuccessNewProject={isSuccessNewProject}
cancelAction={isVercelIntegrationFlow ? 'close' : 'studio'}
/>
}
>
<>
{projectCreationDisabled ? (
<DisabledWarningDueToIncident title="Project creation is currently disabled" />
) : (
<div className="divide-y divide-border-border">
<OrganizationSelector
form={form}
disableOrganizationSelection={isVercelIntegrationFlow}
/>
{canCreateProject && (
<>
{!isVercelIntegrationFlow && canConfigureGitHubOnCreate && (
<Panel.Content>
<GitHubRepositoryField
form={form}
name="githubRepositoryId"
installationIdField="githubInstallationId"
repositoryNameField="githubRepositoryName"
label="GitHub (optional)"
description={
<>
Ideal for agent-first workflows: update your schema in code, push it
to GitHub, and Supabase deploys the changes automatically.{' '}
<a
href="https://supabase.com/docs/guides/deployment/branching/github-integration"
target="_blank"
rel="noreferrer noopener"
className="text-link"
>
Learn more
</a>
</>
}
disabled={isCreatingNewProject}
repositories={githubRepos}
gitHubAuthorization={gitHubAuthorization}
hasPartialResponseDueToSSO={hasPartialResponseDueToSSO}
isLoading={isLoadingRepositoryOptions}
refetch={refetchRepositoryOptions}
onConnectClick={() => track('project_creation_github_connect_clicked')}
/>
</Panel.Content>
)}
<ProjectNameInput form={form} />
<HighAvailabilityInput
form={form}
highAvailabilityRegionName={highAvailabilityRegion?.name}
/>
{canChooseInstanceSize && <ComputeSizeSelector form={form} />}
<DatabasePasswordInput form={form} />
<RegionSelector
form={form}
instanceSize={instanceSize as DesiredInstanceSize}
/>
{isVercelIntegrationFlow && !!externalId && <DataSeeding form={form} />}
<SecurityOptions form={form} surface={surface} />
{showInternalOnlyConfiguration && <InternalOnlyConfiguration form={form} />}
{showAdvancedConfig &&
!!availableOrioleVersion &&
highAvailability !== true && <AdvancedConfiguration form={form} />}
{shouldShowFreeProjectInfo ? (
<Admonition
className="rounded-none border-0"
type="note"
title="Need a free project?"
description={
<p>
You can have up to 2 free projects across all organizations.{' '}
<Link className="underline text-foreground" href="/new">
Create a free organization
</Link>{' '}
to use them.
</p>
}
/>
) : null}
</>
)}
{freePlanWithExceedingLimits ? (
isAdmin &&
slug && (
<FreeProjectLimitWarning membersExceededLimit={membersExceededLimit || []} />
)
) : hasOutstandingInvoices ? (
<Panel.Content>
<Admonition
type="default"
title="Your organization has overdue invoices"
description={
<div className="space-y-3">
<p className="text-sm leading-normal">
Please resolve all outstanding invoices first before creating a new
project
</p>
<div>
<Button asChild variant="default">
<Link href={`/org/${slug}/billing#invoices`}>View invoices</Link>
</Button>
</div>
</div>
}
/>
</Panel.Content>
) : null}
</div>
)}
{projectCreationError && (
<Panel.Content>
<p role="alert" className="text-sm text-destructive">
{projectCreationError}
</p>
</Panel.Content>
)}
</>
</Panel>
<ConfirmationModal
size="large"
variant="warning"
loading={isCreatingNewProject}
visible={isComputeCostsConfirmationModalVisible}
title="Confirm compute costs"
confirmLabel="I understand"
onCancel={() => setIsComputeCostsConfirmationModalVisible(false)}
onConfirm={async () => {
const values = form.getValues()
await onSubmit(values)
setIsComputeCostsConfirmationModalVisible(false)
}}
>
<div className="text-sm text-foreground-light space-y-1">
<p>
Launching a project on compute size "{instanceLabel(instanceSize)}" increases your
monthly costs by ${additionalMonthlySpend}, independent of how actively you use it. By
clicking "I understand", you agree to the additional costs.{' '}
<Link
href={`${DOCS_URL}/guides/platform/manage-your-usage/compute`}
target="_blank"
className="underline"
>
Compute Costs
</Link>{' '}
are non-refundable.
</p>
</div>
</ConfirmationModal>
</form>
</Form>
)
}