Repository: supabase/supabase
Pull request #33966: Chore/support updating email and unlinking identities
Author: login: joshenlim
Created: 2025-03-03T12:00:51Z | Merged: 2025-03-21T09:15:05Z

Fixes FE-1438

> [!WARNING]
> Dependant on API PR: https://github.com/supabase/infrastructure/pull/21806
> Ready on staging - ensure that changes are through to prod prior to merging

Changes are mainly in Account Preferences (/account/me) - ignore the TS issues for now, will fix again once the API PR goes through cause I think there's some discrepancies with the API typing

<img src="https://github.com/user-attachments/assets/a527a2b7-4053-42f7-85b2-e7ae00e51ecc" />

## Some context
- An account can only have GH, or Email, or both - SSO identities cannot be associated with any other identity (So SSO accounts are the only identity for that account)
- To create an account with Email + GH identities, create an Email + Password account first with an email that matches the GH account's email. Thereafter, log out, and log in via GH with that same email - accounts will then be linked afterwards
- Note that if you unlink the identity that you're currently logged in with, we currently don't log users out immediately because there's no way atm to identity which identity the user is currently logged in with
  - Auth team is aware of this and will work on the required changes to support this

## Changes involved
- Account Information is now replaced with Account Identities
- Unlink CTA only shows up if there's more than 1 identity
- Reset password CTA only shows up for the email identity
- Update email address modal content will vary based on the provider

## Things to test
- [ ] Can view all identities of the account
- [ ] Can change email for email password identity (And verify that the user flow is clear)
  - I won't explain how the flow works in this PR, but if you have trouble figuring that you - means that we need to improve the UX here
- [ ] Update email address modal contents are contextualized to the provider
- [ ] Can unlink an identity if there's more than 1 identity tied to the account


Diff:
diff --git a/apps/studio/components/interfaces/Account/Preferences/AccountIdentities.tsx b/apps/studio/components/interfaces/Account/Preferences/AccountIdentities.tsx
new file mode 100644
index 0000000000000..1f68854fcc3ed
--- /dev/null
+++ b/apps/studio/components/interfaces/Account/Preferences/AccountIdentities.tsx
@@ -0,0 +1,192 @@
+import dayjs from 'dayjs'
+import { Edit, Unlink } from 'lucide-react'
+import Image from 'next/image'
+import Link from 'next/link'
+import { useRouter } from 'next/router'
+import { useEffect, useState } from 'react'
+import { toast } from 'sonner'
+
+import { ButtonTooltip } from 'components/ui/ButtonTooltip'
+import Panel from 'components/ui/Panel'
+import { useProfileIdentitiesQuery } from 'data/profile/profile-identities-query'
+import { useUnlinkIdentityMutation } from 'data/profile/profile-unlink-identity-mutation'
+import { BASE_PATH } from 'lib/constants'
+import {
+  Badge,
+  Button,
+  cn,
+  Dialog,
+  DialogContent,
+  DialogHeader,
+  DialogTitle,
+  Tooltip,
+  TooltipContent,
+  TooltipTrigger,
+} from 'ui'
+import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal'
+import ShimmeringLoader from 'ui-patterns/ShimmeringLoader'
+import {
+  ChangeEmailAddressForm,
+  GitHubChangeEmailAddress,
+  SSOChangeEmailAddress,
+} from './ChangeEmailAddress'
+
+export const AccountIdentities = () => {
+  const router = useRouter()
+
+  const { data, isLoading, isSuccess } = useProfileIdentitiesQuery()
+  const identities = data?.identities ?? []
+  const isChangeExpired = data?.email_change_sent_at
+    ? dayjs().utc().diff(dayjs(data?.email_change_sent_at).utc(), 'minute') > 10
+    : false
+
+  const [selectedProviderUnlink, setSelectedProviderUnlink] = useState<string>()
+  const [selectedProviderUpdateEmail, setSelectedProviderUpdateEmail] = useState<string>()
+
+  const { mutate: unlinkIdentity, isLoading: isUnlinking } = useUnlinkIdentityMutation({
+    onSuccess: () => {
+      toast.success(
+        `Successfully unlinked ${getProviderName(selectedProviderUnlink ?? '')} identity!`
+      )
+      setSelectedProviderUnlink(undefined)
+    },
+  })
+
+  const [_, message] = router.asPath.split('#message=')
+
+  const getProviderName = (provider: string) =>
+    provider === 'github' ? 'GitHub' : provider.startsWith('sso') ? 'SSO' : provider
+
+  const onConfirmUnlinkIdentity = async () => {
+    const identity = identities.find((i) => i.provider === selectedProviderUnlink)
+    if (identity) unlinkIdentity(identity)
+  }
+
+  useEffect(() => {
+    if (message) toast.success(message.replaceAll('+', ' '))
+  }, [message])
+
+  return (
+    <>
+      <Panel className="mb-4 md:mb-8" title={<h5>Account Identities</h5>}>
+        {isLoading && (
+          <Panel.Content>
+            <ShimmeringLoader />
+          </Panel.Content>
+        )}
+        {isSuccess &&
+          identities.map((identity) => {
+            const { identity_id, provider } = identity
+            const username = identity.identity_data?.user_name
+            const providerName = getProviderName(provider)
+            const iconKey =
+              provider === 'github'
+                ? 'github-icon'
+                : provider === 'email'
+                  ? 'email-icon2'
+                  : 'saml-icon'
+
+            return (
+              <Panel.Content
+                key={identity_id}
+                className={cn('flex justify-between', identities.length > 1 ? 'last:border-t' : '')}
+              >
+                <div className="flex gap-x-4">
+                  <Image
+                    className={cn(iconKey === 'github-icon' ? 'dark:invert' : '')}
+                    src={`${BASE_PATH}/img/icons/${iconKey}.svg`}
+                    width={30}
+                    height={30}
+                    alt={`${identity.provider} icon`}
+                  />
+                  <div>
+                    <div className="flex items-center gap-x-2">
+                      <p className="text-sm capitalize">{providerName}</p>
+                      {provider === 'email' && data.new_email && !isChangeExpired && (
+                        <Tooltip>
+                          <TooltipTrigger className="flex items-center">
+                            <Badge variant="default">Pending change</Badge>
+                          </TooltipTrigger>
+                          <TooltipContent>Changing to {data.new_email}</TooltipContent>
+                        </Tooltip>
+                      )}
+                      {/* [Joshen] Below is not supported yet, but ideal UX */}
+                      {/* {false && <Badge>Logged in as</Badge>} */}
+                    </div>
+                    <p className="text-sm text-foreground-lighter">
+                      {!!username ? <span>{username} • </span> : null}
+                      {(identity as any).email}
+                    </p>
+                  </div>
+                </div>
+                <div className="flex items-center gap-x-1">
+                  {provider === 'email' && (
+                    <Button asChild type="default">
+                      <Link href="/reset-password">Reset password</Link>
+                    </Button>
+                  )}
+                  <ButtonTooltip
+                    type="text"
+                    icon={<Edit />}
+                    className="w-7"
+                    onClick={() => setSelectedProviderUpdateEmail(provider)}
+                    tooltip={{ content: { side: 'bottom', text: 'Update email address' } }}
+                  />
+                  {identities.length > 1 && (
+                    <ButtonTooltip
+                      type="text"
+                      icon={<Unlink />}
+                      className="w-7"
+                      onClick={() => setSelectedProviderUnlink(provider)}
+                      tooltip={{ content: { side: 'bottom', text: 'Unlink identity' } }}
+                    />
+                  )}
+                </div>
+              </Panel.Content>
+            )
+          })}
+      </Panel>
+
+      <Dialog
+        open={!!selectedProviderUpdateEmail}
+        onOpenChange={(open: boolean) => {
+          if (!open) setSelectedProviderUpdateEmail(undefined)
+        }}
+      >
+        <DialogContent>
+          <DialogHeader className="border-b">
+            <DialogTitle>
+              {selectedProviderUpdateEmail !== 'email'
+                ? `Updating email address for ${getProviderName(selectedProviderUpdateEmail ?? '')} identity`
+                : 'Update email address'}
+            </DialogTitle>
+          </DialogHeader>
+          {selectedProviderUpdateEmail === 'github' ? (
+            <GitHubChangeEmailAddress />
+          ) : selectedProviderUpdateEmail?.startsWith('sso') ? (
+            <SSOChangeEmailAddress />
+          ) : (
+            <ChangeEmailAddressForm onClose={() => setSelectedProviderUpdateEmail(undefined)} />
+          )}
+        </DialogContent>
+      </Dialog>
+
+      <ConfirmationModal
+        variant="warning"
+        size="small"
+        loading={isUnlinking}
+        visible={!!selectedProviderUnlink}
+        title={`Unlink ${getProviderName(selectedProviderUnlink ?? '')} identity`}
+        onCancel={() => setSelectedProviderUnlink(undefined)}
+        onConfirm={onConfirmUnlinkIdentity}
+        confirmLabel="Unlink identity"
+        confirmLabelLoading="Unlinking identity"
+        alert={{
+          base: { variant: 'warning' },
+          title: `Confirm to disconnect your ${getProviderName(selectedProviderUnlink ?? '')} identity`,
+          description: `After disconnecting, you will only be able to sign in via ${selectedProviderUnlink === 'github' ? 'email and password' : 'your GitHub identity'}`,
+        }}
+      />
+    </>
+  )
+}
diff --git a/apps/studio/components/interfaces/Account/Preferences/AccountInformation.tsx b/apps/studio/components/interfaces/Account/Preferences/AccountInformation.tsx
deleted file mode 100644
index 8a23254d4a455..0000000000000
--- a/apps/studio/components/interfaces/Account/Preferences/AccountInformation.tsx
+++ /dev/null
@@ -1,119 +0,0 @@
-import { ChevronDown } from 'lucide-react'
-import Link from 'next/link'
-
-import Panel from 'components/ui/Panel'
-import type { Profile } from 'data/profile/types'
-import { useSession } from 'lib/auth'
-import {
-  Button,
-  CollapsibleContent_Shadcn_,
-  CollapsibleTrigger_Shadcn_,
-  Collapsible_Shadcn_,
-  Input_Shadcn_,
-  Label_Shadcn_,
-} from 'ui'
-
-const AccountInformation = ({ profile }: { profile?: Profile }) => {
-  const session = useSession()
-
-  return (
-    <Panel
-      title={
-        <h5 key="panel-title" className="mb-0">
-          Account Information
-        </h5>
-      }
-    >
-      <Panel.Content>
-        <div className="grid gap-4">
-          <div className="grid grid-cols-12 gap-6">
-            <Label_Shadcn_ className="col-span-4 text-foreground-light">Username</Label_Shadcn_>
-            <Input_Shadcn_
-              className="col-span-8"
-              readOnly
-              disabled
-              placeholder="Username"
-              value={profile?.username ?? ''}
-            />
-          </div>
-          <div className="grid grid-cols-12 gap-6">
-            <Label_Shadcn_ className="col-span-4 text-foreground-light">Email</Label_Shadcn_>
-
-            <div className="col-span-8">
-              <Input_Shadcn_
-                className="w-full"
-                readOnly
-                disabled
-                placeholder="Email"
-                value={profile?.primary_email ?? ''}
-              />
-              <div className="col-span-8 mt-4">
-                <Collapsible_Shadcn_ className="-mt-1.5 pb-1.5">
-                  <CollapsibleTrigger_Shadcn_ className="group  font-normal p-0 [&[data-state=open]>div>svg]:!-rotate-180">
-                    <div className="flex items-center gap-x-2 w-full">
-                      <p className="text-xs text-foreground-light group-hover:text-foreground transition">
-                        How can I change my email?
-                      </p>
-                      <ChevronDown
-                        className="transition-transform duration-200"
-                        strokeWidth={1.5}
-                        size={14}
-                      />
-                    </div>
-                  </CollapsibleTrigger_Shadcn_>
-                  <CollapsibleContent_Shadcn_ className=" mt-2">
-                    <div className="bg-surface-200 p-4 rounded-lg grid gap-4">
-                      {session?.user.app_metadata.provider === 'email' ? (
-                        <div>
-                          <ol className="text-sm ml-4 pl-2 list-decimal">
-                            <li>Create a new account with the new email</li>
-                            <li>Invite this email to be an owner of the Org</li>
-                            <li>Join the Org</li>
-                            <li>Remove the old email from the Org</li>
-                            <li>Email Supabase Support to delete the old email (Optional) </li>
-                          </ol>
-                        </div>
-                      ) : (
-                        <div>
-                          <ol className="text-sm ml-4 pl-2 list-decimal">
-                            <li>Log out of Supabase</li>
-                            <li>
-                              Change your Primary Email in{' '}
-                              <Link href="https://docs.github.com/en/account-and-profile/setting-up-and-managing-your-personal-account-on-github/managing-email-preferences/changing-your-primary-email-address">
-                                GitHub
-                              </Link>{' '}
-                              (your public, not primary email)
-                            </li>
-                            <li>Log out of GitHub</li>
-                            <li>
-                              Log back into GitHub (with the new, desired email set as primary)
-                            </li>
-                            <li>Log back into Supabase</li>
-                          </ol>
-                        </div>
-                      )}
-                    </div>
-                  </CollapsibleContent_Shadcn_>
-                </Collapsible_Shadcn_>
-              </div>
-            </div>
-          </div>
-          {session?.user.app_metadata.provider === 'email' && (
-            <div className="text-sm grid gap-2 md:grid md:grid-cols-12 md:gap-x-4">
-              <div className="flex flex-col space-y-2 col-span-4 ">
-                <p className="text-foreground-light break-all">Password</p>
-              </div>
-              <div className="col-span-8">
-                <Button asChild type="default">
-                  <Link href="/reset-password">Reset password</Link>
-                </Button>
-              </div>
-            </div>
-          )}
-        </div>
-      </Panel.Content>
-    </Panel>
-  )
-}
-
-export default AccountInformation
diff --git a/apps/studio/components/interfaces/Account/Preferences/AnalyticsSettings.tsx b/apps/studio/components/interfaces/Account/Preferences/AnalyticsSettings.tsx
index 81c22e953eac6..221e28ea833e5 100644
--- a/apps/studio/components/interfaces/Account/Preferences/AnalyticsSettings.tsx
+++ b/apps/studio/components/interfaces/Account/Preferences/AnalyticsSettings.tsx
@@ -4,7 +4,7 @@ import Panel from 'components/ui/Panel'
 import { useSendResetMutation } from 'data/telemetry/send-reset-mutation'
 import { useAppStateSnapshot } from 'state/app-state'
 
-const AnalyticsSettings = () => {
+export const AnalyticsSettings = () => {
   const snap = useAppStateSnapshot()
   const { mutate: sendReset } = useSendResetMutation()
 
@@ -27,5 +27,3 @@ const AnalyticsSettings = () => {
     </Panel>
   )
 }
-
-export default AnalyticsSettings
diff --git a/apps/studio/components/interfaces/Account/Preferences/ChangeEmailAddress.tsx b/apps/studio/components/interfaces/Account/Preferences/ChangeEmailAddress.tsx
new file mode 100644
index 0000000000000..c853e7514c992
--- /dev/null
+++ b/apps/studio/components/interfaces/Account/Preferences/ChangeEmailAddress.tsx
@@ -0,0 +1,134 @@
+import HCaptcha from '@hcaptcha/react-hcaptcha'
+import { zodResolver } from '@hookform/resolvers/zod'
+import { useRef, useState } from 'react'
+import { SubmitHandler, useForm } from 'react-hook-form'
+import { toast } from 'sonner'
+import * as z from 'zod'
+
+import { InlineLink } from 'components/ui/InlineLink'
+import { useEmailUpdateMutation } from 'data/profile/profile-update-email-mutation'
+import {
+  Button,
+  DialogFooter,
+  DialogSection,
+  Form_Shadcn_,
+  FormControl_Shadcn_,
+  FormField_Shadcn_,
+  Input_Shadcn_,
+} from 'ui'
+import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout'
+
+export const GitHubChangeEmailAddress = () => {
+  return (
+    <DialogSection className="flex flex-col gap-y-2">
+      <p className="text-sm">
+        Email addresses for GitHub identities should be updated through GitHub
+      </p>
+      <ol className="flex flex-col gap-y-0.5 text-sm ml-4 pl-2 list-decimal text-foreground-light">
+        <li>Log out of Supabase</li>
+        <li>
+          Change your Primary Email in{' '}
+          <InlineLink href="https://docs.github.com/en/account-and-profile/setting-up-and-managing-your-personal-account-on-github/managing-email-preferences/changing-your-primary-email-address">
+            GitHub
+          </InlineLink>{' '}
+          (your primary email)
+        </li>
+        <li>Log out of GitHub</li>
+        <li>Log back into GitHub (with the new, desired email set as primary)</li>
+        <li>Log back into Supabase</li>
+      </ol>
+    </DialogSection>
+  )
+}
+
+export const SSOChangeEmailAddress = () => {
+  return (
+    <DialogSection className="flex flex-col gap-y-2">
+      <p className="text-sm">
+        Email addresses for SSO should be updated through your identity provider
+      </p>
+      <ol className="flex flex-col gap-y-0.5 text-sm ml-4 pl-2 list-decimal text-foreground-light">
+        <li>Contact the owner / admin for your team to change your email</li>
+      </ol>
+    </DialogSection>
+  )
+}
+
+export const ChangeEmailAddressForm = ({ onClose }: { onClose: () => void }) => {
+  const captchaRef = useRef<HCaptcha>(null)
+  const [captchaToken, setCaptchaToken] = useState<string | null>(null)
+
+  const FormSchema = z.object({ email: z.string().email() })
+  const form = useForm<z.infer<typeof FormSchema>>({
+    mode: 'onBlur',
+    reValidateMode: 'onBlur',
+    resolver: zodResolver(FormSchema),
+    defaultValues: { email: '' },
+  })
+
+  const { mutate: updateEmail, isLoading } = useEmailUpdateMutation({
+    onSuccess: (_, vars) => {
+      toast.success(
+        `A confirmation email has been sent to ${vars.email}. Please confirm the change within 10 minutes.`
+      )
+      onClose()
+    },
+    onError: (error) => {
+      toast.error(`Failed to update email: ${error.message}`)
+      setCaptchaToken(null)
+      captchaRef.current?.resetCaptcha()
+    },
+  })
+
+  const onSubmit: SubmitHandler<z.infer<typeof FormSchema>> = async (values) => {
+    let token = captchaToken
+    if (!token) {
+      const captchaResponse = await captchaRef.current?.execute({ async: true })
+      token = captchaResponse?.response ?? null
+    }
+
+    updateEmail({ email: values.email, hcaptchaToken: token ?? null })
+  }
+
+  return (
+    <Form_Shadcn_ {...form}>
+      <form id="update-email-form" onSubmit={form.handleSubmit(onSubmit)}>
+        <div className="self-center">
+          <HCaptcha
+            ref={captchaRef}
+            sitekey={process.env.NEXT_PUBLIC_HCAPTCHA_SITE_KEY!}
+            size="invisible"
+            onVerify={(token) => setCaptchaToken(token)}
+            onExpire={() => setCaptchaToken(null)}
+          />
+        </div>
+
+        <DialogSection>
+          <FormField_Shadcn_
+            name="email"
+            control={form.control}
+            render={({ field }) => (
+              <FormItemLayout
+                label="Provide a new email address"
+                description="A confirmation email will be sent to the provided email address"
+              >
+                <FormControl_Shadcn_>
+                  <Input_Shadcn_ {...field} placeholder="example@email.com" />
+                </FormControl_Shadcn_>
+              </FormItemLayout>
+            )}
+          />
+        </DialogSection>
+
+        <DialogFooter>
+          <Button type="default" disabled={isLoading} onClick={onClose}>
+            Cancel
+          </Button>
+          <Button htmlType="submit" loading={isLoading} disabled={isLoading}>
+            Confirm
+          </Button>
+        </DialogFooter>
+      </form>
+    </Form_Shadcn_>
+  )
+}
diff --git a/apps/studio/components/interfaces/Account/Preferences/DeleteAccountButton.tsx b/apps/studio/components/interfaces/Account/Preferences/DeleteAccountButton.tsx
index c87f19e247864..dea8f8ccf889b 100644
--- a/apps/studio/components/interfaces/Account/Preferences/DeleteAccountButton.tsx
+++ b/apps/studio/components/interfaces/Account/Preferences/DeleteAccountButton.tsx
@@ -14,9 +14,9 @@ import {
   Dialog,
   DialogContent,
   DialogDescription,
+  DialogFooter,
   DialogHeader,
   DialogSection,
-  DialogSectionSeparator,
   DialogTitle,
   DialogTrigger,
   FormControl_Shadcn_,
@@ -108,8 +108,8 @@ export const DeleteAccountButton = () => {
           Request to delete account
         </Button>
       </DialogTrigger>
-      <DialogContent className="!w-[500px]">
-        <DialogHeader className="pb-0">
+      <DialogContent className="!w-[450px]">
+        <DialogHeader className="border-b">
           {(organizations ?? []).length > 0 ? (
             <>
               <DialogTitle>Leave all organizations before requesting account deletion</DialogTitle>
@@ -151,33 +151,34 @@ export const DeleteAccountButton = () => {
               <Form_Shadcn_ {...form}>
                 <form
                   id="account-deletion-request"
-                  className="flex flex-col gap-y-4"
                   onSubmit={form.handleSubmit(() => onConfirmDelete())}
                 >
-                  <FormField_Shadcn_
-                    name="account"
-                    control={form.control}
-                    render={({ field }) => (
-                      <FormItem_Shadcn_ className="px-7">
-                        <FormLabel_Shadcn_>
-                          Please type{' '}
-                          <span className="font-bold">{profile?.primary_email ?? ''}</span> to
-                          confirm
-                        </FormLabel_Shadcn_>
-                        <FormControl_Shadcn_>
-                          <Input_Shadcn_
-                            autoFocus
-                            {...field}
-                            autoComplete="off"
-                            disabled={isLoading}
-                            placeholder="Enter the account above"
-                          />
-                        </FormControl_Shadcn_>
-                      </FormItem_Shadcn_>
-                    )}
-                  />
-                  <DialogSectionSeparator />
-                  <div className="px-7 pb-4">
+                  <DialogSection>
+                    <FormField_Shadcn_
+                      name="account"
+                      control={form.control}
+                      render={({ field }) => (
+                        <FormItem_Shadcn_>
+                          <FormLabel_Shadcn_>
+                            Please type{' '}
+                            <span className="font-bold">{profile?.primary_email ?? ''}</span> to
+                            confirm
+                          </FormLabel_Shadcn_>
+                          <FormControl_Shadcn_>
+                            <Input_Shadcn_
+                              autoFocus
+                              {...field}
+                              autoComplete="off"
+                              disabled={isLoading}
+                              placeholder="Enter the account above"
+                            />
+                          </FormControl_Shadcn_>
+                        </FormItem_Shadcn_>
+                      )}
+                    />
+                  </DialogSection>
+
+                  <DialogFooter>
                     <Button
                       block
                       size="small"
@@ -188,7 +189,7 @@ export const DeleteAccountButton = () => {
                     >
                       Submit request for account deletion
                     </Button>
-                  </div>
+                  </DialogFooter>
                 </form>
               </Form_Shadcn_>
             )}
diff --git a/apps/studio/components/interfaces/Account/Preferences/ProfileInformation.tsx b/apps/studio/components/interfaces/Account/Preferences/ProfileInformation.tsx
index 55d8808234003..e4497165102f7 100644
--- a/apps/studio/components/interfaces/Account/Preferences/ProfileInformation.tsx
+++ b/apps/studio/components/interfaces/Account/Preferences/ProfileInformation.tsx
@@ -15,7 +15,7 @@ import z from 'zod'
 import { FormActions } from 'components/ui/Forms/FormActions'
 import Panel from 'components/ui/Panel'
 import { useProfileUpdateMutation } from 'data/profile/profile-update-mutation'
-import type { Profile } from 'data/profile/types'
+import { useProfile } from 'lib/profile'
 import type { FormSchema } from 'types'
 
 const FormSchema = z.object({
@@ -25,10 +25,12 @@ const FormSchema = z.object({
 
 const formId = 'profile-information-form'
 
-export const ProfileInformation = ({ profile }: { profile: Profile }) => {
+export const ProfileInformation = () => {
+  const { profile } = useProfile()
+
   const form = useForm({
     resolver: zodResolver(FormSchema),
-    defaultValues: { first_name: profile.first_name ?? '', last_name: profile.last_name ?? '' },
+    defaultValues: { first_name: profile?.first_name ?? '', last_name: profile?.last_name ?? '' },
   })
 
   const { mutate: updateProfile, isLoading } = useProfileUpdateMutation({
diff --git a/apps/studio/components/interfaces/Account/Preferences/index.ts b/apps/studio/components/interfaces/Account/Preferences/index.ts
deleted file mode 100644
index 4c1f94c38e27d..0000000000000
--- a/apps/studio/components/interfaces/Account/Preferences/index.ts
+++ /dev/null
@@ -1,2 +0,0 @@
-export { default as AccountInformation } from './AccountInformation'
-export { default as AnalyticsSettings } from './AnalyticsSettings'
diff --git a/apps/studio/components/interfaces/Organization/BillingSettings/PaymentMethods/PaymentMethods.tsx b/apps/studio/components/interfaces/Organization/BillingSettings/PaymentMethods/PaymentMethods.tsx
index 85a6504bf5121..7b950d285d73e 100644
--- a/apps/studio/components/interfaces/Organization/BillingSettings/PaymentMethods/PaymentMethods.tsx
+++ b/apps/studio/components/interfaces/Organization/BillingSettings/PaymentMethods/PaymentMethods.tsx
@@ -1,9 +1,10 @@
 import { PermissionAction } from '@supabase/shared-types/out/constants'
-import { useParams } from 'common'
+import { CreditCardIcon, Plus } from 'lucide-react'
 import Link from 'next/link'
 import { useState } from 'react'
 import { toast } from 'sonner'
 
+import { useParams } from 'common'
 import AddNewPaymentMethodModal from 'components/interfaces/Billing/Payment/AddNewPaymentMethodModal'
 import {
   ScaffoldSection,
@@ -21,7 +22,6 @@ import { useOrgSubscriptionQuery } from 'data/subscriptions/org-subscription-que
 import { useCheckPermissions } from 'hooks/misc/useCheckPermissions'
 import { useSelectedOrganization } from 'hooks/misc/useSelectedOrganization'
 import { getURL } from 'lib/helpers'
-import { CreditCardIcon, Plus } from 'lucide-react'
 import { Alert, Button } from 'ui'
 import ChangePaymentMethodModal from './ChangePaymentMethodModal'
 import CreditCard from './CreditCard'
diff --git a/apps/studio/components/interfaces/Settings/Database/ConnectionPooling/ConnectionPooling.tsx b/apps/studio/components/interfaces/Settings/Database/ConnectionPooling/ConnectionPooling.tsx
index 165863f7aa626..176a054cef0d2 100644
--- a/apps/studio/components/interfaces/Settings/Database/ConnectionPooling/ConnectionPooling.tsx
+++ b/apps/studio/components/interfaces/Settings/Database/ConnectionPooling/ConnectionPooling.tsx
@@ -250,7 +250,7 @@ export const ConnectionPooling = () => {
           pgbouncer_enabled: true,
           ignore_startup_parameters: pgbouncerConfig.ignore_startup_parameters ?? '',
           pool_mode: pool_mode as 'transaction' | 'session' | 'statement',
-          max_client_conn,
+          max_client_conn: max_client_conn ?? undefined,
           default_pool_size: default_pool_size as number | undefined,
         },
         {
@@ -274,7 +274,7 @@ export const ConnectionPooling = () => {
           ref: projectRef,
           pgbouncer_enabled: false,
           ignore_startup_parameters: pgbouncerConfig.ignore_startup_parameters ?? '',
-          pool_mode: pgbouncerConfig.pool_mode as 'transaction' | 'session' | 'statement',
+          pool_mode: pgbouncerConfig.pool_mode,
         })
       }
       updateSupavisorConfig(
@@ -453,7 +453,7 @@ export const ConnectionPooling = () => {
                                 form.setValue('max_client_conn', supavisorConfig.max_client_conn)
                               } else if (e === 'PgBouncer' && pgbouncerConfig) {
                                 form.setValue('type', 'PgBouncer')
-                                form.setValue('pool_mode', pgbouncerConfig.pool_mode as any)
+                                form.setValue('pool_mode', pgbouncerConfig.pool_mode)
                                 form.setValue(
                                   'default_pool_size',
                                   pgbouncerConfig.default_pool_size as any
diff --git a/apps/studio/data/api-keys/api-keys-query.ts b/apps/studio/data/api-keys/api-keys-query.ts
index 27a5bcaeabc0a..2ade5311c9d83 100644
--- a/apps/studio/data/api-keys/api-keys-query.ts
+++ b/apps/studio/data/api-keys/api-keys-query.ts
@@ -2,7 +2,6 @@ import { useQuery, UseQueryOptions } from '@tanstack/react-query'
 import { get, handleError } from 'data/fetchers'
 import { ResponseError } from 'types'
 
-import { components } from 'api-types'
 import { apiKeysKeys } from './keys'
 
 type LegacyKeys = {
@@ -13,7 +12,7 @@ type LegacyKeys = {
   inserted_at?: string | null
   name: string
   prefix?: string | null
-  secret_jwt_template?: components['schemas']['ApiKeySecretJWTTemplate'] | null
+  secret_jwt_template?: { role: string } | null
   type: 'legacy' | null
   updated_at?: string | null
 }
@@ -26,7 +25,7 @@ type SecretKeys = {
   inserted_at: string
   name: string
   prefix: string
-  secret_jwt_template: components['schemas']['ApiKeySecretJWTTemplate']
+  secret_jwt_template: { role: string }
   type: 'secret'
   updated_at?: string
 }
@@ -39,7 +38,7 @@ type PublishableKeys = {
   inserted_at: string
   name: string
   prefix?: string
-  secret_jwt_template?: components['schemas']['ApiKeySecretJWTTemplate'] | null
+  secret_jwt_template?: { role: string } | null
   type: 'publishable'
   updated_at?: string
 }
diff --git a/apps/studio/data/config/project-upgrade-eligibility-query.ts b/apps/studio/data/config/project-upgrade-eligibility-query.ts
index 0da7ddc744042..f63fb0d4629f3 100644
--- a/apps/studio/data/config/project-upgrade-eligibility-query.ts
+++ b/apps/studio/data/config/project-upgrade-eligibility-query.ts
@@ -1,14 +1,14 @@
 import { useQuery, UseQueryOptions } from '@tanstack/react-query'
 
+import { components } from 'api-types'
 import { IS_PLATFORM } from 'common'
 import { get, handleError } from 'data/fetchers'
 import { useProjectByRef } from 'hooks/misc/useSelectedProject'
 import { PROJECT_STATUS } from 'lib/constants/infrastructure'
 import type { ResponseError } from 'types'
 import { configKeys } from './keys'
-import { components } from 'api-types'
 
-export type ProjectUpgradeTargetVersion = components['schemas']['ProjectVersion']
+export type ProjectUpgradeTargetVersion = { postgres_version: string; release_channel: string }
 export type ProjectUpgradeEligibilityVariables = { projectRef?: string }
 export type ProjectUpgradeEligibilityResponse =
   components['schemas']['ProjectUpgradeEligibilityResponse']
diff --git a/apps/studio/data/edge-functions/edge-functions-deploy-mutation.ts b/apps/studio/data/edge-functions/edge-functions-deploy-mutation.ts
index 7a0a2da951bb1..d10eff3efc40a 100644
--- a/apps/studio/data/edge-functions/edge-functions-deploy-mutation.ts
+++ b/apps/studio/data/edge-functions/edge-functions-deploy-mutation.ts
@@ -8,7 +8,7 @@ import { edgeFunctionsKeys } from './keys'
 
 export type EdgeFunctionsDeployVariables = {
   projectRef: string
-  metadata: components['schemas']['FunctionDeployMetadata']
+  metadata: components['schemas']['FunctionDeployBody']['metadata']
   files: { name: string; content: string }[]
 }
 
@@ -30,7 +30,7 @@ export async function deployEdgeFunction({
 
       formData.append('metadata', JSON.stringify(body.metadata))
 
-      body.file.forEach((f: any) => {
+      body?.file?.forEach((f: any) => {
         const file = f as { name: string; content: string }
         const blob = new Blob([file.content], { type: 'text/plain' })
         formData.append('file', blob, file.name)
diff --git a/apps/studio/data/profile/keys.ts b/apps/studio/data/profile/keys.ts
index bd25d359282ba..829e7986d075b 100644
--- a/apps/studio/data/profile/keys.ts
+++ b/apps/studio/data/profile/keys.ts
@@ -1,6 +1,7 @@
 export const profileKeys = {
   profile: () => ['profile'] as const,
   mfaFactors: () => ['mfa', 'factors'] as const,
+  identities: () => ['profile', 'identities'] as const,
   aaLevel: () => ['mfa', 'aaLevel'] as const,
   auditLogs: ({
     date_start,
diff --git a/apps/studio/data/profile/profile-identities-query.ts b/apps/studio/data/profile/profile-identities-query.ts
new file mode 100644
index 0000000000000..b5eca84e2ff2a
--- /dev/null
+++ b/apps/studio/data/profile/profile-identities-query.ts
@@ -0,0 +1,32 @@
+import type { UserIdentity } from '@supabase/supabase-js'
+import { useQuery, UseQueryOptions } from '@tanstack/react-query'
+
+import { auth } from 'lib/gotrue'
+import { profileKeys } from './keys'
+
+export async function getProfileIdentities() {
+  const { error, data } = await auth.getUser()
+
+  if (error) throw error
+
+  const { identities = [], new_email, email_change_sent_at } = data.user
+  return { identities, new_email, email_change_sent_at }
+}
+
+type ProfileIdentitiesData = {
+  identities: UserIdentity[]
+  new_email?: string
+  email_change_sent_at?: string
+}
+type ProfileIdentitiesError = any
+
+export const useProfileIdentitiesQuery = <TData = ProfileIdentitiesData>({
+  enabled = true,
+  ...options
+}: UseQueryOptions<ProfileIdentitiesData, ProfileIdentitiesError, TData> = {}) => {
+  return useQuery<ProfileIdentitiesData, ProfileIdentitiesError, TData>(
+    profileKeys.identities(),
+    () => getProfileIdentities(),
+    { ...options }
+  )
+}
diff --git a/apps/studio/data/profile/profile-unlink-identity-mutation.ts b/apps/studio/data/profile/profile-unlink-identity-mutation.ts
new file mode 100644
index 0000000000000..846be9b2fd235
--- /dev/null
+++ b/apps/studio/data/profile/profile-unlink-identity-mutation.ts
@@ -0,0 +1,44 @@
+import type { UserIdentity } from '@supabase/supabase-js'
+import { useMutation, UseMutationOptions, useQueryClient } from '@tanstack/react-query'
+import { toast } from 'sonner'
+
+import { auth } from 'lib/gotrue'
+import { profileKeys } from './keys'
+
+const unlinkIdentity = async (identity: UserIdentity) => {
+  const { error, data } = await auth.unlinkIdentity(identity)
+
+  if (error) throw error
+  return data
+}
+
+type UnlinkIdentityResponse = any
+type UnlinkIdentityError = any
+
+export const useUnlinkIdentityMutation = ({
+  onSuccess,
+  onError,
+  ...options
+}: Omit<
+  UseMutationOptions<UnlinkIdentityResponse, UnlinkIdentityError, UserIdentity>,
+  'mutationFn'
+> = {}) => {
+  const queryClient = useQueryClient()
+  return useMutation((vars) => unlinkIdentity(vars), {
+    async onSuccess(data, variables, context) {
+      await Promise.all([
+        auth.refreshSession(),
+        queryClient.invalidateQueries(profileKeys.identities()),
+      ])
+      await onSuccess?.(data, variables, context)
+    },
+    async onError(data, variables, context) {
+      if (onError === undefined) {
+        toast.error(`Failed to unlink identity: ${data.message}`)
+      } else {
+        onError(data, variables, context)
+      }
+    },
+    ...options,
+  })
+}
diff --git a/apps/studio/data/profile/profile-update-email-mutation.ts b/apps/studio/data/profile/profile-update-email-mutation.ts
new file mode 100644
index 0000000000000..3319ad4d33a5c
--- /dev/null
+++ b/apps/studio/data/profile/profile-update-email-mutation.ts
@@ -0,0 +1,55 @@
+import { useMutation, UseMutationOptions, useQueryClient } from '@tanstack/react-query'
+import { toast } from 'sonner'
+
+import { handleError, put } from 'data/fetchers'
+import { auth } from 'lib/gotrue'
+import type { ResponseError } from 'types'
+import { profileKeys } from './keys'
+
+export type EmailUpdateVariables = {
+  email: string
+  hcaptchaToken: string | null
+}
+
+export async function updateEmail({ email, hcaptchaToken }: EmailUpdateVariables) {
+  const { data, error } = await put('/platform/update-email', {
+    body: { newEmail: email, hcaptchaToken },
+  })
+
+  if (error) handleError(error)
+  return data
+}
+
+type EmailUpdateData = Awaited<ReturnType<typeof updateEmail>>
+
+export const useEmailUpdateMutation = ({
+  onSuccess,
+  onError,
+  ...options
+}: Omit<
+  UseMutationOptions<EmailUpdateData, ResponseError, EmailUpdateVariables>,
+  'mutationFn'
+> = {}) => {
+  const queryClient = useQueryClient()
+
+  return useMutation<EmailUpdateData, ResponseError, EmailUpdateVariables>(
+    (vars) => updateEmail(vars),
+    {
+      async onSuccess(data, variables, context) {
+        await Promise.all([
+          auth.refreshSession(),
+          queryClient.invalidateQueries(profileKeys.profile()),
+        ])
+        await onSuccess?.(data, variables, context)
+      },
+      async onError(data, variables, context) {
+        if (onError === undefined) {
+          toast.error(`Failed to update email: ${data.message}`)
+        } else {
+          onError(data, variables, context)
+        }
+      },
+      ...options,
+    }
+  )
+}
diff --git a/apps/studio/pages/account/me.tsx b/apps/studio/pages/account/me.tsx
index 695f047387e2d..fb8cf59bda600 100644
--- a/apps/studio/pages/account/me.tsx
+++ b/apps/studio/pages/account/me.tsx
@@ -1,5 +1,6 @@
-import { AccountInformation, AnalyticsSettings } from 'components/interfaces/Account/Preferences'
 import { AccountDeletion } from 'components/interfaces/Account/Preferences/AccountDeletion'
+import { AccountIdentities } from 'components/interfaces/Account/Preferences/AccountIdentities'
+import { AnalyticsSettings } from 'components/interfaces/Account/Preferences/AnalyticsSettings'
 import { ProfileInformation } from 'components/interfaces/Account/Preferences/ProfileInformation'
 import { ThemeSettings } from 'components/interfaces/Account/Preferences/ThemeSettings'
 import AccountLayout from 'components/layouts/AccountLayout/AccountLayout'
@@ -48,7 +49,7 @@ export default User
 
 const ProfileCard = () => {
   const profileUpdateEnabled = useIsFeatureEnabled('profile:update')
-  const { profile, error, isLoading, isError, isSuccess } = useProfile()
+  const { error, isLoading, isError, isSuccess } = useProfile()
 
   return (
     <article>
@@ -68,10 +69,8 @@ const ProfileCard = () => {
       )}
       {isSuccess && (
         <>
-          <section>
-            <AccountInformation profile={profile} />
-          </section>
-          {profileUpdateEnabled && isSuccess ? <ProfileInformation profile={profile!} /> : null}
+          {profileUpdateEnabled && isSuccess ? <ProfileInformation /> : null}
+          <AccountIdentities />
         </>
       )}
 
diff --git a/packages/api-types/types/api.d.ts b/packages/api-types/types/api.d.ts
index 6f9ab1e315906..50e879b80936a 100644
--- a/packages/api-types/types/api.d.ts
+++ b/packages/api-types/types/api.d.ts
@@ -32,6 +32,26 @@ export interface paths {
     patch: operations['v1-update-a-branch-config']
     trace?: never
   }
+  '/v1/branches/{branch_id}/merge': {
+    parameters: {
+      query?: never
+      header?: never
+      path?: never
+      cookie?: never
+    }
+    get?: never
+    put?: never
+    /**
+     * Merges a database branch
+     * @description Merges the specified database branch
+     */
+    post: operations['v1-merge-a-branch']
+    delete?: never
+    options?: never
+    head?: never
+    patch?: never
+    trace?: never
+  }
   '/v1/branches/{branch_id}/push': {
     parameters: {
       query?: never
@@ -433,7 +453,7 @@ export interface paths {
       path?: never
       cookie?: never
     }
-    /** [Alpha] Lists all third-party auth integrations */
+    /** Lists all third-party auth integrations */
     get: operations['listTPAForProject']
     put?: never
     /** Creates a new third-party auth integration */
@@ -451,11 +471,11 @@ export interface paths {
       path?: never
       cookie?: never
     }
-    /** [Alpha] Get a third-party integration */
+    /** Get a third-party integration */
     get: operations['getTPAForProject']
     put?: never
     post?: never
-    /** [Alpha] Removes a third-party auth integration */
+    /** Removes a third-party auth integration */
     delete: operations['deleteTPAForProject']
     options?: never
     head?: never
@@ -487,14 +507,14 @@ export interface paths {
       cookie?: never
     }
     /** Gets project's supavisor config */
-    get: operations['v1-get-supavisor-config']
+    get: operations['getSupavisorConfig']
     put?: never
     post?: never
     delete?: never
     options?: never
     head?: never
     /** Updates project's supavisor config */
-    patch: operations['v1-update-supavisor-config']
+    patch: operations['updateSupavisorConfig']
     trace?: never
   }
   '/v1/projects/{ref}/config/database/postgres': {
@@ -1261,17 +1281,18 @@ export interface components {
       description?: string | null
       hash?: string | null
       id?: string | null
+      /** Format: date-time */
       inserted_at?: string | null
       name: string
       prefix?: string | null
-      secret_jwt_template?: components['schemas']['ApiKeySecretJWTTemplate'] | null
+      secret_jwt_template?: {
+        role: string
+      } | null
       /** @enum {string|null} */
       type?: 'publishable' | 'secret' | 'legacy' | null
+      /** Format: date-time */
       updated_at?: string | null
     }
-    ApiKeySecretJWTTemplate: {
-      role: string
-    }
     ApplyProjectAddonBodyDto: {
       /** @enum {string} */
       addon_type:
@@ -1489,7 +1510,8 @@ export interface components {
     /** @enum {string} */
     BillingPlanId: 'free' | 'pro' | 'team' | 'enterprise'
     BranchDeleteResponse: {
-      message: string
+      /** @enum {string} */
+      message: 'ok'
     }
     BranchDetailResponse: {
       db_host: string
@@ -1546,11 +1568,11 @@ export interface components {
       updated_at: string
     }
     BranchUpdateResponse: {
-      message: string
+      /** @enum {string} */
+      message: 'ok'
       workflow_run_id: string
     }
     BulkUpdateFunctionBody: {
-      /** Format: int64 */
       created_at?: number
       entrypoint_path?: string
       id: string
@@ -1564,7 +1586,20 @@ export interface components {
       version: number
     }
     BulkUpdateFunctionResponse: {
-      functions: components['schemas']['FunctionResponse'][]
+      functions: {
+        created_at: number
+        entrypoint_path?: string
+        id: string
+        import_map?: boolean
+        import_map_path?: string
+        name: string
+        slug: string
+        /** @enum {string} */
+        status: 'ACTIVE' | 'REMOVED' | 'THROTTLED'
+        updated_at: number
+        verify_jwt?: boolean
+        version: number
+      }[]
     }
     CfResponse: {
       errors: Record<string, never>[]
@@ -1574,18 +1609,40 @@ export interface components {
     }
     CreateApiKeyBody: {
       description?: string | null
-      secret_jwt_template?: components['schemas']['ApiKeySecretJWTTemplate'] | null
+      secret_jwt_template?: {
+        role: string
+      } | null
       /** @enum {string} */
       type: 'publishable' | 'secret'
     }
     CreateBranchBody: {
       branch_name: string
-      desired_instance_size?: components['schemas']['DesiredInstanceSize']
+      /** @enum {string} */
+      desired_instance_size?:
+        | 'nano'
+        | 'micro'
+        | 'small'
+        | 'medium'
+        | 'large'
+        | 'xlarge'
+        | '2xlarge'
+        | '4xlarge'
+        | '8xlarge'
+        | '12xlarge'
+        | '16xlarge'
       git_branch?: string
       persistent?: boolean
-      postgres_engine?: components['schemas']['PostgresEngine']
+      /**
+       * @description Postgres engine version. If not provided, the latest version will be used.
+       * @enum {string}
+       */
+      postgres_engine?: '15' | '17-oriole'
       region?: string
-      release_channel?: components['schemas']['ReleaseChannel']
+      /**
+       * @description Release channel. If not provided, GA will be used.
+       * @enum {string}
+       */
+      release_channel?: 'internal' | 'alpha' | 'beta' | 'ga' | 'withdrawn' | 'preview'
     }
     CreateOrganizationV1Dto: {
       name: string
@@ -1636,39 +1693,37 @@ export interface components {
       status: string
       verification_errors?: string[]
     }
-    DatabaseUpgradeStatus: {
-      /** @enum {string} */
-      error?:
-        | '1_upgraded_instance_launch_failed'
-        | '2_volume_detachchment_from_upgraded_instance_failed'
-        | '3_volume_attachment_to_original_instance_failed'
-        | '4_data_upgrade_initiation_failed'
-        | '5_data_upgrade_completion_failed'
-        | '6_volume_detachchment_from_original_instance_failed'
-        | '7_volume_attachment_to_upgraded_instance_failed'
-        | '8_upgrade_completion_failed'
-        | '9_post_physical_backup_failed'
-      initiated_at: string
-      latest_status_at: string
-      /** @enum {string} */
-      progress?:
-        | '0_requested'
-        | '1_started'
-        | '2_launched_upgraded_instance'
-        | '3_detached_volume_from_upgraded_instance'
-        | '4_attached_volume_to_original_instance'
-        | '5_initiated_data_upgrade'
-        | '6_completed_data_upgrade'
-        | '7_detached_volume_from_original_instance'
-        | '8_attached_volume_to_upgraded_instance'
-        | '9_completed_upgrade'
-        | '10_completed_post_physical_backup'
-      /** @enum {integer} */
-      status: 0 | 1 | 2
-      target_version: number
-    }
     DatabaseUpgradeStatusResponse: {
-      databaseUpgradeStatus: components['schemas']['DatabaseUpgradeStatus'] | null
+      databaseUpgradeStatus: {
+        /** @enum {string} */
+        error?:
+          | '1_upgraded_instance_launch_failed'
+          | '2_volume_detachchment_from_upgraded_instance_failed'
+          | '3_volume_attachment_to_original_instance_failed'
+          | '4_data_upgrade_initiation_failed'
+          | '5_data_upgrade_completion_failed'
+          | '6_volume_detachchment_from_original_instance_failed'
+          | '7_volume_attachment_to_upgraded_instance_failed'
+          | '8_upgrade_completion_failed'
+          | '9_post_physical_backup_failed'
+        initiated_at: string
+        latest_status_at: string
+        /** @enum {string} */
+        progress?:
+          | '0_requested'
+          | '1_started'
+          | '2_launched_upgraded_instance'
+          | '3_detached_volume_from_upgraded_instance'
+          | '4_attached_volume_to_original_instance'
+          | '5_initiated_data_upgrade'
+          | '6_completed_data_upgrade'
+          | '7_detached_volume_from_original_instance'
+          | '8_attached_volume_to_upgraded_instance'
+          | '9_completed_upgrade'
+          | '10_completed_post_physical_backup'
+        status: number
+        target_version: number
+      } | null
     }
     DeleteProviderResponse: {
       created_at?: string
@@ -1678,7 +1733,6 @@ export interface components {
       updated_at?: string
     }
     DeployFunctionResponse: {
-      /** Format: int64 */
       created_at?: number
       entrypoint_path?: string
       id: string
@@ -1688,23 +1742,10 @@ export interface components {
       slug: string
       /** @enum {string} */
       status: 'ACTIVE' | 'REMOVED' | 'THROTTLED'
-      /** Format: int64 */
       updated_at?: number
       verify_jwt?: boolean
       version: number
     }
-    /** @enum {string} */
-    DesiredInstanceSize:
-      | 'micro'
-      | 'small'
-      | 'medium'
-      | 'large'
-      | 'xlarge'
-      | '2xlarge'
-      | '4xlarge'
-      | '8xlarge'
-      | '12xlarge'
-      | '16xlarge'
     Domain: {
       created_at?: string
       domain?: string
@@ -1712,18 +1753,16 @@ export interface components {
       updated_at?: string
     }
     FunctionDeployBody: {
-      file: string[]
-      metadata: components['schemas']['FunctionDeployMetadata']
-    }
-    FunctionDeployMetadata: {
-      entrypoint_path: string
-      import_map_path?: string
-      name?: string
-      static_patterns?: string[]
-      verify_jwt?: boolean
+      file?: string[]
+      metadata: {
+        entrypoint_path: string
+        import_map_path?: string
+        name?: string
+        static_patterns?: string[]
+        verify_jwt?: boolean
+      }
     }
     FunctionResponse: {
-      /** Format: int64 */
       created_at: number
       entrypoint_path?: string
       id: string
@@ -1733,13 +1772,11 @@ export interface components {
       slug: string
       /** @enum {string} */
       status: 'ACTIVE' | 'REMOVED' | 'THROTTLED'
-      /** Format: int64 */
       updated_at: number
       verify_jwt?: boolean
       version: number
     }
     FunctionSlugResponse: {
-      /** Format: int64 */
       created_at: number
       entrypoint_path?: string
       id: string
@@ -1749,7 +1786,6 @@ export interface components {
       slug: string
       /** @enum {string} */
       status: 'ACTIVE' | 'REMOVED' | 'THROTTLED'
-      /** Format: int64 */
       updated_at: number
       verify_jwt?: boolean
       version: number
@@ -1878,10 +1914,18 @@ export interface components {
       dbAllowedCidrsV6?: string[]
     }
     NetworkRestrictionsResponse: {
-      config: components['schemas']['NetworkRestrictionsRequest']
+      /** @description At any given point in time, this is the config that the user has requested be applied to their project. The `status` field indicates if it has been applied to the project, or is pending. When an updated config is received, the applied config is moved to `old_config`. */
+      config: {
+        dbAllowedCidrs?: string[]
+        dbAllowedCidrsV6?: string[]
+      }
       /** @enum {string} */
       entitlement: 'disallowed' | 'allowed'
-      old_config?: components['schemas']['NetworkRestrictionsRequest']
+      /** @description Populated when a new config has been received, but not registered as successfully applied to a project. */
+      old_config?: {
+        dbAllowedCidrs?: string[]
+        dbAllowedCidrsV6?: string[]
+      }
       /** @enum {string} */
       status: 'stored' | 'applied'
     }
@@ -1892,8 +1936,9 @@ export interface components {
       refresh_token: string
     }
     OAuthTokenBody: {
-      client_id: string
-      client_secret: string
+      /** Format: uuid */
+      client_id?: string
+      client_secret?: string
       code?: string
       code_verifier?: string
       /** @enum {string} */
@@ -1903,7 +1948,6 @@ export interface components {
     }
     OAuthTokenResponse: {
       access_token: string
-      /** Format: int64 */
       expires_in: number
       refresh_token: string
       /** @enum {string} */
@@ -1947,11 +1991,6 @@ export interface components {
       wal_sender_timeout?: string
       work_mem?: string
     }
-    /**
-     * @description Postgres engine version. If not provided, the latest version will be used.
-     * @enum {string}
-     */
-    PostgresEngine: '15' | '17-oriole'
     PostgrestConfigWithJWTSecretResponse: {
       db_extra_search_path: string
       /** @description If `null`, the value is automatically configured based on compute size. */
@@ -1969,23 +2008,31 @@ export interface components {
     }
     ProjectUpgradeEligibilityResponse: {
       current_app_version: string
-      current_app_version_release_channel: components['schemas']['ReleaseChannel']
+      /** @enum {string} */
+      current_app_version_release_channel:
+        | 'internal'
+        | 'alpha'
+        | 'beta'
+        | 'ga'
+        | 'withdrawn'
+        | 'preview'
       duration_estimate_hours: number
       eligible: boolean
       extension_dependent_objects: string[]
       latest_app_version: string
       legacy_auth_custom_roles: string[]
       potential_breaking_changes: string[]
-      target_upgrade_versions: components['schemas']['ProjectVersion'][]
+      target_upgrade_versions: {
+        app_version: string
+        /** @enum {string} */
+        postgres_version: '15' | '17-oriole'
+        /** @enum {string} */
+        release_channel: 'internal' | 'alpha' | 'beta' | 'ga' | 'withdrawn' | 'preview'
+      }[]
     }
     ProjectUpgradeInitiateResponse: {
       tracking_id: string
     }
-    ProjectVersion: {
-      app_version: string
-      postgres_version: components['schemas']['PostgresEngine']
-      release_channel: components['schemas']['ReleaseChannel']
-    }
     Provider: {
       created_at?: string
       domains?: components['schemas']['Domain'][]
@@ -2125,14 +2172,15 @@ export interface components {
       username: string
     }
     SslEnforcementRequest: {
-      requestedConfig: components['schemas']['SslEnforcements']
+      requestedConfig: {
+        database: boolean
+      }
     }
     SslEnforcementResponse: {
       appliedSuccessfully: boolean
-      currentConfig: components['schemas']['SslEnforcements']
-    }
-    SslEnforcements: {
-      database: boolean
+      currentConfig: {
+        database: boolean
+      }
     }
     SslValidation: {
       status: string
@@ -2159,6 +2207,10 @@ export interface components {
     }
     SupavisorConfigResponse: {
       connection_string: string
+      /**
+       * @deprecated
+       * @description Use connection_string instead
+       */
       connectionString: string
       /** @enum {string} */
       database_type: 'PRIMARY' | 'READ_REPLICA'
@@ -2189,7 +2241,9 @@ export interface components {
     }
     UpdateApiKeyBody: {
       description?: string | null
-      secret_jwt_template?: components['schemas']['ApiKeySecretJWTTemplate'] | null
+      secret_jwt_template?: {
+        role: string
+      } | null
     }
     UpdateAuthConfigBody: {
       api_max_request_duration?: number
@@ -2460,6 +2514,7 @@ export interface components {
     UpdateSupavisorConfigBody: {
       default_pool_size?: number | null
       /**
+       * @deprecated
        * @description Dedicated pooler mode for the project
        * @enum {string}
        */
@@ -2470,7 +2525,8 @@ export interface components {
       pool_mode: string
     }
     UpgradeDatabaseBody: {
-      release_channel: components['schemas']['ReleaseChannel']
+      /** @enum {string} */
+      release_channel?: 'internal' | 'alpha' | 'beta' | 'ga' | 'withdrawn' | 'preview'
       target_version: string
     }
     V1AnalyticsResponse: {
@@ -2751,7 +2807,6 @@ export interface operations {
       query?: never
       header?: never
       path: {
-        /** @description Branch ID */
         branch_id: string
       }
       cookie?: never
@@ -2780,7 +2835,6 @@ export interface operations {
       query?: never
       header?: never
       path: {
-        /** @description Branch ID */
         branch_id: string
       }
       cookie?: never
@@ -2809,7 +2863,6 @@ export interface operations {
       query?: never
       header?: never
       path: {
-        /** @description Branch ID */
         branch_id: string
       }
       cookie?: never
@@ -2837,12 +2890,39 @@ export interface operations {
       }
     }
   }
+  'v1-merge-a-branch': {
+    parameters: {
+      query?: never
+      header?: never
+      path: {
+        branch_id: string
+      }
+      cookie?: never
+    }
+    requestBody?: never
+    responses: {
+      201: {
+        headers: {
+          [name: string]: unknown
+        }
+        content: {
+          'application/json': components['schemas']['BranchUpdateResponse']
+        }
+      }
+      /** @description Failed to merge database branch */
+      500: {
+        headers: {
+          [name: string]: unknown
+        }
+        content?: never
+      }
+    }
+  }
   'v1-push-a-branch': {
     parameters: {
       query?: never
       header?: never
       path: {
-        /** @description Branch ID */
         branch_id: string
       }
       cookie?: never
@@ -2871,7 +2951,6 @@ export interface operations {
       query?: never
       header?: never
       path: {
-        /** @description Branch ID */
         branch_id: string
       }
       cookie?: never
@@ -2902,9 +2981,7 @@ export interface operations {
         code_challenge?: string
         code_challenge_method?: 'plain' | 'sha256' | 'S256'
         redirect_uri: string
-        response_mode?: string
-        response_type: 'code' | 'token' | 'id_token token'
-        scope?: string
+        response_type: string
         state?: string
       }
       header?: never
@@ -3040,6 +3117,12 @@ export interface operations {
           'application/json': components['schemas']['V1OrganizationSlugResponse']
         }
       }
+      403: {
+        headers: {
+          [name: string]: unknown
+        }
+        content?: never
+      }
     }
   }
   'v1-list-organization-members': {
@@ -3061,6 +3144,12 @@ export interface operations {
           'application/json': components['schemas']['V1OrganizationMemberResponse'][]
         }
       }
+      403: {
+        headers: {
+          [name: string]: unknown
+        }
+        content?: never
+      }
     }
   }
   'v1-list-all-projects': {
@@ -3125,6 +3214,12 @@ export interface operations {
           'application/json': components['schemas']['V1ProjectWithDatabaseResponse']
         }
       }
+      403: {
+        headers: {
+          [name: string]: unknown
+        }
+        content?: never
+      }
       /** @description Failed to retrieve project */
       500: {
         headers: {
@@ -3195,12 +3290,11 @@ export interface operations {
   }
   'v1-get-project-api-keys': {
     parameters: {
-      query: {
-        reveal: boolean
+      query?: {
+        reveal?: boolean
       }
       header?: never
       path: {
-        /** @description Project ref */
         ref: string
       }
       cookie?: never
@@ -3215,16 +3309,21 @@ export interface operations {
           'application/json': components['schemas']['ApiKeyResponse'][]
         }
       }
+      403: {
+        headers: {
+          [name: string]: unknown
+        }
+        content?: never
+      }
     }
   }
   createApiKey: {
     parameters: {
-      query: {
-        reveal: boolean
+      query?: {
+        reveal?: boolean
       }
       header?: never
       path: {
-        /** @description Project ref */
         ref: string
       }
       cookie?: never
@@ -3243,17 +3342,22 @@ export interface operations {
           'application/json': components['schemas']['ApiKeyResponse']
         }
       }
+      403: {
+        headers: {
+          [name: string]: unknown
+        }
+        content?: never
+      }
     }
   }
   getApiKey: {
     parameters: {
-      query: {
-        reveal: boolean
+      query?: {
+        reveal?: boolean
       }
       header?: never
       path: {
         id: string
-        /** @description Project ref */
         ref: string
       }
       cookie?: never
@@ -3268,17 +3372,22 @@ export interface operations {
           'application/json': components['schemas']['ApiKeyResponse']
         }
       }
+      403: {
+        headers: {
+          [name: string]: unknown
+        }
+        content?: never
+      }
     }
   }
   deleteApiKey: {
     parameters: {
-      query: {
-        reveal: boolean
+      query?: {
+        reveal?: boolean
       }
       header?: never
       path: {
         id: string
-        /** @description Project ref */
         ref: string
       }
       cookie?: never
@@ -3303,13 +3412,12 @@ export interface operations {
   }
   updateApiKey: {
     parameters: {
-      query: {
-        reveal: boolean
+      query?: {
+        reveal?: boolean
       }
       header?: never
       path: {
         id: string
-        /** @description Project ref */
         ref: string
       }
       cookie?: never
@@ -3328,6 +3436,12 @@ export interface operations {
           'application/json': components['schemas']['ApiKeyResponse']
         }
       }
+      403: {
+        headers: {
+          [name: string]: unknown
+        }
+        content?: never
+      }
     }
   }
   'v1-list-project-addons': {
@@ -3453,7 +3567,6 @@ export interface operations {
       query?: never
       header?: never
       path: {
-        /** @description Project ref */
         ref: string
       }
       cookie?: never
@@ -3468,6 +3581,12 @@ export interface operations {
           'application/json': components['schemas']['BranchResponse'][]
         }
       }
+      403: {
+        headers: {
+          [name: string]: unknown
+        }
+        content?: never
+      }
       /** @description Failed to retrieve database branches */
       500: {
         headers: {
@@ -3482,7 +3601,6 @@ export interface operations {
       query?: never
       header?: never
       path: {
-        /** @description Project ref */
         ref: string
       }
       cookie?: never
@@ -3501,6 +3619,12 @@ export interface operations {
           'application/json': components['schemas']['BranchResponse']
         }
       }
+      403: {
+        headers: {
+          [name: string]: unknown
+        }
+        content?: never
+      }
       /** @description Failed to create database branch */
       500: {
         headers: {
@@ -3515,7 +3639,6 @@ export interface operations {
       query?: never
       header?: never
       path: {
-        /** @description Project ref */
         ref: string
       }
       cookie?: never
@@ -3528,6 +3651,12 @@ export interface operations {
         }
         content?: never
       }
+      403: {
+        headers: {
+          [name: string]: unknown
+        }
+        content?: never
+      }
       /** @description Failed to disable preview branching */
       500: {
         headers: {
@@ -3690,6 +3819,12 @@ export interface operations {
           'application/json': components['schemas']['SigningKeyResponse']
         }
       }
+      403: {
+        headers: {
+          [name: string]: unknown
+        }
+        content?: never
+      }
     }
   }
   deleteSigningKey: {
@@ -4076,6 +4211,12 @@ export interface operations {
           'application/json': components['schemas']['V1PgbouncerConfigResponse']
         }
       }
+      403: {
+        headers: {
+          [name: string]: unknown
+        }
+        content?: never
+      }
       /** @description Failed to retrieve project's pgbouncer config */
       500: {
         headers: {
@@ -4085,7 +4226,7 @@ export interface operations {
       }
     }
   }
-  'v1-get-supavisor-config': {
+  getSupavisorConfig: {
     parameters: {
       query?: never
       header?: never
@@ -4114,7 +4255,7 @@ export interface operations {
       }
     }
   }
-  'v1-update-supavisor-config': {
+  updateSupavisorConfig: {
     parameters: {
       query?: never
       header?: never
@@ -4173,6 +4314,12 @@ export interface operations {
           'application/json': components['schemas']['PostgresConfigResponse']
         }
       }
+      403: {
+        headers: {
+          [name: string]: unknown
+        }
+        content?: never
+      }
       /** @description Failed to retrieve project's Postgres config */
       500: {
         headers: {
@@ -4490,6 +4637,12 @@ export interface operations {
           'application/json': components['schemas']['V1BackupsResponse']
         }
       }
+      403: {
+        headers: {
+          [name: string]: unknown
+        }
+        content?: never
+      }
       /** @description Failed to get backups */
       500: {
         headers: {
@@ -4504,7 +4657,6 @@ export interface operations {
       query?: never
       header?: never
       path: {
-        /** @description Project ref */
         ref: string
       }
       cookie?: never
@@ -4521,6 +4673,12 @@ export interface operations {
         }
         content?: never
       }
+      403: {
+        headers: {
+          [name: string]: unknown
+        }
+        content?: never
+      }
     }
   }
   getDatabaseMetadata: {
@@ -4555,7 +4713,6 @@ export interface operations {
       query?: never
       header?: never
       path: {
-        /** @description Project ref */
         ref: string
       }
       cookie?: never
@@ -4627,7 +4784,6 @@ export interface operations {
       query?: never
       header?: never
       path: {
-        /** @description Project ref */
         ref: string
       }
       cookie?: never
@@ -4662,7 +4818,6 @@ export interface operations {
       query?: never
       header?: never
       path: {
-        /** @description Project ref */
         ref: string
       }
       cookie?: never
@@ -4708,7 +4863,6 @@ export interface operations {
       }
       header?: never
       path: {
-        /** @description Project ref */
         ref: string
       }
       cookie?: never
@@ -4748,9 +4902,7 @@ export interface operations {
       query?: never
       header?: never
       path: {
-        /** @description Function slug */
         function_slug: string
-        /** @description Project ref */
         ref: string
       }
       cookie?: never
@@ -4785,9 +4937,7 @@ export interface operations {
       query?: never
       header?: never
       path: {
-        /** @description Function slug */
         function_slug: string
-        /** @description Project ref */
         ref: string
       }
       cookie?: never
@@ -4822,14 +4972,11 @@ export interface operations {
         import_map?: boolean
         import_map_path?: string
         name?: string
-        slug?: string
         verify_jwt?: boolean
       }
       header?: never
       path: {
-        /** @description Function slug */
         function_slug: string
-        /** @description Project ref */
         ref: string
       }
       cookie?: never
@@ -4869,9 +5016,7 @@ export interface operations {
       query?: never
       header?: never
       path: {
-        /** @description Function slug */
         function_slug: string
-        /** @description Project ref */
         ref: string
       }
       cookie?: never
@@ -4907,7 +5052,6 @@ export interface operations {
       }
       header?: never
       path: {
-        /** @description Project ref */
         ref: string
       }
       cookie?: never
@@ -4984,7 +5128,6 @@ export interface operations {
       query?: never
       header?: never
       path: {
-        /** @description Project ref */
         ref: string
       }
       cookie?: never
@@ -5021,7 +5164,6 @@ export interface operations {
       query?: never
       header?: never
       path: {
-        /** @description Project ref */
         ref: string
       }
       cookie?: never
@@ -5056,7 +5198,6 @@ export interface operations {
       query?: never
       header?: never
       path: {
-        /** @description Project ref */
         ref: string
       }
       cookie?: never
@@ -5091,7 +5232,6 @@ export interface operations {
       query?: never
       header?: never
       path: {
-        /** @description Project ref */
         ref: string
       }
       cookie?: never
@@ -5156,7 +5296,6 @@ export interface operations {
       query?: never
       header?: never
       path: {
-        /** @description Project ref */
         ref: string
       }
       cookie?: never
@@ -5191,7 +5330,6 @@ export interface operations {
       query?: never
       header?: never
       path: {
-        /** @description Project ref */
         ref: string
       }
       cookie?: never
@@ -5304,7 +5442,6 @@ export interface operations {
       query?: never
       header?: never
       path: {
-        /** @description Project ref */
         ref: string
       }
       cookie?: never
@@ -5341,7 +5478,6 @@ export interface operations {
       query?: never
       header?: never
       path: {
-        /** @description Project ref */
         ref: string
       }
       cookie?: never
@@ -5393,6 +5529,12 @@ export interface operations {
           'application/json': components['schemas']['ReadOnlyStatusResponse']
         }
       }
+      403: {
+        headers: {
+          [name: string]: unknown
+        }
+        content?: never
+      }
       /** @description Failed to get project readonly mode status */
       500: {
         headers: {
@@ -5420,6 +5562,12 @@ export interface operations {
         }
         content?: never
       }
+      403: {
+        headers: {
+          [name: string]: unknown
+        }
+        content?: never
+      }
       /** @description Failed to disable project's readonly mode */
       500: {
         headers: {
@@ -5515,7 +5663,6 @@ export interface operations {
       query?: never
       header?: never
       path: {
-        /** @description Project ref */
         ref: string
       }
       cookie?: never
@@ -5550,7 +5697,6 @@ export interface operations {
       query?: never
       header?: never
       path: {
-        /** @description Project ref */
         ref: string
       }
       cookie?: never
@@ -5587,7 +5733,6 @@ export interface operations {
       query?: never
       header?: never
       path: {
-        /** @description Project ref */
         ref: string
       }
       cookie?: never
@@ -5626,7 +5771,6 @@ export interface operations {
       query?: never
       header?: never
       path: {
-        /** @description Project ref */
         ref: string
       }
       cookie?: never
@@ -5661,7 +5805,6 @@ export interface operations {
       query?: never
       header?: never
       path: {
-        /** @description Project ref */
         ref: string
       }
       cookie?: never
@@ -5737,7 +5880,6 @@ export interface operations {
       }
       header?: never
       path: {
-        /** @description Project ref */
         ref: string
       }
       cookie?: never
@@ -5772,7 +5914,6 @@ export interface operations {
       query?: never
       header?: never
       path: {
-        /** @description Project ref */
         ref: string
       }
       cookie?: never
@@ -5811,7 +5952,6 @@ export interface operations {
       query?: never
       header?: never
       path: {
-        /** @description Project ref */
         ref: string
       }
       cookie?: never
@@ -5848,7 +5988,6 @@ export interface operations {
       }
       header?: never
       path: {
-        /** @description Project ref */
         ref: string
       }
       cookie?: never
@@ -5883,7 +6022,6 @@ export interface operations {
       query?: never
       header?: never
       path: {
-        /** @description Project ref */
         ref: string
       }
       cookie?: never
@@ -5918,7 +6056,6 @@ export interface operations {
       query?: never
       header?: never
       path: {
-        /** @description Project ref */
         ref: string
       }
       cookie?: never
@@ -5951,7 +6088,6 @@ export interface operations {
       query?: never
       header?: never
       path: {
-        /** @description Project ref */
         ref: string
       }
       cookie?: never
@@ -5990,7 +6126,6 @@ export interface operations {
       query?: never
       header?: never
       path: {
-        /** @description Project ref */
         ref: string
       }
       cookie?: never
diff --git a/packages/api-types/types/platform.d.ts b/packages/api-types/types/platform.d.ts
index 660edc672bf1e..955aa4573aa53 100644
--- a/packages/api-types/types/platform.d.ts
+++ b/packages/api-types/types/platform.d.ts
@@ -1034,7 +1034,7 @@ export interface paths {
     /** Gets the Billing customer */
     get: operations['CustomerController_getCustomer']
     /** Updates the billing customer */
-    put: operations['updateCustomerV2']
+    put: operations['CustomerController_updateCustomer']
     post?: never
     delete?: never
     options?: never
@@ -1312,7 +1312,7 @@ export interface paths {
       cookie?: never
     }
     /** Gets Stripe payment methods for the given organization */
-    get: operations['getPaymentMethodsV2']
+    get: operations['PaymentsController_getPaymentMethods']
     put?: never
     post?: never
     /** Detach payment method with the given card ID */
@@ -2104,10 +2104,10 @@ export interface paths {
       cookie?: never
     }
     /** Lists project's warehouse access tokens from logflare */
-    get: operations['v1-list-all-warehouse-tokens']
+    get: operations['AccessTokenController_listAccessTokens']
     put?: never
     /** Create a warehouse access token */
-    post: operations['v1-create-a-warehouse-token']
+    post: operations['AccessTokenController_createAccessToken']
     delete?: never
     options?: never
     head?: never
@@ -2125,7 +2125,7 @@ export interface paths {
     put?: never
     post?: never
     /** Delete a warehouse access token */
-    delete: operations['v1-delete-a-warehouse-token']
+    delete: operations['AccessTokenController_deleteAccessToken']
     options?: never
     head?: never
     patch?: never
@@ -2139,10 +2139,10 @@ export interface paths {
       cookie?: never
     }
     /** Lists project's telemetry collections from logflare */
-    get: operations['v1-list-all-telemetry-collections']
+    get: operations['CollectionController_listCollections']
     put?: never
     /** Create a telemetry collection */
-    post: operations['v1-create-a-telemetry-collection']
+    post: operations['CollectionController_createCollection']
     delete?: never
     options?: never
     head?: never
@@ -2157,15 +2157,15 @@ export interface paths {
       cookie?: never
     }
     /** Get a telemetry collection */
-    get: operations['v1-get-a-telemetry-collection']
+    get: operations['CollectionController_getCollectionSchema']
     put?: never
     post?: never
     /** Delete a telemetry collection */
-    delete: operations['v1-delete-a-telemetry-collection']
+    delete: operations['CollectionController_deleteCollection']
     options?: never
     head?: never
     /** Update a telemetry collection */
-    patch: operations['v1-update-a-telemetry-collection']
+    patch: operations['CollectionController_updateCollection']
     trace?: never
   }
   '/platform/projects/{ref}/analytics/warehouse/collections/{token}/schema': {
@@ -2176,7 +2176,7 @@ export interface paths {
       cookie?: never
     }
     /** Get a telemetry collection schema */
-    get: operations['v1-get-a-telemetry-collection-schema']
+    get: operations['CollectionController_getCollection']
     put?: never
     post?: never
     delete?: never
@@ -2193,10 +2193,10 @@ export interface paths {
       cookie?: never
     }
     /** Lists project's warehouse endpoints from logflare */
-    get: operations['v1-list-all-warehouse-endpoints']
+    get: operations['EndpointController_listEndpoints']
     put?: never
     /** Create a warehouse endpoint */
-    post: operations['v1-create-a-warehouse-endpoint']
+    post: operations['EndpointController_createEndpoint']
     delete?: never
     options?: never
     head?: never
@@ -2212,10 +2212,10 @@ export interface paths {
     }
     get?: never
     /** Update a warehouse endpoint */
-    put: operations['v1-update-a-warehouse-endpoint']
+    put: operations['EndpointController_updateEndpoint']
     post?: never
     /** Delete a warehouse endpoint */
-    delete: operations['v1-delete-a-warehouse-endpoint']
+    delete: operations['EndpointController_deleteEndpoint']
     options?: never
     head?: never
     patch?: never
@@ -2229,7 +2229,7 @@ export interface paths {
       cookie?: never
     }
     /** Lists project's warehouse queries from logflare */
-    get: operations['v1-list-all-warehouse-queries']
+    get: operations['WarehouseQueryController_runQuery']
     put?: never
     post?: never
     delete?: never
@@ -2246,7 +2246,7 @@ export interface paths {
       cookie?: never
     }
     /** Parses a warehouse query */
-    get: operations['v1-parse-warehouse-query']
+    get: operations['WarehouseQueryController_parseQuery']
     put?: never
     post?: never
     delete?: never
@@ -2263,7 +2263,7 @@ export interface paths {
       cookie?: never
     }
     /** Gets project's warehouse tenant from logflare */
-    get: operations['v1-provision-a-warehouse-tenant']
+    get: operations['TenantController_getTenant']
     put?: never
     post?: never
     delete?: never
@@ -2384,9 +2384,9 @@ export interface paths {
       cookie?: never
     }
     /** Gets project's Postgres config */
-    get: operations['v1-get-postgres-config']
+    get: operations['PostgresConfigController_getConfig']
     /** Updates project's Postgres config */
-    put: operations['v1-update-postgres-config']
+    put: operations['PostgresConfigController_updateConfig']
     post?: never
     delete?: never
     options?: never
@@ -2473,14 +2473,14 @@ export interface paths {
       cookie?: never
     }
     /** Gets project's supavisor config */
-    get: operations['v1-get-supavisor-config']
+    get: operations['SupavisorConfigController_getSupavisorConfig']
     put?: never
     post?: never
     delete?: never
     options?: never
     head?: never
     /** Updates project's supavisor config */
-    patch: operations['v1-update-supavisor-config']
+    patch: operations['SupavisorConfigController_updateSupavisorConfig']
     trace?: never
   }
   '/platform/projects/{ref}/content': {
@@ -4335,7 +4335,7 @@ export interface components {
         lint_category: string | null
         lint_name: string | null
         note: string | null
-        project_id: number
+        project_ref: string
       }[]
     }
     CreateOAuthAppBody: {
@@ -5479,7 +5479,7 @@ export interface components {
         lint_category: string | null
         lint_name: string | null
         note: string | null
-        project_id: number
+        project_ref: string
       }[]
     }
     LoadBalancerDatabase: {
@@ -5771,23 +5771,21 @@ export interface components {
     }
     PgbouncerConfigResponse: {
       connection_string: string
-      connectionString: string
       db_dns_name: string
       db_host: string
       db_name: string
       db_port: number
       db_user: string
       default_pool_size?: number
-      ignore_startup_parameters?: string
+      ignore_startup_parameters: string
       inserted_at: string
       max_client_conn?: number
       pgbouncer_enabled: boolean
       /** @enum {string} */
-      pgbouncer_status: 'COMING_DOWN' | 'COMING_UP' | 'DISABLED' | 'ENABLED' | 'RELOADING'
+      pgbouncer_status: 'COMING_UP' | 'COMING_DOWN' | 'RELOADING' | 'ENABLED' | 'DISABLED'
       /** @enum {string} */
-      pool_mode?: 'transaction' | 'session' | 'statement'
+      pool_mode: 'transaction' | 'session' | 'statement'
       ssl_enforced: boolean
-      supavisor_enabled: boolean
     }
     PgbouncerStatusResponse: {
       active: boolean
@@ -6652,6 +6650,10 @@ export interface components {
     }
     SupavisorConfigResponse: {
       connection_string: string
+      /**
+       * @deprecated
+       * @description Use connection_string instead
+       */
       connectionString: string
       /** @enum {string} */
       database_type: 'PRIMARY' | 'READ_REPLICA'
@@ -7090,10 +7092,10 @@ export interface components {
     UpdatePgbouncerConfigBody: {
       default_pool_size?: number
       ignore_startup_parameters: string
-      max_client_conn?: number | null
-      pgbouncer_enabled: boolean
+      max_client_conn?: number
+      pgbouncer_enabled?: boolean
       /** @enum {string} */
-      pool_mode: 'transaction' | 'session' | 'statement'
+      pool_mode?: 'transaction' | 'session' | 'statement'
     }
     UpdatePolicyBody: {
       check?: string
@@ -7105,12 +7107,16 @@ export interface components {
     UpdatePoolingConfigResponse: {
       default_pool_size?: number
       ignore_startup_parameters: string
-      max_client_conn?: number | null
+      max_client_conn?: number
       pgbouncer_enabled: boolean
       /** @enum {string} */
-      pgbouncer_status: 'COMING_DOWN' | 'COMING_UP' | 'DISABLED' | 'ENABLED' | 'RELOADING'
-      /** @enum {string} */
-      pool_mode: 'transaction' | 'session' | 'statement'
+      pgbouncer_status: 'COMING_UP' | 'COMING_DOWN' | 'RELOADING' | 'ENABLED' | 'DISABLED'
+      /**
+       * @deprecated
+       * @description Cannot be changed, this is ignored
+       * @enum {string}
+       */
+      pool_mode?: 'transaction' | 'session' | 'statement'
     }
     UpdatePostgresConfigBody: {
       effective_cache_size?: string
@@ -7234,6 +7240,7 @@ export interface components {
     UpdateSupavisorConfigBody: {
       default_pool_size?: number | null
       /**
+       * @deprecated
        * @description Dedicated pooler mode for the project
        * @enum {string}
        */
@@ -9464,7 +9471,6 @@ export interface operations {
       }
       header?: never
       path: {
-        /** @description Organization slug */
         slug: string
       }
       cookie?: never
@@ -9499,7 +9505,6 @@ export interface operations {
       query?: never
       header?: never
       path: {
-        /** @description Organization slug */
         slug: string
       }
       cookie?: never
@@ -9535,7 +9540,6 @@ export interface operations {
       header?: never
       path: {
         invoiceId: string
-        /** @description Organization slug */
         slug: string
       }
       cookie?: never
@@ -9605,7 +9609,6 @@ export interface operations {
       query?: never
       header?: never
       path: {
-        /** @description Organization slug */
         slug: string
       }
       cookie?: never
@@ -9808,7 +9811,7 @@ export interface operations {
       }
     }
   }
-  updateCustomerV2: {
+  CustomerController_updateCustomer: {
     parameters: {
       query?: never
       header?: never
@@ -10526,7 +10529,7 @@ export interface operations {
       }
     }
   }
-  getPaymentMethodsV2: {
+  PaymentsController_getPaymentMethods: {
     parameters: {
       query?: never
       header?: never
@@ -13478,7 +13481,7 @@ export interface operations {
       }
     }
   }
-  'v1-list-all-warehouse-tokens': {
+  AccessTokenController_listAccessTokens: {
     parameters: {
       query?: never
       header?: never
@@ -13513,7 +13516,7 @@ export interface operations {
       }
     }
   }
-  'v1-create-a-warehouse-token': {
+  AccessTokenController_createAccessToken: {
     parameters: {
       query?: never
       header?: never
@@ -13545,7 +13548,7 @@ export interface operations {
       }
     }
   }
-  'v1-delete-a-warehouse-token': {
+  AccessTokenController_deleteAccessToken: {
     parameters: {
       query?: never
       header?: never
@@ -13575,7 +13578,7 @@ export interface operations {
       }
     }
   }
-  'v1-list-all-telemetry-collections': {
+  CollectionController_listCollections: {
     parameters: {
       query?: never
       header?: never
@@ -13610,7 +13613,7 @@ export interface operations {
       }
     }
   }
-  'v1-create-a-telemetry-collection': {
+  CollectionController_createCollection: {
     parameters: {
       query?: never
       header?: never
@@ -13642,7 +13645,7 @@ export interface operations {
       }
     }
   }
-  'v1-get-a-telemetry-collection': {
+  CollectionController_getCollectionSchema: {
     parameters: {
       query?: never
       header?: never
@@ -13674,7 +13677,7 @@ export interface operations {
       }
     }
   }
-  'v1-delete-a-telemetry-collection': {
+  CollectionController_deleteCollection: {
     parameters: {
       query?: never
       header?: never
@@ -13706,7 +13709,7 @@ export interface operations {
       }
     }
   }
-  'v1-update-a-telemetry-collection': {
+  CollectionController_updateCollection: {
     parameters: {
       query?: never
       header?: never
@@ -13738,7 +13741,7 @@ export interface operations {
       }
     }
   }
-  'v1-get-a-telemetry-collection-schema': {
+  CollectionController_getCollection: {
     parameters: {
       query?: never
       header?: never
@@ -13770,7 +13773,7 @@ export interface operations {
       }
     }
   }
-  'v1-list-all-warehouse-endpoints': {
+  EndpointController_listEndpoints: {
     parameters: {
       query?: never
       header?: never
@@ -13805,7 +13808,7 @@ export interface operations {
       }
     }
   }
-  'v1-create-a-warehouse-endpoint': {
+  EndpointController_createEndpoint: {
     parameters: {
       query?: never
       header?: never
@@ -13837,7 +13840,7 @@ export interface operations {
       }
     }
   }
-  'v1-update-a-warehouse-endpoint': {
+  EndpointController_updateEndpoint: {
     parameters: {
       query?: never
       header?: never
@@ -13869,7 +13872,7 @@ export interface operations {
       }
     }
   }
-  'v1-delete-a-warehouse-endpoint': {
+  EndpointController_deleteEndpoint: {
     parameters: {
       query?: never
       header?: never
@@ -13899,7 +13902,7 @@ export interface operations {
       }
     }
   }
-  'v1-list-all-warehouse-queries': {
+  WarehouseQueryController_runQuery: {
     parameters: {
       query?: never
       header?: never
@@ -13932,7 +13935,7 @@ export interface operations {
       }
     }
   }
-  'v1-parse-warehouse-query': {
+  WarehouseQueryController_parseQuery: {
     parameters: {
       query?: never
       header?: never
@@ -13965,7 +13968,7 @@ export interface operations {
       }
     }
   }
-  'v1-provision-a-warehouse-tenant': {
+  TenantController_getTenant: {
     parameters: {
       query?: never
       header?: never
@@ -14279,7 +14282,7 @@ export interface operations {
       }
     }
   }
-  'v1-get-postgres-config': {
+  PostgresConfigController_getConfig: {
     parameters: {
       query?: never
       header?: never
@@ -14308,7 +14311,7 @@ export interface operations {
       }
     }
   }
-  'v1-update-postgres-config': {
+  PostgresConfigController_updateConfig: {
     parameters: {
       query?: never
       header?: never
@@ -14591,7 +14594,7 @@ export interface operations {
       }
     }
   }
-  'v1-get-supavisor-config': {
+  SupavisorConfigController_getSupavisorConfig: {
     parameters: {
       query?: never
       header?: never
@@ -14620,7 +14623,7 @@ export interface operations {
       }
     }
   }
-  'v1-update-supavisor-config': {
+  SupavisorConfigController_updateSupavisorConfig: {
     parameters: {
       query?: never
       header?: never


Reviews:
user: login: kangmingtay
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-04T09:22:48Z
user: login: kangmingtay
body: @joshenlim is it also possible to show the new email as "pending" after initiating the update? 
<img width="1650" alt="image" src="https://github.com/user-attachments/assets/8b0e0904-c40e-4613-8200-f14dca237f97" />

state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-06T21:19:15Z
user: login: SimonGodefroid
state: COMMENTED
author_association: NONE
submitted_at: 2025-03-18T06:32:27Z
user: login: SimonGodefroid
state: COMMENTED
author_association: NONE
submitted_at: 2025-03-18T06:33:40Z
user: login: alaister
body: small light mode issue:

<img width="389" alt="Screenshot 2025-03-20 at 12 27 15" src="https://github.com/user-attachments/assets/47a85c9e-b333-40f5-b166-a2d0440ff9b4" />

state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-20T11:27:31Z
user: login: alaister
body: LGTM!

- Tested as per instructions

A couple of small nits:
state: APPROVED
author_association: MEMBER
submitted_at: 2025-03-20T16:49:47Z
user: login: staaldraad
state: APPROVED
author_association: MEMBER
submitted_at: 2025-03-21T07:51:31Z

Review comments:
diff_hunk: @@ -0,0 +1,133 @@
+import HCaptcha from '@hcaptcha/react-hcaptcha'
+import { zodResolver } from '@hookform/resolvers/zod'
+import { SubmitHandler, useForm } from 'react-hook-form'
+import { toast } from 'sonner'
+import * as z from 'zod'
+
+import { InlineLink } from 'components/ui/InlineLink'
+import { useEmailUpdateMutation } from 'data/profile/profile-update-email-mutation'
+import { useRef, useState } from 'react'
+import {
+  Button,
+  DialogFooter,
+  DialogSection,
+  Form_Shadcn_,
+  FormControl_Shadcn_,
+  FormField_Shadcn_,
+  Input_Shadcn_,
+} from 'ui'
+import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout'
+
+export const GitHubChangeEmailAddress = () => {
+  return (
+    <DialogSection className="flex flex-col gap-y-2">
+      <p className="text-sm">
+        Email addresses for GitHub identities should be updated through GitHub
+      </p>
+      <ol className="flex flex-col gap-y-0.5 text-sm ml-4 pl-2 list-decimal text-foreground-light">
+        <li>Log out of Supabase</li>
+        <li>
+          Change your Primary Email in{' '}
+          <InlineLink href="https://docs.github.com/en/account-and-profile/setting-up-and-managing-your-personal-account-on-github/managing-email-preferences/changing-your-primary-email-address">
+            GitHub
+          </InlineLink>{' '}
+          (your public, not primary email)
+        </li>
+        <li>Log out of GitHub</li>
+        <li>Log back into GitHub (with the new, desired email set as primary)</li>
+        <li>Log back into Supabase</li>
+      </ol>
+    </DialogSection>
+  )
+}
+
+export const SSOChangeEmailAddress = () => {
+  return (
+    <DialogSection className="flex flex-col gap-y-2">
+      <p className="text-sm">
+        Email addresses for SSO should be updated through your identity provider
+      </p>
+      <ol className="flex flex-col gap-y-0.5 text-sm ml-4 pl-2 list-decimal text-foreground-light">
+        <li>Log out of Supabase</li>
+        <li>Change your Primary Email in your identity provider</li>
+        <li>Log back into Supabase via SSO with the new, desired email</li>
+      </ol>
path: apps/studio/components/interfaces/Account/Preferences/ChangeEmailAddress.tsx
user: login: kangmingtay
body: ```suggestion
      <p className="text-sm">
        Email addresses for SSO should be updated through your identity provider
      </p>
      <ol className="flex flex-col gap-y-0.5 text-sm ml-4 pl-2 list-decimal text-foreground-light">
        <li>Contact the owner / admin for your team to change your email</li>
      </ol>
```
created_at: 2025-03-04T09:22:06Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,133 @@
+import HCaptcha from '@hcaptcha/react-hcaptcha'
+import { zodResolver } from '@hookform/resolvers/zod'
+import { SubmitHandler, useForm } from 'react-hook-form'
+import { toast } from 'sonner'
+import * as z from 'zod'
+
+import { InlineLink } from 'components/ui/InlineLink'
+import { useEmailUpdateMutation } from 'data/profile/profile-update-email-mutation'
+import { useRef, useState } from 'react'
+import {
+  Button,
+  DialogFooter,
+  DialogSection,
+  Form_Shadcn_,
+  FormControl_Shadcn_,
+  FormField_Shadcn_,
+  Input_Shadcn_,
+} from 'ui'
+import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout'
+
+export const GitHubChangeEmailAddress = () => {
+  return (
+    <DialogSection className="flex flex-col gap-y-2">
+      <p className="text-sm">
+        Email addresses for GitHub identities should be updated through GitHub
+      </p>
+      <ol className="flex flex-col gap-y-0.5 text-sm ml-4 pl-2 list-decimal text-foreground-light">
+        <li>Log out of Supabase</li>
+        <li>
+          Change your Primary Email in{' '}
+          <InlineLink href="https://docs.github.com/en/account-and-profile/setting-up-and-managing-your-personal-account-on-github/managing-email-preferences/changing-your-primary-email-address">
+            GitHub
+          </InlineLink>{' '}
+          (your public, not primary email)
+        </li>
+        <li>Log out of GitHub</li>
+        <li>Log back into GitHub (with the new, desired email set as primary)</li>
+        <li>Log back into Supabase</li>
+      </ol>
+    </DialogSection>
+  )
+}
+
+export const SSOChangeEmailAddress = () => {
+  return (
+    <DialogSection className="flex flex-col gap-y-2">
+      <p className="text-sm">
+        Email addresses for SSO should be updated through your identity provider
+      </p>
+      <ol className="flex flex-col gap-y-0.5 text-sm ml-4 pl-2 list-decimal text-foreground-light">
+        <li>Log out of Supabase</li>
+        <li>Change your Primary Email in your identity provider</li>
+        <li>Log back into Supabase via SSO with the new, desired email</li>
+      </ol>
path: apps/studio/components/interfaces/Account/Preferences/ChangeEmailAddress.tsx
user: login: kangmingtay
body: SSO emails are managed through the company's IT admin 
created_at: 2025-03-04T09:22:32Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,133 @@
+import HCaptcha from '@hcaptcha/react-hcaptcha'
+import { zodResolver } from '@hookform/resolvers/zod'
+import { SubmitHandler, useForm } from 'react-hook-form'
+import { toast } from 'sonner'
+import * as z from 'zod'
+
+import { InlineLink } from 'components/ui/InlineLink'
+import { useEmailUpdateMutation } from 'data/profile/profile-update-email-mutation'
+import { useRef, useState } from 'react'
+import {
+  Button,
+  DialogFooter,
+  DialogSection,
+  Form_Shadcn_,
+  FormControl_Shadcn_,
+  FormField_Shadcn_,
+  Input_Shadcn_,
+} from 'ui'
+import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout'
+
+export const GitHubChangeEmailAddress = () => {
+  return (
+    <DialogSection className="flex flex-col gap-y-2">
+      <p className="text-sm">
+        Email addresses for GitHub identities should be updated through GitHub
+      </p>
+      <ol className="flex flex-col gap-y-0.5 text-sm ml-4 pl-2 list-decimal text-foreground-light">
+        <li>Log out of Supabase</li>
+        <li>
+          Change your Primary Email in{' '}
+          <InlineLink href="https://docs.github.com/en/account-and-profile/setting-up-and-managing-your-personal-account-on-github/managing-email-preferences/changing-your-primary-email-address">
+            GitHub
+          </InlineLink>{' '}
+          (your public, not primary email)
path: apps/studio/components/interfaces/Account/Preferences/ChangeEmailAddress.tsx
user: login: kangmingtay
body: hmm i think this should also be changed to:
```suggestion
          (your primary email)
```
created_at: 2025-03-06T15:35:41Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,192 @@
+import { Edit, Unlink } from 'lucide-react'
+import Image from 'next/image'
+import Link from 'next/link'
+import { useRouter } from 'next/router'
+import { useEffect, useState } from 'react'
+import { toast } from 'sonner'
+
+import { ButtonTooltip } from 'components/ui/ButtonTooltip'
+import Panel from 'components/ui/Panel'
+import { useProfileIdentitiesQuery } from 'data/profile/profile-identities-query'
+import { useUnlinkIdentityMutation } from 'data/profile/profile-unlink-identity-mutation'
+import dayjs from 'dayjs'
+import { BASE_PATH } from 'lib/constants'
+import {
+  Badge,
+  Button,
+  cn,
+  Dialog,
+  DialogContent,
+  DialogHeader,
+  DialogTitle,
+  Tooltip,
+  TooltipContent,
+  TooltipTrigger,
+} from 'ui'
+import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal'
+import ShimmeringLoader from 'ui-patterns/ShimmeringLoader'
+import {
+  ChangeEmailAddressForm,
+  GitHubChangeEmailAddress,
+  SSOChangeEmailAddress,
+} from './ChangeEmailAddress'
+
+export const AccountIdentities = () => {
+  const router = useRouter()
+
+  const { data, isLoading, isSuccess } = useProfileIdentitiesQuery()
+  const identities = data?.identities ?? []
+  const isChangeExpired = data?.email_change_sent_at
+    ? dayjs().utc().diff(dayjs(data?.email_change_sent_at).utc(), 'minute') > 10
+    : false
+
+  const [selectedProviderUnlink, setSelectedProviderUnlink] = useState<string>()
+  const [selectedProviderUpdateEmail, setSelectedProviderUpdateEmail] = useState<string>()
+
+  const { mutate: unlinkIdentity, isLoading: isUnlinking } = useUnlinkIdentityMutation({
+    onSuccess: () => {
+      toast.success(
+        `Successfully unlinked ${getProviderName(selectedProviderUnlink ?? '')} identity!`
+      )
+      setSelectedProviderUnlink(undefined)
+    },
+  })
+
+  const [_, message] = router.asPath.split('#message=')
+
+  const getProviderName = (provider: string) =>
+    provider === 'github' ? 'GitHub' : provider.startsWith('sso') ? 'SSO' : provider
+
+  const onConfirmUnlinkIdentity = async () => {
+    const identity = identities.find((i) => i.provider === selectedProviderUnlink)
+    if (identity) unlinkIdentity(identity)
+  }
+
+  useEffect(() => {
+    if (message) toast.success(message.replaceAll('+', ' '))
+  }, [message])
+
+  return (
+    <>
+      <Panel className="mb-4 md:mb-8" title={<h5>Account Identities</h5>}>
+        {isLoading && (
+          <Panel.Content>
+            <ShimmeringLoader />
+          </Panel.Content>
+        )}
+        {isSuccess &&
+          identities.map((identity) => {
+            const { identity_id, provider } = identity
+            const username = identity.identity_data?.user_name
+            const providerName = getProviderName(provider)
+            const iconKey =
+              provider === 'github'
+                ? 'github-icon'
+                : provider === 'email'
+                  ? 'email-icon2'
+                  : 'saml-icon'
+
+            return (
+              <Panel.Content
+                key={identity_id}
+                className={cn('flex justify-between', identities.length > 1 ? 'last:border-t' : '')}
+              >
+                <div className="flex gap-x-4">
+                  <Image
+                    className={cn(iconKey === 'github-icon' ? 'invert' : '')}
+                    src={`${BASE_PATH}/img/icons/${iconKey}.svg`}
+                    width={30}
+                    height={30}
+                    alt={`${identity.provider} icon`}
+                  />
+                  <div>
+                    <div className="flex items-center gap-x-2">
+                      <p className="text-sm capitalize">{providerName}</p>
+                      {provider === 'email' && data.new_email && !isChangeExpired && (
+                        <Tooltip>
+                          <TooltipTrigger className="flex items-center">
+                            <Badge variant="default">Pending change</Badge>
+                          </TooltipTrigger>
+                          <TooltipContent>Changing to {data.new_email}</TooltipContent>
+                        </Tooltip>
+                      )}
+                      {/* [Joshen] Below is not supported yet, but ideal UX */}
+                      {/* {false && <Badge>Logged in as</Badge>} */}
+                    </div>
+                    <p className="text-sm text-foreground-lighter">
+                      {!!username ? <span>{username} • </span> : null}
+                      {(identity as any).email}
+                    </p>
+                  </div>
+                </div>
+                <div className="flex items-center gap-x-1">
+                  {provider === 'email' && (
+                    <Button asChild type="default">
+                      <Link href="/reset-password">Reset password</Link>
+                    </Button>
+                  )}
+                  <ButtonTooltip
+                    type="text"
+                    icon={<Edit />}
+                    className="w-7"
+                    onClick={() => setSelectedProviderUpdateEmail(provider)}
+                    tooltip={{ content: { side: 'bottom', text: 'Update email address' } }}
+                  />
+                  {identities.length > 1 && (
+                    <ButtonTooltip
+                      type="text"
+                      icon={<Unlink />}
+                      className="w-7"
+                      onClick={() => setSelectedProviderUnlink(provider)}
+                      tooltip={{ content: { side: 'bottom', text: 'Unlink identity' } }}
+                    />
+                  )}
+                </div>
+              </Panel.Content>
+            )
+          })}
+      </Panel>
+
+      <Dialog
+        open={!!selectedProviderUpdateEmail}
+        onOpenChange={(open: boolean) => {
+          if (!open) setSelectedProviderUpdateEmail(undefined)
+        }}
+      >
+        <DialogContent>
+          <DialogHeader className="border-b">
+            <DialogTitle>
+              {selectedProviderUpdateEmail !== 'email'
+                ? `Updating email address for ${getProviderName(selectedProviderUpdateEmail ?? '')} identity`
+                : 'Update email address'}
+            </DialogTitle>
+          </DialogHeader>
+          {selectedProviderUpdateEmail === 'github' ? (
+            <GitHubChangeEmailAddress />
+          ) : selectedProviderUpdateEmail?.startsWith('sso') ? (
+            <SSOChangeEmailAddress />
+          ) : (
+            <ChangeEmailAddressForm onClose={() => setSelectedProviderUpdateEmail(undefined)} />
+          )}
+        </DialogContent>
+      </Dialog>
+
+      <ConfirmationModal
+        variant="warning"
+        size="small"
+        loading={isUnlinking}
+        visible={!!selectedProviderUnlink}
+        title={`Unlink ${getProviderName(selectedProviderUnlink ?? '')} identity`}
+        onCancel={() => setSelectedProviderUnlink(undefined)}
+        onConfirm={() => onConfirmUnlinkIdentity()}
path: apps/studio/components/interfaces/Account/Preferences/AccountIdentities.tsx
user: login: SimonGodefroid
body: ```suggestion
        onConfirm={onConfirmUnlinkIdentity}
```
created_at: 2025-03-18T06:32:27Z
author_association: NONE
diff_hunk: @@ -0,0 +1,134 @@
+import HCaptcha from '@hcaptcha/react-hcaptcha'
+import { zodResolver } from '@hookform/resolvers/zod'
+import { useRef, useState } from 'react'
+import { SubmitHandler, useForm } from 'react-hook-form'
+import { toast } from 'sonner'
+import * as z from 'zod'
+
+import { InlineLink } from 'components/ui/InlineLink'
+import { useEmailUpdateMutation } from 'data/profile/profile-update-email-mutation'
+import {
+  Button,
+  DialogFooter,
+  DialogSection,
+  Form_Shadcn_,
+  FormControl_Shadcn_,
+  FormField_Shadcn_,
+  Input_Shadcn_,
+} from 'ui'
+import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout'
+
+export const GitHubChangeEmailAddress = () => {
+  return (
+    <DialogSection className="flex flex-col gap-y-2">
+      <p className="text-sm">
+        Email addresses for GitHub identities should be updated through GitHub
+      </p>
+      <ol className="flex flex-col gap-y-0.5 text-sm ml-4 pl-2 list-decimal text-foreground-light">
+        <li>Log out of Supabase</li>
+        <li>
+          Change your Primary Email in{' '}
+          <InlineLink href="https://docs.github.com/en/account-and-profile/setting-up-and-managing-your-personal-account-on-github/managing-email-preferences/changing-your-primary-email-address">
+            GitHub
+          </InlineLink>{' '}
+          (your primary email)
+        </li>
+        <li>Log out of GitHub</li>
+        <li>Log back into GitHub (with the new, desired email set as primary)</li>
+        <li>Log back into Supabase</li>
+      </ol>
+    </DialogSection>
+  )
+}
+
+export const SSOChangeEmailAddress = () => {
+  return (
+    <DialogSection className="flex flex-col gap-y-2">
+      <p className="text-sm">
+        Email addresses for SSO should be updated through your identity provider
+      </p>
+      <ol className="flex flex-col gap-y-0.5 text-sm ml-4 pl-2 list-decimal text-foreground-light">
+        <li>Contact the owner / admin for your team to change your email</li>
+      </ol>
+    </DialogSection>
+  )
+}
+
+export const ChangeEmailAddressForm = ({ onClose }: { onClose: () => void }) => {
+  const captchaRef = useRef<HCaptcha>(null)
+  const [captchaToken, setCaptchaToken] = useState<string | null>(null)
+
+  const FormSchema = z.object({ email: z.string().email() })
+  const form = useForm<z.infer<typeof FormSchema>>({
+    mode: 'onBlur',
+    reValidateMode: 'onBlur',
+    resolver: zodResolver(FormSchema),
+    defaultValues: { email: '' },
+  })
+
+  const { mutate: updateEmail, isLoading } = useEmailUpdateMutation({
+    onSuccess: (_, vars) => {
+      toast.success(
+        `A confirmation email has been sent to ${vars.email}. Please confirm the change within 10 minutes.`
+      )
+      onClose()
+    },
+    onError: (error) => {
+      toast.error(`Failed to update email: ${error.message}`)
+      setCaptchaToken(null)
+      captchaRef.current?.resetCaptcha()
+    },
+  })
+
+  const onSubmit: SubmitHandler<z.infer<typeof FormSchema>> = async (values) => {
+    let token = captchaToken
+    if (!token) {
+      const captchaResponse = await captchaRef.current?.execute({ async: true })
+      token = captchaResponse?.response ?? null
+    }
+
+    updateEmail({ email: values.email, hcaptchaToken: token ?? null })
+  }
+
+  return (
+    <Form_Shadcn_ {...form}>
+      <form id="update-email-form" onSubmit={form.handleSubmit(onSubmit)}>
+        <div className="self-center">
+          <HCaptcha
+            ref={captchaRef}
+            sitekey={process.env.NEXT_PUBLIC_HCAPTCHA_SITE_KEY!}
+            size="invisible"
+            onVerify={(token) => setCaptchaToken(token)}
+            onExpire={() => setCaptchaToken(null)}
+          />
+        </div>
+
+        <DialogSection>
+          <FormField_Shadcn_
+            name="email"
+            control={form.control}
+            render={({ field }) => (
+              <FormItemLayout
+                label="Provide a new email address"
+                description="A confirmation email will be sent to the provided email address"
+              >
+                <FormControl_Shadcn_>
+                  <Input_Shadcn_ {...field} placeholder="example@email.com" />
+                </FormControl_Shadcn_>
+              </FormItemLayout>
+            )}
+          />
+        </DialogSection>
+
+        <DialogFooter>
+          <Button type="default" disabled={isLoading} onClick={() => onClose()}>
path: apps/studio/components/interfaces/Account/Preferences/ChangeEmailAddress.tsx
user: login: SimonGodefroid
body: ```suggestion
          <Button type="default" disabled={isLoading} onClick={onClose}>
```
created_at: 2025-03-18T06:33:39Z
author_association: NONE
diff_hunk: @@ -0,0 +1,41 @@
+import type { UserIdentity } from '@supabase/supabase-js'
+import { useMutation, UseMutationOptions, useQueryClient } from '@tanstack/react-query'
+import { toast } from 'sonner'
+
+import { auth } from 'lib/gotrue'
+import { profileKeys } from './keys'
+
+const unlinkIdentity = async (identity: UserIdentity) => {
+  const { error, data } = await auth.unlinkIdentity(identity)
+
+  if (error) throw error
+  return data
+}
+
+type UnlinkIdentityResponse = any
+type UnlinkIdentityError = any
+
+export const useUnlinkIdentityMutation = ({
+  onSuccess,
+  onError,
+  ...options
+}: Omit<
+  UseMutationOptions<UnlinkIdentityResponse, UnlinkIdentityError, UserIdentity>,
+  'mutationFn'
+> = {}) => {
+  const queryClient = useQueryClient()
+  return useMutation((vars) => unlinkIdentity(vars), {
+    async onSuccess(data, variables, context) {
+      await Promise.all([auth.getUser(), queryClient.invalidateQueries(profileKeys.identities())])
path: apps/studio/data/profile/profile-unlink-identity-mutation.ts
user: login: alaister
body: I assume you're calling `auth.getUser()` here to try and refresh the token? I think it only refreshes if the token has expired, so perhaps `auth.refreshSession()` would be better.
created_at: 2025-03-20T16:49:05Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,52 @@
+import { useMutation, UseMutationOptions, useQueryClient } from '@tanstack/react-query'
+import { toast } from 'sonner'
+
+import { handleError, put } from 'data/fetchers'
+import { auth } from 'lib/gotrue'
+import type { ResponseError } from 'types'
+import { profileKeys } from './keys'
+
+export type EmailUpdateVariables = {
+  email: string
+  hcaptchaToken: string | null
+}
+
+export async function updateEmail({ email, hcaptchaToken }: EmailUpdateVariables) {
+  const { data, error } = await put('/platform/update-email', {
+    body: { newEmail: email, hcaptchaToken },
+  })
+
+  if (error) handleError(error)
+  return data
+}
+
+type EmailUpdateData = Awaited<ReturnType<typeof updateEmail>>
+
+export const useEmailUpdateMutation = ({
+  onSuccess,
+  onError,
+  ...options
+}: Omit<
+  UseMutationOptions<EmailUpdateData, ResponseError, EmailUpdateVariables>,
+  'mutationFn'
+> = {}) => {
+  const queryClient = useQueryClient()
+
+  return useMutation<EmailUpdateData, ResponseError, EmailUpdateVariables>(
+    (vars) => updateEmail(vars),
+    {
+      async onSuccess(data, variables, context) {
+        await Promise.all([auth.getUser(), queryClient.invalidateQueries(profileKeys.profile())])
path: apps/studio/data/profile/profile-update-email-mutation.ts
user: login: alaister
body: Same here
created_at: 2025-03-20T16:49:19Z
author_association: MEMBER

PR comments:
user: login: vercel[bot]
created_at: 2025-03-03T12:00:55Z
body: [vc]: #FxF95VXo9CUK7h9IRp+A4AS0jGumCp8DcfZmCbggg0Q=:eyJpc01vbm9yZXBvIjp0cnVlLCJ0eXBlIjoiZ2l0aHViIiwicHJvamVjdHMiOlt7Im5hbWUiOiJ6b25lLXd3dy1kb3QtY29tIiwicm9vdERpcmVjdG9yeSI6ImFwcHMvd3d3IiwibGl2ZUZlZWRiYWNrIjp7InJlc29sdmVkIjowLCJ1bnJlc29sdmVkIjowLCJ0b3RhbCI6MCwibGluayI6InpvbmUtd3d3LWRvdC1jb20tZ2l0LWNob3JlLXN1cHBvcnQtdXBkYXRpbmctZW1hLWNmNjU2Yi1zdXBhYmFzZS52ZXJjZWwuYXBwIn0sImluc3BlY3RvclVybCI6Imh0dHBzOi8vdmVyY2VsLmNvbS9zdXBhYmFzZS96b25lLXd3dy1kb3QtY29tL0RFNFJxbnN2WTVNaG92M1Q4UXFicm4yVnh1eGQiLCJwcmV2aWV3VXJsIjoiem9uZS13d3ctZG90LWNvbS1naXQtY2hvcmUtc3VwcG9ydC11cGRhdGluZy1lbWEtY2Y2NTZiLXN1cGFiYXNlLnZlcmNlbC5hcHAiLCJuZXh0Q29tbWl0U3RhdHVzIjoiREVQTE9ZRUQifSx7Im5hbWUiOiJkb2NzIiwicm9vdERpcmVjdG9yeSI6ImFwcHMvZG9jcyIsImxpdmVGZWVkYmFjayI6eyJyZXNvbHZlZCI6MCwidW5yZXNvbHZlZCI6MCwidG90YWwiOjAsImxpbmsiOiJkb2NzLWdpdC1jaG9yZS1zdXBwb3J0LXVwZGF0aW5nLWVtYWlsLWFuZC11bmxpbi05YjA0MjYtc3VwYWJhc2UudmVyY2VsLmFwcCJ9LCJpbnNwZWN0b3JVcmwiOiJodHRwczovL3ZlcmNlbC5jb20vc3VwYWJhc2UvZG9jcy82a3RxZEVTNGNYZEV1b2IxNmIxUzh3VzJaMUhDIiwicHJldmlld1VybCI6ImRvY3MtZ2l0LWNob3JlLXN1cHBvcnQtdXBkYXRpbmctZW1haWwtYW5kLXVubGluLTliMDQyNi1zdXBhYmFzZS52ZXJjZWwuYXBwIiwibmV4dENvbW1pdFN0YXR1cyI6IkRFUExPWUVEIn0seyJuYW1lIjoic3R1ZGlvLXNlbGYtaG9zdGVkIiwicm9vdERpcmVjdG9yeSI6ImFwcHMvc3R1ZGlvIiwibGl2ZUZlZWRiYWNrIjp7InJlc29sdmVkIjowLCJ1bnJlc29sdmVkIjowLCJ0b3RhbCI6MCwibGluayI6InN0dWRpby1zZWxmLWhvc3RlZC1naXQtY2hvcmUtc3VwcG9ydC11cGRhdGluZy1lLThmMmYzOS1zdXBhYmFzZS52ZXJjZWwuYXBwIn0sImluc3BlY3RvclVybCI6Imh0dHBzOi8vdmVyY2VsLmNvbS9zdXBhYmFzZS9zdHVkaW8tc2VsZi1ob3N0ZWQvQml6Q1ZaaXN0alRhMnZGTmJKUFdxWXEzZXBONSIsInByZXZpZXdVcmwiOiJzdHVkaW8tc2VsZi1ob3N0ZWQtZ2l0LWNob3JlLXN1cHBvcnQtdXBkYXRpbmctZS04ZjJmMzktc3VwYWJhc2UudmVyY2VsLmFwcCIsIm5leHRDb21taXRTdGF0dXMiOiJERVBMT1lFRCJ9LHsibmFtZSI6InN0dWRpbyIsInJvb3REaXJlY3RvcnkiOiJhcHBzL3N0dWRpbyIsImxpdmVGZWVkYmFjayI6eyJyZXNvbHZlZCI6MCwidW5yZXNvbHZlZCI6MCwidG90YWwiOjAsImxpbmsiOiIifSwiaW5zcGVjdG9yVXJsIjoiaHR0cHM6Ly92ZXJjZWwuY29tL3N1cGFiYXNlL3N0dWRpby9EaDhValJRVDJKZXdiNEcyRGlEcUx1amUyYk1qIiwibmV4dENvbW1pdFN0YXR1cyI6IklHTk9SRUQiLCJwcmV2aWV3VXJsIjoic3R1ZGlvLWdpdC1jaG9yZS1zdXBwb3J0LXVwZGF0aW5nLWVtYWlsLWFuZC11bmwtODFiMjBhLXN1cGFiYXNlLnZlcmNlbC5hcHAifSx7Im5hbWUiOiJzdHVkaW8tc3RhZ2luZyIsInJvb3REaXJlY3RvcnkiOiJhcHBzL3N0dWRpbyIsImxpdmVGZWVkYmFjayI6eyJyZXNvbHZlZCI6MCwidW5yZXNvbHZlZCI6MCwidG90YWwiOjAsImxpbmsiOiJzdHVkaW8tc3RhZ2luZy1naXQtY2hvcmUtc3VwcG9ydC11cGRhdGluZy1lbWFpbC0zZDY3Y2Itc3VwYWJhc2UudmVyY2VsLmFwcCJ9LCJpbnNwZWN0b3JVcmwiOiJodHRwczovL3ZlcmNlbC5jb20vc3VwYWJhc2Uvc3R1ZGlvLXN0YWdpbmcvMTJxdE54b0ZpbnU3endaWWhiaUZuSDl3emJmeiIsInByZXZpZXdVcmwiOiJzdHVkaW8tc3RhZ2luZy1naXQtY2hvcmUtc3VwcG9ydC11cGRhdGluZy1lbWFpbC0zZDY3Y2Itc3VwYWJhc2UudmVyY2VsLmFwcCIsIm5leHRDb21taXRTdGF0dXMiOiJERVBMT1lFRCJ9LHsibmFtZSI6ImRlc2lnbi1zeXN0ZW0iLCJyb290RGlyZWN0b3J5IjoiYXBwcy9kZXNpZ24tc3lzdGVtIiwibGl2ZUZlZWRiYWNrIjp7InJlc29sdmVkIjowLCJ1bnJlc29sdmVkIjowLCJ0b3RhbCI6MCwibGluayI6ImRlc2lnbi1zeXN0ZW0tZ2l0LWNob3JlLXN1cHBvcnQtdXBkYXRpbmctZW1haWwtMmFhMmFkLXN1cGFiYXNlLnZlcmNlbC5hcHAifSwiaW5zcGVjdG9yVXJsIjoiaHR0cHM6Ly92ZXJjZWwuY29tL3N1cGFiYXNlL2Rlc2lnbi1zeXN0ZW0vNjVLS1VYRlQ3OWVGVVdDc0MxSlpiaGdpTVNaeSIsInByZXZpZXdVcmwiOiJkZXNpZ24tc3lzdGVtLWdpdC1jaG9yZS1zdXBwb3J0LXVwZGF0aW5nLWVtYWlsLTJhYTJhZC1zdXBhYmFzZS52ZXJjZWwuYXBwIiwibmV4dENvbW1pdFN0YXR1cyI6IkRFUExPWUVEIn0seyJuYW1lIjoidWktbGlicmFyeSIsInJvb3REaXJlY3RvcnkiOiJhcHBzL3VpLWxpYnJhcnkiLCJpbnNwZWN0b3JVcmwiOiJodHRwczovL3ZlcmNlbC5jb20vc3VwYWJhc2UvdWktbGlicmFyeS9SRWhXSHFlcjhBZ2lSUHBiYXNWWGF6dThKSGpIIiwicHJldmlld1VybCI6InVpLWxpYnJhcnktZ2l0LWNob3JlLXN1cHBvcnQtdXBkYXRpbmctZW1haWwtYW5kLTRkYTU4Ny1zdXBhYmFzZS52ZXJjZWwuYXBwIiwibmV4dENvbW1pdFN0YXR1cyI6IkRFUExPWUVEIiwibGl2ZUZlZWRiYWNrIjp7InJlc29sdmVkIjowLCJ1bnJlc29sdmVkIjowLCJ0b3RhbCI6MCwibGluayI6InVpLWxpYnJhcnktZ2l0LWNob3JlLXN1cHBvcnQtdXBkYXRpbmctZW1haWwtYW5kLTRkYTU4Ny1zdXBhYmFzZS52ZXJjZWwuYXBwIn19XX0=
**The latest updates on your projects**. Learn more about [Vercel for Git ↗︎](https://vercel.link/github-learn-more)

| Name | Status | Preview | Comments | Updated (UTC) |
| :--- | :----- | :------ | :------- | :------ |
| **design-system** | ✅ Ready ([Inspect](https://vercel.com/supabase/design-system/65KKUXFT79eFUWCsC1JZbhgiMSZy)) | [Visit Preview](https://design-system-git-chore-support-updating-email-2aa2ad-supabase.vercel.app) | 💬 [**Add feedback**](https://vercel.live/open-feedback/design-system-git-chore-support-updating-email-2aa2ad-supabase.vercel.app?via=pr-comment-feedback-link) | Mar 21, 2025 4:05am |
| **docs** | ✅ Ready ([Inspect](https://vercel.com/supabase/docs/6ktqdES4cXdEuob16b1S8wW2Z1HC)) | [Visit Preview](https://docs-git-chore-support-updating-email-and-unlin-9b0426-supabase.vercel.app) | 💬 [**Add feedback**](https://vercel.live/open-feedback/docs-git-chore-support-updating-email-and-unlin-9b0426-supabase.vercel.app?via=pr-comment-feedback-link) | Mar 21, 2025 4:05am |
| **studio-self-hosted** | ✅ Ready ([Inspect](https://vercel.com/supabase/studio-self-hosted/BizCVZistjTa2vFNbJPWqYq3epN5)) | [Visit Preview](https://studio-self-hosted-git-chore-support-updating-e-8f2f39-supabase.vercel.app) | 💬 [**Add feedback**](https://vercel.live/open-feedback/studio-self-hosted-git-chore-support-updating-e-8f2f39-supabase.vercel.app?via=pr-comment-feedback-link) | Mar 21, 2025 4:05am |
| **studio-staging** | ✅ Ready ([Inspect](https://vercel.com/supabase/studio-staging/12qtNxoFinu7zwZYhbiFnH9wzbfz)) | [Visit Preview](https://studio-staging-git-chore-support-updating-email-3d67cb-supabase.vercel.app) | 💬 [**Add feedback**](https://vercel.live/open-feedback/studio-staging-git-chore-support-updating-email-3d67cb-supabase.vercel.app?via=pr-comment-feedback-link) | Mar 21, 2025 4:05am |
| **ui-library** | ✅ Ready ([Inspect](https://vercel.com/supabase/ui-library/REhWHqer8AgiRPpbasVXazu8JHjH)) | [Visit Preview](https://ui-library-git-chore-support-updating-email-and-4da587-supabase.vercel.app) | 💬 [**Add feedback**](https://vercel.live/open-feedback/ui-library-git-chore-support-updating-email-and-4da587-supabase.vercel.app?via=pr-comment-feedback-link) | Mar 21, 2025 4:05am |
| **zone-www-dot-com** | ✅ Ready ([Inspect](https://vercel.com/supabase/zone-www-dot-com/DE4RqnsvY5Mhov3T8Qqbrn2Vxuxd)) | [Visit Preview](https://zone-www-dot-com-git-chore-support-updating-ema-cf656b-supabase.vercel.app) | 💬 [**Add feedback**](https://vercel.live/open-feedback/zone-www-dot-com-git-chore-support-updating-ema-cf656b-supabase.vercel.app?via=pr-comment-feedback-link) | Mar 21, 2025 4:05am |

<details><summary>1 Skipped Deployment</summary>

| Name | Status | Preview | Comments | Updated (UTC) |
| :--- | :----- | :------ | :------- | :------ |
| **studio** | ⬜️ Ignored ([Inspect](https://vercel.com/supabase/studio/Dh8UjRQT2Jewb4G2DiDqLuje2bMj)) | [Visit Preview](https://studio-git-chore-support-updating-email-and-unl-81b20a-supabase.vercel.app) |  | Mar 21, 2025 4:05am |
</details>








author_association: NONE
user: login: supabase[bot]
created_at: 2025-03-03T12:00:56Z
body: [supa]:xguihxuzqibwxjnimxev
This pull request has been ignored for the connected project `xguihxuzqibwxjnimxev` because there are no changes detected in `supabase` directory. You can change this behaviour in [Project Integrations Settings ↗︎](https://supabase.com/dashboard/project/xguihxuzqibwxjnimxev/settings/integrations).

<hr/>

Preview Branches by Supabase.
Learn more about [Supabase Branching ↗︎](https://supabase.com/docs/guides/platform/branching).
author_association: NONE
user: login: linear[bot]
created_at: 2025-03-03T12:01:04Z
body: <p><a href="https://linear.app/supabase/issue/FE-1438/expose-email-update-and-identity-linking-methods-in-the-dashboard">FE-1438 Expose email update and identity linking methods in the dashboard</a></p>
author_association: NONE
user: login: kangmingtay
created_at: 2025-03-06T21:33:06Z
body: @joshenlim hmm i think from a user's POV, unlinking an identity isn't super clear. I took a look at sentry and i like how they phrase it as "disconnecting". Maybe we can also add something like: "You will no longer be able to use \<identity\> to sign-in".

<img width="1732" alt="image" src="https://github.com/user-attachments/assets/3980f266-9ddf-46f0-b7b3-d509fad77960" />

author_association: MEMBER
user: login: kangmingtay
created_at: 2025-03-10T08:46:35Z
body: @joshenlim was testing this on staging with a sso user (yes the api changes are out already :))

the identity displayed is kinda weird - i think we don't have to show the `provider` field here for sso identities because it's not useful information
<img width="767" alt="image" src="https://github.com/user-attachments/assets/5500edcd-a4fe-4187-a71a-4f558c155684" />

i think we shouldn't even allow the user to update their email for sso identities:
<img width="1071" alt="image" src="https://github.com/user-attachments/assets/f4992c26-9ea6-4d2b-9750-9b746c954c64" />

author_association: MEMBER
user: login: joshenlim
created_at: 2025-03-10T16:50:54Z
body: @kangmingtay 

> is it also possible to show the new email as "pending" after initiating the update?

is there any property from the API / getProfileIdentities that will support FE to render the "pending" state though? otherwise if we do the logic on the FE, the badge will not persist across sessions
author_association: MEMBER
user: login: joshenlim
created_at: 2025-03-10T16:51:03Z
body: addressed all the other feedbacks otherwise 😄 
author_association: MEMBER
user: login: joshenlim
created_at: 2025-03-20T15:11:11Z
body: ^ gotcha! i'll look into this tomorrow once this [PR](https://github.com/supabase/supabase/pull/34240) goes through which should also fix the type problems here 😅 
author_association: MEMBER