diff --git a/.github/workflows/studio-unit-tests.yml b/.github/workflows/studio-unit-tests.yml index f82f29127b5f2..8ca7828af2ed1 100644 --- a/.github/workflows/studio-unit-tests.yml +++ b/.github/workflows/studio-unit-tests.yml @@ -26,8 +26,7 @@ permissions: jobs: test: # Uses larger hosted runner as it significantly decreases build times - runs-on: - group: Default Larger Runners + runs-on: ubuntu-latest strategy: matrix: test_number: [1] diff --git a/.github/workflows/typecheck.yml b/.github/workflows/typecheck.yml index 1ea8e4613028e..d0f24df07f08c 100644 --- a/.github/workflows/typecheck.yml +++ b/.github/workflows/typecheck.yml @@ -16,8 +16,7 @@ permissions: jobs: typecheck: # Uses larger hosted runner as it significantly decreases build times - runs-on: - group: Default Larger Runners + runs-on: ubuntu-latest steps: - name: Checkout diff --git a/apps/docs/app/contributing/content.mdx b/apps/docs/app/contributing/content.mdx index 2a5086cd2d922..c3632971a2a52 100644 --- a/apps/docs/app/contributing/content.mdx +++ b/apps/docs/app/contributing/content.mdx @@ -354,11 +354,11 @@ Some guides and tutorials will require that users copy their Supabase project UR ```mdx - + ``` - + ### Step Hike diff --git a/apps/docs/components/ProjectConfigVariables/ProjectConfigVariables.tsx b/apps/docs/components/ProjectConfigVariables/ProjectConfigVariables.tsx index 88423df94174e..b34ed122426bb 100644 --- a/apps/docs/components/ProjectConfigVariables/ProjectConfigVariables.tsx +++ b/apps/docs/components/ProjectConfigVariables/ProjectConfigVariables.tsx @@ -274,7 +274,7 @@ function VariableView({ variable, className }: { variable: Variable; className?: const hasBranches = selectedProject?.is_branch_enabled ?? false const ref = hasBranches ? selectedBranch?.project_ref : selectedProject?.ref - const needsApiQuery = variable === 'anonKey' || variable === 'url' + const needsApiQuery = variable === 'publishableKey' || variable === 'url' const needsSupavisorQuery = variable === 'sessionPooler' const { @@ -303,7 +303,7 @@ function VariableView({ variable, className }: { variable: Variable; className?: switch (variable) { case 'url': return !apiData.app_config?.endpoint - case 'anonKey': + case 'publishableKey': return !apiData.service_api_keys?.some((key) => key.tags === 'anon') } } @@ -336,7 +336,7 @@ function VariableView({ variable, className }: { variable: Variable; className?: case 'url': variableValue = `https://${apiData?.app_config?.endpoint}` break - case 'anonKey': + case 'publishableKey': variableValue = apiData?.service_api_keys?.find((key) => key.tags === 'anon')?.api_key || '' break case 'sessionPooler': diff --git a/apps/docs/components/ProjectConfigVariables/ProjectConfigVariables.utils.ts b/apps/docs/components/ProjectConfigVariables/ProjectConfigVariables.utils.ts index 1d1a125e3dd60..2ed28261bedd4 100644 --- a/apps/docs/components/ProjectConfigVariables/ProjectConfigVariables.utils.ts +++ b/apps/docs/components/ProjectConfigVariables/ProjectConfigVariables.utils.ts @@ -6,7 +6,7 @@ export type Org = OrganizationsData[number] export type Project = ProjectsData[number] export type Branch = BranchesData[number] -export type Variable = 'url' | 'anonKey' | 'sessionPooler' +export type Variable = 'url' | 'publishableKey' | 'sessionPooler' function removeDoubleQuotes(str: string) { return str.replaceAll('"', '') @@ -30,7 +30,7 @@ function unescapeDoubleQuotes(str: string) { export const prettyFormatVariable: Record = { url: 'Project URL', - anonKey: 'Anon key', + publishableKey: 'Publishable key', sessionPooler: 'Connection string (pooler session mode)', } diff --git a/apps/docs/content/guides/auth/server-side/creating-a-client.mdx b/apps/docs/content/guides/auth/server-side/creating-a-client.mdx index 08e8649f1d433..bbefe0d40c0c5 100644 --- a/apps/docs/content/guides/auth/server-side/creating-a-client.mdx +++ b/apps/docs/content/guides/auth/server-side/creating-a-client.mdx @@ -42,7 +42,7 @@ pnpm add @supabase/ssr @supabase/supabase-js In your environment variables file, set your Supabase URL and Supabase Anon Key: - + diff --git a/apps/docs/content/guides/auth/server-side/nextjs.mdx b/apps/docs/content/guides/auth/server-side/nextjs.mdx index ca43981674c78..6d437f6caddff 100644 --- a/apps/docs/content/guides/auth/server-side/nextjs.mdx +++ b/apps/docs/content/guides/auth/server-side/nextjs.mdx @@ -39,7 +39,7 @@ Create a `.env.local` file in your project root directory. Fill in your `NEXT_PUBLIC_SUPABASE_URL` and `NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY`: - + @@ -560,7 +560,7 @@ Create a `.env.local` file in your project root directory. Fill in your `NEXT_PUBLIC_SUPABASE_URL` and `NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY`: - + diff --git a/apps/docs/content/guides/auth/server-side/sveltekit.mdx b/apps/docs/content/guides/auth/server-side/sveltekit.mdx index 81db989fa9ecc..404fb252e2a48 100644 --- a/apps/docs/content/guides/auth/server-side/sveltekit.mdx +++ b/apps/docs/content/guides/auth/server-side/sveltekit.mdx @@ -34,7 +34,7 @@ Create a `.env.local` file in your project root directory. Fill in your `PUBLIC_SUPABASE_URL` and `PUBLIC_SUPABASE_PUBLISHABLE_KEY`: - + diff --git a/apps/docs/content/guides/functions/auth.mdx b/apps/docs/content/guides/functions/auth.mdx index 7e8959a6368e6..ca91575b35ca0 100644 --- a/apps/docs/content/guides/functions/auth.mdx +++ b/apps/docs/content/guides/functions/auth.mdx @@ -25,7 +25,7 @@ import { createClient } from 'npm:@supabase/supabase-js@2' Deno.serve(async (req: Request) => { const supabaseClient = createClient( Deno.env.get('SUPABASE_URL') ?? '', - Deno.env.get('SUPABASE_PUBLISHABLE_KEY') ?? '', + Deno.env.get('SUPABASE_ANON_KEY') ?? '', // Create client with Auth context of the user that called the function. // This way your row-level-security (RLS) policies are applied. { diff --git a/apps/docs/content/guides/functions/secrets.mdx b/apps/docs/content/guides/functions/secrets.mdx index 5a7a5ff431ac3..64bd322300740 100644 --- a/apps/docs/content/guides/functions/secrets.mdx +++ b/apps/docs/content/guides/functions/secrets.mdx @@ -10,7 +10,7 @@ subtitle: 'Manage sensitive data securely across environments.' Edge Functions have access to these secrets by default: - `SUPABASE_URL`: The API gateway for your Supabase project -- `SUPABASE_PUBLISHABLE_KEY`: The `publishable` key for your Supabase API. This is safe to use in a browser when you have Row Level Security enabled +- `SUPABASE_ANON_KEY`: The `anon` key for your Supabase API. This is safe to use in a browser when you have Row Level Security enabled - `SUPABASE_SERVICE_ROLE_KEY`: The `service_role` key for your Supabase API. This is safe to use in Edge Functions, but it should NEVER be used in a browser. This key will bypass Row Level Security - `SUPABASE_DB_URL`: The URL for your Postgres database. You can use this to connect directly to your database @@ -32,7 +32,7 @@ import { createClient } from 'npm:@supabase/supabase-js@2' // For user-facing operations (respects RLS) const supabase = createClient( Deno.env.get('SUPABASE_URL')!, - Deno.env.get('SUPABASE_PUBLISHABLE_KEY')! + Deno.env.get('SUPABASE_ANON_KEY')! ) // For admin operations (bypasses RLS) diff --git a/apps/docs/content/guides/getting-started/quickstarts/flutter.mdx b/apps/docs/content/guides/getting-started/quickstarts/flutter.mdx index 6420a59f1390e..54142ac73c5b6 100644 --- a/apps/docs/content/guides/getting-started/quickstarts/flutter.mdx +++ b/apps/docs/content/guides/getting-started/quickstarts/flutter.mdx @@ -58,7 +58,7 @@ hideToc: true Open `lib/main.dart` and edit the main function to initialize Supabase using your project URL and public API (anon) key: - + diff --git a/apps/docs/content/guides/getting-started/quickstarts/hono.mdx b/apps/docs/content/guides/getting-started/quickstarts/hono.mdx index d85823d199822..983c5086099e5 100644 --- a/apps/docs/content/guides/getting-started/quickstarts/hono.mdx +++ b/apps/docs/content/guides/getting-started/quickstarts/hono.mdx @@ -50,7 +50,7 @@ hideToc: true Lastly, [enable anonymous sign-ins](https://supabase.com/dashboard/project/_/settings/auth) in the Auth settings. - + diff --git a/apps/docs/content/guides/getting-started/quickstarts/ios-swiftui.mdx b/apps/docs/content/guides/getting-started/quickstarts/ios-swiftui.mdx index 91f73e5616ac3..9424fd47d9f77 100644 --- a/apps/docs/content/guides/getting-started/quickstarts/ios-swiftui.mdx +++ b/apps/docs/content/guides/getting-started/quickstarts/ios-swiftui.mdx @@ -42,7 +42,7 @@ hideToc: true Create a new `Supabase.swift` file add a new Supabase instance using your project URL and public API (anon) key: - + diff --git a/apps/docs/content/guides/getting-started/quickstarts/kotlin.mdx b/apps/docs/content/guides/getting-started/quickstarts/kotlin.mdx index 38c857552c379..59f7b6cf283bc 100644 --- a/apps/docs/content/guides/getting-started/quickstarts/kotlin.mdx +++ b/apps/docs/content/guides/getting-started/quickstarts/kotlin.mdx @@ -79,7 +79,7 @@ hideToc: true Replace the `supabaseUrl` and `supabaseKey` with your own: - + diff --git a/apps/docs/content/guides/getting-started/quickstarts/nextjs.mdx b/apps/docs/content/guides/getting-started/quickstarts/nextjs.mdx index 70277ee447ef3..6db673183c3d2 100644 --- a/apps/docs/content/guides/getting-started/quickstarts/nextjs.mdx +++ b/apps/docs/content/guides/getting-started/quickstarts/nextjs.mdx @@ -40,7 +40,7 @@ hideToc: true Rename `.env.example` to `.env.local` and populate with your Supabase connection variables: - + diff --git a/apps/docs/content/guides/getting-started/quickstarts/nuxtjs.mdx b/apps/docs/content/guides/getting-started/quickstarts/nuxtjs.mdx index cad739e7e8624..5524606a613ea 100644 --- a/apps/docs/content/guides/getting-started/quickstarts/nuxtjs.mdx +++ b/apps/docs/content/guides/getting-started/quickstarts/nuxtjs.mdx @@ -56,7 +56,7 @@ hideToc: true Create a `.env` file and populate with your Supabase connection variables: - + diff --git a/apps/docs/content/guides/getting-started/quickstarts/reactjs.mdx b/apps/docs/content/guides/getting-started/quickstarts/reactjs.mdx index 1e15500c1518c..1bf67565651f1 100644 --- a/apps/docs/content/guides/getting-started/quickstarts/reactjs.mdx +++ b/apps/docs/content/guides/getting-started/quickstarts/reactjs.mdx @@ -56,7 +56,7 @@ hideToc: true Create a `.env.local` file and populate with your Supabase connection variables: - + diff --git a/apps/docs/content/guides/getting-started/quickstarts/refine.mdx b/apps/docs/content/guides/getting-started/quickstarts/refine.mdx index da4656c9f4276..a6cb4ff632ed0 100644 --- a/apps/docs/content/guides/getting-started/quickstarts/refine.mdx +++ b/apps/docs/content/guides/getting-started/quickstarts/refine.mdx @@ -78,7 +78,7 @@ hideToc: true You now have to update the `supabaseClient` with the `SUPABASE_URL` and `SUPABASE_KEY` of your Supabase API. The `supabaseClient` is used in auth provider and data provider methods that allow the refine app to connect to your Supabase backend. - + diff --git a/apps/docs/content/guides/getting-started/quickstarts/solidjs.mdx b/apps/docs/content/guides/getting-started/quickstarts/solidjs.mdx index 0646a93b22ad8..10aa736884a14 100644 --- a/apps/docs/content/guides/getting-started/quickstarts/solidjs.mdx +++ b/apps/docs/content/guides/getting-started/quickstarts/solidjs.mdx @@ -56,7 +56,7 @@ hideToc: true Create a `.env.local` file and populate with your Supabase connection variables: - + diff --git a/apps/docs/content/guides/getting-started/quickstarts/sveltekit.mdx b/apps/docs/content/guides/getting-started/quickstarts/sveltekit.mdx index f71f5bda2b84b..c5b20f469cb02 100644 --- a/apps/docs/content/guides/getting-started/quickstarts/sveltekit.mdx +++ b/apps/docs/content/guides/getting-started/quickstarts/sveltekit.mdx @@ -57,7 +57,7 @@ hideToc: true Create a `.env` file at the root of your project and populate with your Supabase connection variables: - + diff --git a/apps/docs/content/guides/getting-started/quickstarts/vue.mdx b/apps/docs/content/guides/getting-started/quickstarts/vue.mdx index f98c7b3556252..2db179b77f4c4 100644 --- a/apps/docs/content/guides/getting-started/quickstarts/vue.mdx +++ b/apps/docs/content/guides/getting-started/quickstarts/vue.mdx @@ -56,7 +56,7 @@ hideToc: true Create a `.env.local` file and populate with your Supabase connection variables: - + diff --git a/apps/docs/content/guides/telemetry/log-drains.mdx b/apps/docs/content/guides/telemetry/log-drains.mdx index 7b1920a19557c..be5a1cd18cd9e 100644 --- a/apps/docs/content/guides/telemetry/log-drains.mdx +++ b/apps/docs/content/guides/telemetry/log-drains.mdx @@ -85,7 +85,7 @@ Create a HTTP drain under the [Project Settings > Log Drains](https://supabase.c - Under URL, set it to your edge function URL `https://[PROJECT REF].supabase.co/functions/v1/hello-world` - Under Headers, set the `Authorization: Bearer [ANON KEY]` - + diff --git a/apps/studio/components/interfaces/BranchManagement/BranchManagement.utils.ts b/apps/studio/components/interfaces/BranchManagement/BranchManagement.utils.ts new file mode 100644 index 0000000000000..e1f23edabf531 --- /dev/null +++ b/apps/studio/components/interfaces/BranchManagement/BranchManagement.utils.ts @@ -0,0 +1,66 @@ +import { DiskAttributesData } from 'data/config/disk-attributes-query' +import { DesiredInstanceSize, instanceSizeSpecs } from 'data/projects/new-project.constants' +import { + DISK_LIMITS, + DISK_PRICING, + DiskType, + PLAN_DETAILS, +} from '../DiskManagement/ui/DiskManagement.constants' + +// Ref: https://supabase.com/docs/guides/platform/compute-and-disk +const maxDiskForCompute = new Map([ + [10, instanceSizeSpecs.micro], + [50, instanceSizeSpecs.small], + [100, instanceSizeSpecs.medium], + [200, instanceSizeSpecs.large], + [500, instanceSizeSpecs.xlarge], + [1_000, instanceSizeSpecs['2xlarge']], + [2_000, instanceSizeSpecs['4xlarge']], + [4_000, instanceSizeSpecs['8xlarge']], + [6_000, instanceSizeSpecs['12xlarge']], + [10_000, instanceSizeSpecs['16xlarge']], +]) + +export const estimateComputeSize = ( + projectDiskSize: number, + branchComputeSize?: DesiredInstanceSize +) => { + if (branchComputeSize) { + return instanceSizeSpecs[branchComputeSize] + } + // Fallback to estimating based on volume size + for (const [disk, compute] of maxDiskForCompute) { + if (projectDiskSize <= disk) { + return compute + } + } + return instanceSizeSpecs['24xlarge'] +} + +export const estimateDiskCost = (disk: DiskAttributesData['attributes']) => { + const diskType = disk.type as DiskType + + const pricing = DISK_PRICING[diskType] + const includedGB = PLAN_DETAILS['pro'].includedDiskGB[diskType] + const priceSize = Math.max(disk.size_gb - includedGB, 0) * pricing.storage + const includedIOPS = DISK_LIMITS[diskType].includedIops + const priceIOPS = Math.max(disk.iops - includedIOPS, 0) * pricing.iops + + const priceThroughput = + diskType === DiskType.GP3 && 'throughput_mbps' in disk + ? Math.max(disk.throughput_mbps - DISK_LIMITS[DiskType.GP3].includedThroughput, 0) * + DISK_PRICING[DiskType.GP3].throughput + : 0 + + return { + total: priceSize + priceIOPS + priceThroughput, + size: priceSize, + iops: priceIOPS, + throughput: priceThroughput, + } +} + +export const estimateRestoreTime = (disk: DiskAttributesData['attributes']) => { + // This is interpolated from real restore time + return (720 / 21000) * disk.size_gb + 3 +} diff --git a/apps/studio/components/interfaces/BranchManagement/CreateBranchModal.tsx b/apps/studio/components/interfaces/BranchManagement/CreateBranchModal.tsx index 9fb2103792bf8..e6c183d0bf073 100644 --- a/apps/studio/components/interfaces/BranchManagement/CreateBranchModal.tsx +++ b/apps/studio/components/interfaces/BranchManagement/CreateBranchModal.tsx @@ -1,4 +1,5 @@ import { zodResolver } from '@hookform/resolvers/zod' +import { PermissionAction } from '@supabase/shared-types/out/constants' import { useQueryClient } from '@tanstack/react-query' import { DatabaseZap, DollarSign, GitMerge, Github, Loader2 } from 'lucide-react' import Image from 'next/image' @@ -9,20 +10,21 @@ import { useForm } from 'react-hook-form' import { toast } from 'sonner' import * as z from 'zod' -import { PermissionAction } from '@supabase/shared-types/out/constants' import { useFlag, useParams } from 'common' import { useIsBranching2Enabled } from 'components/interfaces/App/FeaturePreview/FeaturePreviewContext' import { BranchingPITRNotice } from 'components/layouts/AppLayout/EnableBranchingButton/BranchingPITRNotice' import AlertError from 'components/ui/AlertError' import { ButtonTooltip } from 'components/ui/ButtonTooltip' +import { InlineLink, InlineLinkClassName } from 'components/ui/InlineLink' import { GenericSkeletonLoader } from 'components/ui/ShimmeringLoader' import UpgradeToPro from 'components/ui/UpgradeToPro' import { useBranchCreateMutation } from 'data/branches/branch-create-mutation' import { useBranchesQuery } from 'data/branches/branches-query' +import { useDiskAttributesQuery } from 'data/config/disk-attributes-query' import { useCheckGithubBranchValidity } from 'data/integrations/github-branch-check-query' import { useGitHubConnectionsQuery } from 'data/integrations/github-connections-query' -import { useCloneBackupsQuery } from 'data/projects/clone-query' import { projectKeys } from 'data/projects/keys' +import { DesiredInstanceSize, instanceSizeSpecs } from 'data/projects/new-project.constants' import { useProjectAddonsQuery } from 'data/subscriptions/project-addons-query' import { useSendEventMutation } from 'data/telemetry/send-event-mutation' import { useAsyncCheckProjectPermissions } from 'hooks/misc/useCheckPermissions' @@ -46,9 +48,18 @@ import { Input_Shadcn_, Label_Shadcn_ as Label, Switch, + Tooltip, + TooltipContent, + TooltipTrigger, cn, } from 'ui' import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout' +import ShimmeringLoader from 'ui-patterns/ShimmeringLoader' +import { + estimateComputeSize, + estimateDiskCost, + estimateRestoreTime, +} from './BranchManagement.utils' export const CreateBranchModal = () => { const { ref } = useParams() @@ -60,90 +71,19 @@ export const CreateBranchModal = () => { const gitlessBranching = useIsBranching2Enabled() const allowDataBranching = useFlag('allowDataBranching') - // [Joshen] This is meant to be short lived while we're figuring out how to control - // requests to this endpoint. Kill switch in case we need to stop the requests - const disableBackupsCheck = useFlag('disableBackupsCheckInCreatebranchmodal') - - const isProPlanAndUp = selectedOrg?.plan?.id !== 'free' - const promptProPlanUpgrade = IS_PLATFORM && !isProPlanAndUp - - const isBranch = projectDetails?.parent_project_ref !== undefined - const projectRef = - projectDetails !== undefined ? (isBranch ? projectDetails.parent_project_ref : ref) : undefined - - const { - data: connections, - error: connectionsError, - isLoading: isLoadingConnections, - isSuccess: isSuccessConnections, - isError: isErrorConnections, - } = useGitHubConnectionsQuery({ - organizationId: selectedOrg?.id, - }) - - const { data: branches } = useBranchesQuery({ projectRef }) - const { data: addons } = useProjectAddonsQuery({ projectRef }) - const hasPitrEnabled = - (addons?.selected_addons ?? []).find((addon) => addon.type === 'pitr') !== undefined - const { mutateAsync: checkGithubBranchValidity, isLoading: isChecking } = - useCheckGithubBranchValidity({ - onError: () => {}, - }) - const { - data: cloneBackups, - error: cloneBackupsError, - isLoading: isLoadingCloneBackups, - } = useCloneBackupsQuery( - { projectRef }, - { - // [Joshen] Only trigger this request when the modal is opened - enabled: showCreateBranchModal && !disableBackupsCheck, - } - ) - const targetVolumeSizeGb = cloneBackups?.target_volume_size_gb ?? 0 - const noPhysicalBackups = cloneBackupsError?.message.startsWith( - 'Physical backups need to be enabled' - ) - - const { mutate: sendEvent } = useSendEventMutation() - - const { mutate: createBranch, isLoading: isCreating } = useBranchCreateMutation({ - onSuccess: async (data) => { - toast.success(`Successfully created preview branch "${data.name}"`) - if (projectRef) { - await Promise.all([queryClient.invalidateQueries(projectKeys.detail(projectRef))]) - } - sendEvent({ - action: 'branch_create_button_clicked', - properties: { - branchType: data.persistent ? 'persistent' : 'preview', - gitlessBranching, - }, - groups: { - project: ref ?? 'Unknown', - organization: selectedOrg?.slug ?? 'Unknown', - }, - }) - - setShowCreateBranchModal(false) - router.push(`/project/${data.project_ref}`) - }, - onError: (error) => { - toast.error(`Failed to create branch: ${error.message}`) - }, - }) const { can: canCreateBranch } = useAsyncCheckProjectPermissions( PermissionAction.CREATE, 'preview_branches' ) - const githubConnection = connections?.find((connection) => connection.project.ref === projectRef) - - // Fetch production/default branch to inspect git_branch linkage - const prodBranch = branches?.find((branch) => branch.is_default) + const isProPlanAndUp = selectedOrg?.plan?.id !== 'free' + const promptProPlanUpgrade = IS_PLATFORM && !isProPlanAndUp - const [repoOwner, repoName] = githubConnection?.repository.name.split('/') ?? [] + const isBranch = projectDetails?.parent_project_ref !== undefined + const projectRef = + projectDetails !== undefined ? (isBranch ? projectDetails.parent_project_ref : ref) : undefined + const noPhysicalBackups = !projectDetails?.is_physical_backups_enabled const formId = 'create-branch-form' const FormSchema = z @@ -193,15 +133,95 @@ export const CreateBranchModal = () => { }) const withData = form.watch('withData') - const canSubmit = !isCreating && !isChecking + const { + data: connections, + error: connectionsError, + isLoading: isLoadingConnections, + isSuccess: isSuccessConnections, + isError: isErrorConnections, + } = useGitHubConnectionsQuery( + { organizationId: selectedOrg?.id }, + { enabled: showCreateBranchModal } + ) + + const { data: branches } = useBranchesQuery({ projectRef }) + const { data: addons, isSuccess: isSuccessAddons } = useProjectAddonsQuery( + { projectRef }, + { enabled: showCreateBranchModal } + ) + const computeAddon = addons?.selected_addons.find((addon) => addon.type === 'compute_instance') + const computeSize = !!computeAddon + ? (computeAddon.variant.identifier.split('ci_')[1] as DesiredInstanceSize) + : undefined + const hasPitrEnabled = + (addons?.selected_addons ?? []).find((addon) => addon.type === 'pitr') !== undefined + + const { + data: disk, + isLoading: isLoadingDiskAttr, + isError: isErrorDiskAttr, + } = useDiskAttributesQuery({ projectRef }, { enabled: showCreateBranchModal && withData }) + const projectDiskAttributes = disk?.attributes ?? { + type: 'gp3', + size_gb: 0, + iops: 0, + throughput_mbps: 0, + } + // Branch disk is oversized to include backup files, it should be scaled back eventually. + const branchDiskAttributes = { + ...projectDiskAttributes, + // [Joshen] JFYI for Qiao - this multiplier may eventually be dropped + size_gb: Math.round(projectDiskAttributes.size_gb * 1.5), + } + const branchComputeSize = estimateComputeSize(projectDiskAttributes.size_gb, computeSize) + const estimatedDiskCost = estimateDiskCost(branchDiskAttributes) + + const { mutate: sendEvent } = useSendEventMutation() + + const { mutateAsync: checkGithubBranchValidity, isLoading: isCheckingGHBranchValidity } = + useCheckGithubBranchValidity({ + onError: () => {}, + }) + + const { mutate: createBranch, isLoading: isCreatingBranch } = useBranchCreateMutation({ + onSuccess: async (data) => { + toast.success(`Successfully created preview branch "${data.name}"`) + if (projectRef) { + await Promise.all([queryClient.invalidateQueries(projectKeys.detail(projectRef))]) + } + sendEvent({ + action: 'branch_create_button_clicked', + properties: { + branchType: data.persistent ? 'persistent' : 'preview', + gitlessBranching, + }, + groups: { + project: ref ?? 'Unknown', + organization: selectedOrg?.slug ?? 'Unknown', + }, + }) + + setShowCreateBranchModal(false) + router.push(`/project/${data.project_ref}`) + }, + onError: (error) => { + toast.error(`Failed to create branch: ${error.message}`) + }, + }) + + // Fetch production/default branch to inspect git_branch linkage + const githubConnection = connections?.find((connection) => connection.project.ref === projectRef) + const prodBranch = branches?.find((branch) => branch.is_default) + const [repoOwner, repoName] = githubConnection?.repository.name.split('/') ?? [] + const isDisabled = + !canCreateBranch || + !isSuccessAddons || !isSuccessConnections || - isCreating || - !canSubmit || - isChecking || - (!gitlessBranching && !githubConnection) || promptProPlanUpgrade || - !canCreateBranch + (!gitlessBranching && !githubConnection) || + isCreatingBranch || + isCheckingGHBranchValidity const onSubmit = (data: z.infer) => { if (!projectRef) return console.error('Project ref is required') @@ -209,6 +229,7 @@ export const CreateBranchModal = () => { projectRef, branchName: data.branchName, is_default: false, + ...(data.withData ? { desired_instance_size: computeSize } : {}), ...(data.gitBranchName ? { gitBranch: data.gitBranchName } : {}), ...(allowDataBranching ? { withData: data.withData } : {}), }) @@ -231,9 +252,7 @@ export const CreateBranchModal = () => { size="large" hideClose onOpenAutoFocus={(e) => { - if (promptProPlanUpgrade) { - e.preventDefault() - } + if (promptProPlanUpgrade) e.preventDefault() }} > @@ -315,7 +334,9 @@ export const CreateBranchModal = () => { />
- {isChecking && } + {isCheckingGHBranchValidity && ( + + )}
@@ -363,7 +384,7 @@ export const CreateBranchModal = () => { label={ <> - {!disableBackupsCheck && (isLoadingCloneBackups || noPhysicalBackups) && ( + {noPhysicalBackups && ( Requires PITR @@ -376,9 +397,7 @@ export const CreateBranchModal = () => { > @@ -406,18 +425,91 @@ export const CreateBranchModal = () => {
-

- Data branch takes longer time to create -

-

- Since your target database volume size is{' '} - {targetVolumeSizeGb} GB, creating a - data branch is estimated to take around{' '} - - {Math.round((720 / 21000) * targetVolumeSizeGb) + 3} minutes - - . -

+ {isLoadingDiskAttr ? ( + <> + + + + ) : ( + <> + {isErrorDiskAttr ? ( + <> +

+ Branch disk size will incur additional cost per month +

+

+ The additional cost and time taken to create a data branch is relative + to the size of your database. We are unable to provide an estimate as + we were unable to retrieve your project's disk configuration +

+ + ) : ( + <> +

+ Branch disk size is billed at ${estimatedDiskCost.total.toFixed(2)}{' '} + per month +

+

+ Creating a data branch will take about{' '} + + {estimateRestoreTime(branchDiskAttributes).toFixed()} minutes + {' '} + and costs{' '} + + ${estimatedDiskCost.total.toFixed(2)} + {' '} + per month based on your current target database volume size of{' '} + {branchDiskAttributes.size_gb} GB and your{' '} + + + + project's disk configuration + + + +

+

Disk type:

+

+ {branchDiskAttributes.type.toUpperCase()} +

+
+
+

Targer disk size:

+

{branchDiskAttributes.size_gb} GB

+

(${estimatedDiskCost.size.toFixed(2)})

+
+
+

IOPs:

+

{branchDiskAttributes.iops} IOPS

+

(${estimatedDiskCost.iops.toFixed(2)})

+
+ {'throughput_mbps' in branchDiskAttributes && ( +
+

Throughput:

+

+ {branchDiskAttributes.throughput_mbps} MB/s +

+

(${estimatedDiskCost.throughput.toFixed(2)})

+
+ )} +

+ More info in{' '} + setShowCreateBranchModal(false)} + className="pointer-events-auto" + href={`/project/${ref}/settings/compute-and-disk`} + > + Compute and Disk + +

+ + + . +

+ + )} + + )}
)} @@ -439,7 +531,7 @@ export const CreateBranchModal = () => { {prodBranch?.git_branch ? ( <> When this branch is merged to{' '} - {prodBranch.git_branch}, + {prodBranch.git_branch}, migrations will be deployed to production. Otherwise, migrations only run on preview branches. @@ -462,9 +554,21 @@ export const CreateBranchModal = () => {
-

Branches are billed $0.01344 per hour

+

+ Branch compute is billed at $ + {withData ? branchComputeSize.priceHourly : instanceSizeSpecs.micro.priceHourly}{' '} + per hour +

- This cost will continue for as long as the branch has not been removed. + {withData ? ( + <> + {branchComputeSize.label} compute + size is automatically selected to match your production branch. You may + downgrade after creation or pause the branch when not in use to save cost. + + ) : ( + <>This cost will continue for as long as the branch has not been removed. + )}

@@ -474,8 +578,8 @@ export const CreateBranchModal = () => { )} diff --git a/apps/studio/components/interfaces/SignIn/SignInWithCustom.tsx b/apps/studio/components/interfaces/SignIn/SignInWithCustom.tsx new file mode 100644 index 0000000000000..a51b1a25a0da6 --- /dev/null +++ b/apps/studio/components/interfaces/SignIn/SignInWithCustom.tsx @@ -0,0 +1,48 @@ +import * as Sentry from '@sentry/nextjs' +import { useState } from 'react' +import { toast } from 'sonner' + +import { BASE_PATH } from 'lib/constants' +import { auth, buildPathWithParams } from 'lib/gotrue' +import { Button } from 'ui' + +interface SignInWithCustomProps { + providerName: string +} + +export const SignInWithCustom = ({ providerName }: SignInWithCustomProps) => { + const [loading, setLoading] = useState(false) + + async function handleCustomSignIn() { + setLoading(true) + + try { + // redirects to /sign-in to check if the user has MFA setup (handled in SignInLayout.tsx) + const redirectTo = buildPathWithParams( + `${ + process.env.NEXT_PUBLIC_VERCEL_ENV === 'preview' + ? location.origin + : process.env.NEXT_PUBLIC_SITE_URL + }${BASE_PATH}/sign-in-mfa?method=${providerName.toLowerCase()}` + ) + + const { error } = await auth.signInWithOAuth({ + // @ts-expect-error - providerName is a string + provider: providerName.toLowerCase(), + options: { redirectTo }, + }) + + if (error) throw error + } catch (error: any) { + toast.error(`Failed to sign in via ${providerName}: ${error.message}`) + Sentry.captureMessage('[CRITICAL] Failed to sign in via GH: ' + error.message) + setLoading(false) + } + } + + return ( + + ) +} diff --git a/apps/studio/components/ui/AIAssistantPanel/Message.tsx b/apps/studio/components/ui/AIAssistantPanel/Message.tsx index a692bade1e0cf..b06015320b5cd 100644 --- a/apps/studio/components/ui/AIAssistantPanel/Message.tsx +++ b/apps/studio/components/ui/AIAssistantPanel/Message.tsx @@ -165,6 +165,7 @@ const Message = function Message({ return ( , 'children'> & { isStreaming?: boolean children: string + showReasoning?: boolean } -export const Reasoning = memo(({ className, isStreaming, children, ...props }: ReasoningProps) => ( - - ( + - {isStreaming ? ( - <> - -

Thinking...

- - ) : ( - <> - -

Reasoned

- - )} - -
+ + {isStreaming ? ( + <> + +

Thinking...

+ + ) : ( + <> + +

Reasoned

+ + )} + {showReasoning && ( + + )} +
- - {children} - -
-)) + + {children} + + + ) +) Reasoning.displayName = 'Reasoning' diff --git a/apps/studio/components/ui/UnknownInterface.tsx b/apps/studio/components/ui/UnknownInterface.tsx index d53ef6c7aaff4..0baf1c5a4015c 100644 --- a/apps/studio/components/ui/UnknownInterface.tsx +++ b/apps/studio/components/ui/UnknownInterface.tsx @@ -1,11 +1,17 @@ import Link from 'next/link' -import { Button } from 'ui' +import { Button, cn } from 'ui' import { Admonition } from 'ui-patterns' -export const UnknownInterface = ({ urlBack }: { urlBack: string }) => { +export const UnknownInterface = ({ + urlBack, + fullHeight = true, +}: { + urlBack: string + fullHeight?: boolean +}) => { return ( -
+
export async function createBranch({ - is_default, projectRef, + is_default, branchName, gitBranch, region, withData, + desired_instance_size, }: BranchCreateVariables) { const { data, error } = await post('/v1/projects/{ref}/branches', { params: { @@ -30,8 +31,9 @@ export async function createBranch({ is_default, branch_name: branchName, git_branch: gitBranch, - region: region, + region, with_data: withData, + desired_instance_size, }, }) diff --git a/apps/studio/hooks/custom-content/CustomContent.types.ts b/apps/studio/hooks/custom-content/CustomContent.types.ts index 28568d054b327..eb4729847dd42 100644 --- a/apps/studio/hooks/custom-content/CustomContent.types.ts +++ b/apps/studio/hooks/custom-content/CustomContent.types.ts @@ -2,6 +2,8 @@ import { CONNECTION_TYPES } from 'components/interfaces/Connect/Connect.constant import type { CloudProvider } from 'shared-data' export type CustomContentTypes = { + dashboardAuthCustomProvider: string + organizationLegalDocuments: { id: string name: string @@ -29,4 +31,6 @@ export type CustomContentTypes = { connectFrameworks: (typeof CONNECTION_TYPES)[number] infraCloudProviders: CloudProvider[] + + sslCertificateUrl: string } diff --git a/apps/studio/hooks/custom-content/custom-content.json b/apps/studio/hooks/custom-content/custom-content.json index 57bd6db86b714..b7e6332bd3dbc 100644 --- a/apps/studio/hooks/custom-content/custom-content.json +++ b/apps/studio/hooks/custom-content/custom-content.json @@ -1,6 +1,8 @@ { "$schema": "./custom-content.schema.json", + "dashboard_auth:custom_provider": null, + "organization:legal_documents": null, "project_homepage:example_projects": null, @@ -9,5 +11,7 @@ "connect:frameworks": null, - "infra:cloud_providers": ["AWS", "AWS_K8S", "FLY"] + "infra:cloud_providers": ["AWS", "AWS_K8S", "FLY"], + + "ssl:certificate_url": "https://supabase-downloads.s3-ap-southeast-1.amazonaws.com/${env}/ssl/${env}-ca-2021.crt" } diff --git a/apps/studio/hooks/custom-content/custom-content.sample.json b/apps/studio/hooks/custom-content/custom-content.sample.json index da5a880f342ee..d19b34e4cdc04 100644 --- a/apps/studio/hooks/custom-content/custom-content.sample.json +++ b/apps/studio/hooks/custom-content/custom-content.sample.json @@ -1,6 +1,8 @@ { "$schema": "./custom-content.schema.json", + "dashboard_auth:custom_provider": "Nimbus", + "organization:legal_documents": [ { "id": "doc1", @@ -74,5 +76,7 @@ ] }, - "infra:cloud_providers": ["AWS_NIMBUS"] + "infra:cloud_providers": ["AWS_NIMBUS"], + + "ssl:certificate_url": "https://supabase-downloads.s3-ap-southeast-1.amazonaws.com/${env}/ssl/${env}-ca-2021.crt" } diff --git a/apps/studio/hooks/custom-content/custom-content.schema.json b/apps/studio/hooks/custom-content/custom-content.schema.json index adea1a5f9feec..b478095fd0ff6 100644 --- a/apps/studio/hooks/custom-content/custom-content.schema.json +++ b/apps/studio/hooks/custom-content/custom-content.schema.json @@ -6,6 +6,11 @@ "type": "string" }, + "dashboard_auth:custom_provider": { + "type": ["string", "null"], + "description": "Show a custom provider on the sign in page (Continue with X)" + }, + "organization:legal_documents": { "type": ["array", "null"], "description": "Renders a provided set of documents under the organization legal documents page", @@ -81,6 +86,11 @@ "type": "string", "enum": ["AWS", "AWS_K8S", "AWS_NIMBUS", "FLY"] } + }, + + "ssl:certificate_url": { + "type": "string", + "description": "The URL of the SSL certificate" } }, "required": [ @@ -88,7 +98,8 @@ "project_homepage:example_projects", "logs:default_query", "connect:frameworks", - "infra:cloud_providers" + "infra:cloud_providers", + "ssl:certificate_url" ], "additionalProperties": false } diff --git a/apps/studio/lib/ai/bedrock.ts b/apps/studio/lib/ai/bedrock.ts index 857808726e920..e080768621eee 100644 --- a/apps/studio/lib/ai/bedrock.ts +++ b/apps/studio/lib/ai/bedrock.ts @@ -4,6 +4,7 @@ import { createCredentialChain, fromNodeProviderChain } from '@aws-sdk/credentia import { CredentialsProviderError } from '@smithy/property-provider' import { awsCredentialsProvider } from '@vercel/functions/oidc' import { selectWeightedKey } from './util' +import { BedrockModel } from './model.utils' const credentialProvider = createCredentialChain( // Vercel OIDC provider will be used for staging/production @@ -61,8 +62,6 @@ export const regionPrefixMap: Record = { euc1: 'eu', } -export type BedrockModel = 'anthropic.claude-3-7-sonnet-20250219-v1:0' | 'openai.gpt-oss-120b-1:0' - export type RegionWeights = Record /** @@ -91,10 +90,8 @@ const modelRegionWeights: Record = { * Used to load balance requests across multiple regions depending on * their capacities. */ -export function createRoutedBedrock(routingKey?: string, useOpenAI = false) { - return async ( - modelId: BedrockModel - ): Promise<{ model: LanguageModel; supportsCachePoint: boolean }> => { +export function createRoutedBedrock(routingKey?: string) { + return async (modelId: BedrockModel): Promise => { const regionWeights = modelRegionWeights[modelId] // Select the Bedrock region based on the routing key and the model @@ -116,7 +113,6 @@ export function createRoutedBedrock(routingKey?: string, useOpenAI = false) { const modelName = activeRegions > 1 ? `${regionPrefixMap[bedrockRegion]}.${modelId}` : modelId const model = bedrock(modelName) - const supportsCachePoint = modelId === 'anthropic.claude-3-7-sonnet-20250219-v1:0' - return { model, supportsCachePoint } + return model } } diff --git a/apps/studio/lib/ai/model.test.ts b/apps/studio/lib/ai/model.test.ts index 59986528989e1..7b5c58a4ae35f 100644 --- a/apps/studio/lib/ai/model.test.ts +++ b/apps/studio/lib/ai/model.test.ts @@ -4,15 +4,12 @@ import * as bedrockModule from './bedrock' import { getModel, ModelErrorMessage } from './model' vi.mock('@ai-sdk/openai', () => ({ - openai: vi.fn(() => 'openai-model'), + openai: { chat: vi.fn(() => 'openai-model') }, })) vi.mock('./bedrock', async () => ({ ...(await vi.importActual('./bedrock')), - createRoutedBedrock: vi.fn(() => async (modelId: string) => ({ - model: 'bedrock-model', - supportsCachePoint: modelId === 'anthropic.claude-3-7-sonnet-20250219-v1:0', - })), + createRoutedBedrock: vi.fn(() => async (modelId: string) => 'bedrock-model'), checkAwsCredentials: vi.fn(), })) @@ -28,25 +25,35 @@ describe('getModel', () => { process.env = { ...originalEnv } }) - it('should return bedrock model when AWS credentials are available and not throttled', async () => { + it('should return bedrock model and promptProviderOptions when default supports caching', async () => { vi.mocked(bedrockModule.checkAwsCredentials).mockResolvedValue(true) vi.stubEnv('IS_THROTTLED', 'false') - const { model, error, supportsCachePoint } = await getModel() + const { model, error, promptProviderOptions } = await getModel({ + routingKey: 'test', + isLimited: false, + }) expect(model).toEqual('bedrock-model') - expect(supportsCachePoint).toBe(true) + // Default bedrock model supportsCaching=false in registry, but if caller + // specifies high-tier, provider options would be present + expect(promptProviderOptions === undefined || typeof promptProviderOptions === 'object').toBe( + true + ) expect(error).toBeUndefined() }) - it('should return bedrock model when AWS credentials are available and throttled', async () => { + it('should return bedrock model when throttled (limited) with default model', async () => { vi.mocked(bedrockModule.checkAwsCredentials).mockResolvedValue(true) vi.stubEnv('IS_THROTTLED', 'true') - const { model, error, supportsCachePoint } = await getModel() + const { model, error, promptProviderOptions } = await getModel({ + routingKey: 'test', + isLimited: true, + }) expect(model).toEqual('bedrock-model') - expect(supportsCachePoint).toBe(false) + expect(promptProviderOptions).toBeUndefined() expect(error).toBeUndefined() }) @@ -54,19 +61,39 @@ describe('getModel', () => { vi.mocked(bedrockModule.checkAwsCredentials).mockResolvedValue(false) process.env.OPENAI_API_KEY = 'test-key' - const { model, supportsCachePoint } = await getModel() + const { model, promptProviderOptions } = await getModel({ + routingKey: 'test', + isLimited: false, + }) expect(model).toEqual('openai-model') - expect(openai).toHaveBeenCalledWith('gpt-4.1-2025-04-14') - expect(supportsCachePoint).toBe(false) + // Default openai model in registry is gpt-5-mini + expect(openai.chat).toHaveBeenCalledWith('gpt-5-mini') + expect(promptProviderOptions).toBeUndefined() }) it('should return error when neither AWS credentials nor OPENAI_API_KEY is available', async () => { vi.mocked(bedrockModule.checkAwsCredentials).mockResolvedValue(false) delete process.env.OPENAI_API_KEY - const { error } = await getModel('test-key') - + const { error } = await getModel({ routingKey: 'test-key', isLimited: false }) expect(error).toEqual(new Error(ModelErrorMessage)) }) + + it('returns specified provider and model when provided (openai gpt-5)', async () => { + vi.mocked(bedrockModule.checkAwsCredentials).mockResolvedValue(false) + process.env.OPENAI_API_KEY = 'test-key' + process.env.IS_THROTTLED = 'false' + + const { model, error } = await getModel({ + provider: 'openai', + model: 'gpt-5', + routingKey: 'rk', + isLimited: false, + }) + + expect(error).toBeUndefined() + expect(model).toEqual('openai-model') + expect(openai.chat).toHaveBeenCalledWith('gpt-5') + }) }) diff --git a/apps/studio/lib/ai/model.ts b/apps/studio/lib/ai/model.ts index 1cf7a56fa7fbd..0bb966f48b862 100644 --- a/apps/studio/lib/ai/model.ts +++ b/apps/studio/lib/ai/model.ts @@ -1,55 +1,110 @@ import { openai } from '@ai-sdk/openai' import { LanguageModel } from 'ai' import { checkAwsCredentials, createRoutedBedrock } from './bedrock' +import { + BedrockModel, + Model, + OpenAIModel, + PROVIDERS, + ProviderModelConfig, + ProviderName, + getDefaultModelForProvider, +} from './model.utils' -const BEDROCK_PRO_MODEL = 'anthropic.claude-3-7-sonnet-20250219-v1:0' -const BEDROCK_NORMAL_MODEL = 'openai.gpt-oss-120b-1:0' -const OPENAI_MODEL = 'gpt-4.1-2025-04-14' +type PromptProviderOptions = Record +type ProviderOptions = Record -export type ModelSuccess = { +type ModelSuccess = { model: LanguageModel - supportsCachePoint: boolean + promptProviderOptions?: PromptProviderOptions + providerOptions?: ProviderOptions error?: never } export type ModelError = { model?: never - supportsCachePoint?: never + promptProviderOptions?: never + providerOptions?: never error: Error } -export type ModelResponse = ModelSuccess | ModelError +type ModelResponse = ModelSuccess | ModelError -export const ModelErrorMessage = - 'No valid AI model available. Please set up a local AWS profile to use Bedrock, or pass an OPENAI_API_KEY to use OpenAI.' +export const ModelErrorMessage = 'No valid AI model available based on available credentials.' + +export type GetModelParams = { + provider?: ProviderName + model?: Model + routingKey: string + isLimited?: boolean +} /** - * Retrieves the appropriate AI model based on available credentials. - * - * An optional routing key can be provided to distribute requests across - * different Bedrock regions. + * Retrieves a LanguageModel from a specific provider and model. + * - If provider/model not specified, auto-selects based on available credentials (prefers Bedrock). + * - If isLimited is true, uses the provider's default model. + * - Returns promptProviderOptions that callers can attach to the system message. */ -export async function getModel(routingKey?: string, isLimited?: boolean): Promise { - // Default behaviour here is to be throttled (e.g if this env var is not available, isThrottled should be true, unless specified 'false') - const isThrottled = process.env.IS_THROTTLED !== 'false' +export async function getModel({ + provider, + model, + routingKey, + isLimited = true, +}: GetModelParams): Promise { + const envThrottled = process.env.IS_THROTTLED !== 'false' - const hasAwsCredentials = await checkAwsCredentials() + let preferredProvider: ProviderName | undefined = provider + const hasAwsCredentials = await checkAwsCredentials() const hasAwsBedrockRoleArn = !!process.env.AWS_BEDROCK_ROLE_ARN const hasOpenAIKey = !!process.env.OPENAI_API_KEY - if (hasAwsBedrockRoleArn && hasAwsCredentials) { - const bedrockModel = isThrottled || isLimited ? BEDROCK_NORMAL_MODEL : BEDROCK_PRO_MODEL - const bedrock = createRoutedBedrock(routingKey) - const { model, supportsCachePoint } = await bedrock(bedrockModel) + // Auto-pick a provider if not specified defaulting to Bedrock + if (!preferredProvider) { + if (hasAwsBedrockRoleArn && hasAwsCredentials) { + preferredProvider = 'bedrock' + } else if (hasOpenAIKey) { + preferredProvider = 'openai' + } + } + + if (!preferredProvider) { + return { error: new Error(ModelErrorMessage) } + } + + const providerRegistry = PROVIDERS[preferredProvider] + if (!providerRegistry) { + return { error: new Error(`Unknown provider: ${preferredProvider}`) } + } + + const models = providerRegistry.models as Record - return { model, supportsCachePoint } + const useDefault = isLimited || envThrottled || !model || !models[model] + + const chosenModelId = useDefault ? getDefaultModelForProvider(preferredProvider) : model + + if (preferredProvider === 'bedrock') { + if (!hasAwsBedrockRoleArn || !hasAwsCredentials) { + return { error: new Error('AWS Bedrock credentials not available') } + } + const bedrock = createRoutedBedrock(routingKey) + const model = await bedrock(chosenModelId as BedrockModel) + const promptProviderOptions = ( + providerRegistry.models as Record + )[chosenModelId as BedrockModel]?.promptProviderOptions + return { model, promptProviderOptions } } - // [Joshen] Only for local/self-hosted, hosted should always only use bedrock - if (hasOpenAIKey) { - return { model: openai(OPENAI_MODEL), supportsCachePoint: false } + if (preferredProvider === 'openai') { + if (!hasOpenAIKey) { + return { error: new Error('OPENAI_API_KEY not available') } + } + return { + model: openai.chat(chosenModelId as OpenAIModel), + promptProviderOptions: models[chosenModelId as OpenAIModel]?.promptProviderOptions, + providerOptions: providerRegistry.providerOptions, + } } - return { error: new Error(ModelErrorMessage) } + return { error: new Error(`Unsupported provider: ${preferredProvider}`) } } diff --git a/apps/studio/lib/ai/model.utils.ts b/apps/studio/lib/ai/model.utils.ts new file mode 100644 index 0000000000000..d876d926b428f --- /dev/null +++ b/apps/studio/lib/ai/model.utils.ts @@ -0,0 +1,75 @@ +export type ProviderName = 'bedrock' | 'openai' | 'anthropic' + +export type BedrockModel = 'anthropic.claude-3-7-sonnet-20250219-v1:0' | 'openai.gpt-oss-120b-1:0' + +export type OpenAIModel = 'gpt-5' | 'gpt-5-mini' + +export type AnthropicModel = 'claude-sonnet-4-20250514' | 'claude-3-5-haiku-20241022' + +export type Model = BedrockModel | OpenAIModel | AnthropicModel + +export type ProviderModelConfig = { + /** Optional providerOptions to attach to the system message for this model */ + promptProviderOptions?: Record + /** The default model for this provider (used when limited or no preferred specified) */ + default: boolean +} + +export type ProviderRegistry = { + bedrock: { + models: Record + providerOptions?: Record + } + openai: { + models: Record + providerOptions?: Record + } + anthropic: { + models: Record + providerOptions?: Record + } +} + +export const PROVIDERS: ProviderRegistry = { + bedrock: { + models: { + 'anthropic.claude-3-7-sonnet-20250219-v1:0': { + promptProviderOptions: { + bedrock: { + // Always cache the system prompt (must not contain dynamic content) + cachePoint: { type: 'default' }, + }, + }, + default: false, + }, + 'openai.gpt-oss-120b-1:0': { + default: true, + }, + }, + }, + openai: { + models: { + 'gpt-5': { default: false }, + 'gpt-5-mini': { default: true }, + }, + providerOptions: { + openai: { + reasoningEffort: 'low', + textVerbosity: 'low', + }, + }, + }, + anthropic: { + models: { + 'claude-sonnet-4-20250514': { default: false }, + 'claude-3-5-haiku-20241022': { default: true }, + }, + }, +} + +export function getDefaultModelForProvider(provider: ProviderName): Model | undefined { + const models = PROVIDERS[provider]?.models as Record + if (!models) return undefined + + return Object.keys(models).find((id) => models[id as Model]?.default) as Model | undefined +} diff --git a/apps/studio/lib/ai/prompts.ts b/apps/studio/lib/ai/prompts.ts index 0a9176265b505..b8c023ab02191 100644 --- a/apps/studio/lib/ai/prompts.ts +++ b/apps/studio/lib/ai/prompts.ts @@ -1,695 +1,122 @@ export const RLS_PROMPT = ` -# RLS Guide -## Overview +Developer: # PostgreSQL RLS in Supabase: Condensed Guide -Row Level Security (RLS) is a PostgreSQL security feature that enables fine-grained access control by restricting which rows users can access in tables based on defined security policies. In Supabase, RLS works seamlessly with Supabase Auth, automatically appending WHERE clauses to SQL queries and filtering data at the database level without requiring application-level changes. +## What is RLS? +Row Level Security (RLS) restricts table rows visible per user via security policies. In Supabase, with RLS enabled, policies filter rows automatically—no app code changes required. RLS plus Supabase Auth means WHERE clauses are injected based on the user's identity or JWT claims. -## Core RLS Concepts +## Core Concepts +- **Enable RLS**: Default for Supabase Dashboard tables; enable with \`ALTER TABLE table_name ENABLE ROW LEVEL SECURITY;\` for SQL-created tables. +- **Default Behavior**: All access denied (except table owner/superuser) until a policy is defined. -### Enabling RLS - -RLS is enabled by default on tables created through the Supabase Dashboard[1]. For tables created via SQL, enable RLS manually: +### Policy Types +- **SELECT**: Use \`USING\` to filter visible rows. +- **INSERT**: Use \`WITH CHECK\` to limit new rows. +- **UPDATE**: Use both \`USING\` (read existing) & \`WITH CHECK\` (restrict changes). +- **DELETE**: Use \`USING\` to allow deletion. +- Policies can also be created for **ALL**. +### Syntax \`\`\`sql -ALTER TABLE table_name ENABLE ROW LEVEL SECURITY; +CREATE POLICY name ON table + [FOR { ALL | SELECT | INSERT | UPDATE | DELETE }] + [TO {role|PUBLIC|CURRENT_USER}] + [USING (expr)] + [WITH CHECK (expr)]; \`\`\` -By default, enabling RLS denies all access to non-superusers and table owners until policies are created[1]. - -### Policy Types and Operations - -RLS policies can be created for specific SQL operations: - -- **SELECT**: Uses \`USING\` clause to filter visible rows -- **INSERT**: Uses \`WITH CHECK\` clause to validate new rows -- **UPDATE**: Uses both \`USING\` (for existing rows) and \`WITH CHECK\` (for modified rows) -- **DELETE**: Uses \`USING\` clause to determine deletable rows -- **ALL**: Applies to all operations +## Supabase Auth Functions +- \`auth.uid()\`: Current user's UUID (for direct user access control). +- \`auth.jwt()\`: Full JWT token (access custom claims, e.g. tenant or role). -### Basic Policy Syntax +## Supabase Built-In Roles +- \`anon\`: public/unauthenticated +- \`authenticated\`: logged in users +- \`service_role\`: full access; bypasses RLS +## RLS Patterns in Supabase +### User Ownership (Single-Tenant) \`\`\`sql -CREATE POLICY policy_name ON table_name - [FOR {ALL | SELECT | INSERT | UPDATE | DELETE}] - [TO {role_name | PUBLIC | CURRENT_USER}] - [USING (using_expression)] - [WITH CHECK (check_expression)]; +-- Users access their own data +grant select, insert, update, delete on user_documents to authenticated; +CREATE POLICY "User view" ON user_documents FOR SELECT TO authenticated USING ((SELECT auth.uid()) = user_id); +CREATE POLICY "User insert" ON user_documents FOR INSERT TO authenticated WITH CHECK ((SELECT auth.uid()) = user_id); +CREATE POLICY "User update" ON user_documents FOR UPDATE TO authenticated USING ((SELECT auth.uid()) = user_id) WITH CHECK ((SELECT auth.uid()) = user_id); +CREATE POLICY "User delete" ON user_documents FOR DELETE TO authenticated USING ((SELECT auth.uid()) = user_id); \`\`\` -## Supabase-Specific Auth Functions - -### Core Auth Functions - -**\`auth.uid()\`**: Returns the UUID of the currently authenticated user[1][2]. This is the primary function for user-based access control: - +### Multi-Tenant & Organization Isolation \`\`\`sql -CREATE POLICY "Users can view their own todos" -ON todos FOR SELECT -USING ((SELECT auth.uid()) = user_id); +-- Tenant from JWT claim +CREATE POLICY "Tenant access" ON customers FOR SELECT TO authenticated USING (tenant_id = (auth.jwt() ->> 'tenant_id')::uuid); +-- Organization via join +grant select on projects to authenticated; +CREATE POLICY "Org member access" ON projects FOR SELECT TO authenticated USING (organization_id IN (SELECT organization_id FROM user_organizations WHERE user_id = (SELECT auth.uid()))); \`\`\` -**\`auth.jwt()\`**: Returns the complete JWT token of the authenticated user[2][3]. Use this to access custom claims or other JWT data: - +### Role-Based Access \`\`\`sql -CREATE POLICY "Admin access only" -ON sensitive_table FOR ALL -USING ((auth.jwt() ->> 'user_role') = 'admin'); +-- Custom roles from JWT +CREATE POLICY "Admin view" ON sensitive_data FOR SELECT TO authenticated USING ((auth.jwt() ->> 'user_role') = 'admin'); +-- Multi-role support +CREATE POLICY "Multi-role access" ON documents FOR SELECT TO authenticated USING ((auth.jwt() ->> 'user_role') = ANY(ARRAY['admin','editor','viewer'])); \`\`\` -### Authentication Roles - -Supabase maps every request to specific database roles[1][4]: - -- **\`anon\`**: Unauthenticated users (public access) -- **\`authenticated\`**: Authenticated users -- **\`service_role\`**: Elevated access that bypasses RLS - -## RLS Implementation Patterns for Supabase - -### 1. User-Based Access Control - -**Basic user ownership pattern:** +### Conditional/Time-Based Access \`\`\`sql -CREATE POLICY "Users can view own data" ON user_documents -FOR SELECT TO authenticated -USING ((SELECT auth.uid()) = user_id); - -CREATE POLICY "Users can insert own data" ON user_documents -FOR INSERT TO authenticated -WITH CHECK ((SELECT auth.uid()) = user_id); - -CREATE POLICY "Users can update own data" ON user_documents -FOR UPDATE TO authenticated -USING ((SELECT auth.uid()) = user_id) -WITH CHECK ((SELECT auth.uid()) = user_id); - -CREATE POLICY "Users can delete own data" ON user_documents -FOR DELETE TO authenticated -USING ((SELECT auth.uid()) = user_id); +-- Users with active subscription +CREATE POLICY "Active subscribers" ON premium_content FOR SELECT TO authenticated USING ((SELECT auth.uid()) IS NOT NULL AND EXISTS (SELECT 1 FROM subscriptions WHERE user_id = (SELECT auth.uid()) AND status='active' AND expires_at>NOW())); \`\`\` -**Profile-based access:** +### Supabase Storage Specifics \`\`\`sql -CREATE POLICY "Users can update own profiles" ON profiles -FOR UPDATE TO authenticated -USING ((SELECT auth.uid()) = id); +-- Only allow upload/view for own folder +CREATE POLICY "User uploads" ON storage.objects FOR INSERT TO authenticated WITH CHECK (bucket_id = 'user-uploads' AND (storage.foldername(name))[1]=(SELECT auth.uid())::text); +CREATE POLICY "User file access" ON storage.objects FOR SELECT TO authenticated USING (bucket_id = 'user-uploads' AND (storage.foldername(name))[1]=(SELECT auth.uid())::text); \`\`\` -### 2. Multi-Tenant Data Isolation - -**Using custom claims from JWT:** +## Advanced Patterns: Security Definer & Custom Claims +- Use \`SECURITY DEFINER\` helper functions for JOIN-heavy checks (e.g. returning tenant_id for user). +- Always revoke EXECUTE on such helper functions from \`anon\` and \`authenticated\`. +- Use custom DB tables/functions for flexible RBAC via JWT claims or cross-table relationships. + +## Best Practices +1. **Enable RLS for all public/user tables.** +2. **Wrap \`auth.uid()\` with \`SELECT\` for better caching.** + \`\`\`sql + CREATE POLICY ... USING ((SELECT auth.uid()) = user_id); + \`\`\` +3. **Index columns** (e.g. user_id, tenant_id) used in policies. +4. **Prefer \`IN\`/\`ANY\` to JOIN:** subqueries in \`USING\`/\`WITH CHECK\` scale better than JOINs. +5. **Specify roles in \`TO\` to limit scope.** +6. **Test as multiple users & measure performance with RLS enabled.** + +## Pitfalls +- \`auth.uid()\` is NULL if JWT/context is missing. +- Always specify the \`TO\` clause; don't omit it. +- Only one operation per policy (no multi-op in FOR clause). +- Never use \`CREATE POLICY IF NOT EXISTS\`—not supported. +- \`SECURITY DEFINER\` functions should not be publicly executable. + +## Minimal Working Example: Multi-Tenant \`\`\`sql -CREATE POLICY "Tenant customers select" ON customers -FOR SELECT TO authenticated -USING ( - tenant_id = (auth.jwt() ->> 'tenant_id')::uuid -); - -CREATE POLICY "Tenant customers insert" ON customers -FOR INSERT TO authenticated -WITH CHECK ( - tenant_id = (auth.jwt() ->> 'tenant_id')::uuid -); - -CREATE POLICY "Tenant customers update" ON customers -FOR UPDATE TO authenticated -USING ( - tenant_id = (auth.jwt() ->> 'tenant_id')::uuid -) -WITH CHECK ( - tenant_id = (auth.jwt() ->> 'tenant_id')::uuid -); - -CREATE POLICY "Tenant customers delete" ON customers -FOR DELETE TO authenticated -USING ( - tenant_id = (auth.jwt() ->> 'tenant_id')::uuid -); -\`\`\` - -**Organization-based access:** -\`\`\`sql -CREATE POLICY "Organization members can view projects" ON projects -FOR SELECT TO authenticated -USING ( - organization_id IN ( - SELECT organization_id FROM user_organizations - WHERE user_id = (SELECT auth.uid()) - ) -); - -CREATE POLICY "Organization members can create projects" ON projects -FOR INSERT TO authenticated -WITH CHECK ( - organization_id IN ( - SELECT organization_id FROM user_organizations - WHERE user_id = (SELECT auth.uid()) - ) -); - -CREATE POLICY "Organization members can update projects" ON projects -FOR UPDATE TO authenticated -USING ( - organization_id IN ( - SELECT organization_id FROM user_organizations - WHERE user_id = (SELECT auth.uid()) - ) -) -WITH CHECK ( - organization_id IN ( - SELECT organization_id FROM user_organizations - WHERE user_id = (SELECT auth.uid()) - ) -); - -CREATE POLICY "Organization members can delete projects" ON projects -FOR DELETE TO authenticated -USING ( - organization_id IN ( - SELECT organization_id FROM user_organizations - WHERE user_id = (SELECT auth.uid()) - ) -); -\`\`\` - -### 3. Role-Based Access Control (RBAC) - -**Using custom claims for roles:** -\`\`\`sql -CREATE POLICY "Admin can view sensitive data" ON sensitive_data -FOR SELECT TO authenticated -USING ((auth.jwt() ->> 'user_role') = 'admin'); - -CREATE POLICY "Admin can insert sensitive data" ON sensitive_data -FOR INSERT TO authenticated -WITH CHECK ((auth.jwt() ->> 'user_role') = 'admin'); - -CREATE POLICY "Admin can update sensitive data" ON sensitive_data -FOR UPDATE TO authenticated -USING ((auth.jwt() ->> 'user_role') = 'admin') -WITH CHECK ((auth.jwt() ->> 'user_role') = 'admin'); - -CREATE POLICY "Admin can delete sensitive data" ON sensitive_data -FOR DELETE TO authenticated -USING ((auth.jwt() ->> 'user_role') = 'admin'); - -CREATE POLICY "Manager or owner access" ON employee_records -FOR SELECT TO authenticated -USING ( - (auth.jwt() ->> 'user_role') = 'manager' - OR owner_id = (SELECT auth.uid()) -); -\`\`\` - -**Multi-role support:** -\`\`\`sql -CREATE POLICY "Multiple roles allowed" ON documents -FOR SELECT TO authenticated -USING ( - (auth.jwt() ->> 'user_role') = ANY(ARRAY['admin', 'editor', 'viewer']) -); -\`\`\` - -### 4. Time-Based and Conditional Access - -**Active subscriptions only:** -\`\`\`sql -CREATE POLICY "Active subscribers" ON premium_content -FOR SELECT TO authenticated -USING ( - (SELECT auth.uid()) IS NOT NULL - AND EXISTS ( - SELECT 1 FROM subscriptions - WHERE user_id = (SELECT auth.uid()) - AND status = 'active' - AND expires_at > NOW() - ) -); -\`\`\` - -**Public or authenticated access:** -\`\`\`sql -CREATE POLICY "Public or own data" ON posts -FOR SELECT TO authenticated -USING ( - is_public = true - OR author_id = (SELECT auth.uid()) -); -\`\`\` - -## Advanced Supabase RLS Techniques - -### Using SECURITY DEFINER Functions - -To avoid recursive policy issues and improve performance, create helper functions: - -\`\`\`sql -CREATE OR REPLACE FUNCTION get_user_tenant_id() -RETURNS uuid -LANGUAGE sql -SECURITY DEFINER -STABLE -AS $$ - SELECT tenant_id FROM user_profiles - WHERE auth_user_id = auth.uid() - LIMIT 1; -$$; - --- Remove execution permissions for anon/authenticated roles -REVOKE EXECUTE ON FUNCTION get_user_tenant_id() FROM anon, authenticated; - -CREATE POLICY "Tenant orders select" ON orders -FOR SELECT TO authenticated -USING (tenant_id = get_user_tenant_id()); - -CREATE POLICY "Tenant orders insert" ON orders -FOR INSERT TO authenticated -WITH CHECK (tenant_id = get_user_tenant_id()); - -CREATE POLICY "Tenant orders update" ON orders -FOR UPDATE TO authenticated -USING (tenant_id = get_user_tenant_id()) -WITH CHECK (tenant_id = get_user_tenant_id()); - -CREATE POLICY "Tenant orders delete" ON orders -FOR DELETE TO authenticated -USING (tenant_id = get_user_tenant_id()); -\`\`\` - -### Custom Claims and RBAC Integration - -**Setting up custom claims with Auth Hooks:** -\`\`\`sql --- Create RBAC tables -CREATE TABLE user_roles ( - user_id uuid REFERENCES auth.users ON DELETE CASCADE, - role text NOT NULL, - PRIMARY KEY (user_id, role) -); - --- Create authorization function -CREATE OR REPLACE FUNCTION authorize( - requested_permission text, - resource_id uuid DEFAULT NULL -) -RETURNS boolean -LANGUAGE plpgsql -SECURITY DEFINER -AS $$ -DECLARE - user_id uuid; - user_role text; -BEGIN - user_id := (SELECT auth.uid()); - - IF user_id IS NULL THEN - RETURN false; - END IF; - - -- Check if user has required role - SELECT role INTO user_role - FROM user_roles - WHERE user_roles.user_id = authorize.user_id - AND role = requested_permission; - - RETURN user_role IS NOT NULL; -END; -$$; - --- Use in RLS policies -CREATE POLICY "Role-based documents select" ON documents -FOR SELECT TO authenticated -USING (authorize('documents.read')); - -CREATE POLICY "Role-based documents insert" ON documents -FOR INSERT TO authenticated -WITH CHECK (authorize('documents.create')); - -CREATE POLICY "Role-based documents update" ON documents -FOR UPDATE TO authenticated -USING (authorize('documents.update')) -WITH CHECK (authorize('documents.update')); - -CREATE POLICY "Role-based documents delete" ON documents -FOR DELETE TO authenticated -USING (authorize('documents.delete')); -\`\`\` - -### Performance Optimization for Supabase - -**1. Wrap auth functions in SELECT statements for caching[5][6]:** -\`\`\`sql --- Instead of: auth.uid() = user_id --- Use: (SELECT auth.uid()) = user_id -CREATE POLICY "Optimized user select" ON table_name -FOR SELECT TO authenticated -USING ((SELECT auth.uid()) = user_id); - -CREATE POLICY "Optimized user insert" ON table_name -FOR INSERT TO authenticated -WITH CHECK ((SELECT auth.uid()) = user_id); - -CREATE POLICY "Optimized user update" ON table_name -FOR UPDATE TO authenticated -USING ((SELECT auth.uid()) = user_id) -WITH CHECK ((SELECT auth.uid()) = user_id); - -CREATE POLICY "Optimized user delete" ON table_name -FOR DELETE TO authenticated -USING ((SELECT auth.uid()) = user_id); -\`\`\` - -**2. Index columns used in RLS policies:** -\`\`\`sql -CREATE INDEX idx_orders_user_id ON orders(user_id); -CREATE INDEX idx_profiles_tenant_id ON profiles(tenant_id); -\`\`\` - -**3. Use GIN indexes for array operations:** -\`\`\`sql -CREATE INDEX idx_user_permissions_gin ON user_permissions USING GIN(permissions); - -CREATE POLICY "Permission-based access" ON resources -FOR SELECT TO authenticated -USING ( - 'read_resource' = ANY( - SELECT permissions FROM user_permissions - WHERE user_id = (SELECT auth.uid()) - ) -); -\`\`\` - -**4. Minimize joins in policies:** -\`\`\`sql --- Instead of joining source to target table, use IN/ANY operations -CREATE POLICY "Users can view records belonging to their teams" ON test_table -FOR SELECT TO authenticated -USING ( -team_id IN ( - SELECT team_id - FROM team_user - WHERE user_id = (SELECT auth.uid()) -- no join -) -); - -CREATE POLICY "Users can insert records belonging to their teams" ON test_table -FOR INSERT TO authenticated -WITH CHECK ( -team_id IN ( - SELECT team_id - FROM team_user - WHERE user_id = (SELECT auth.uid()) -- no join -) -); - -CREATE POLICY "Users can update records belonging to their teams" ON test_table -FOR UPDATE TO authenticated -USING ( -team_id IN ( - SELECT team_id - FROM team_user - WHERE user_id = (SELECT auth.uid()) -- no join -) -) -WITH CHECK ( -team_id IN ( - SELECT team_id - FROM team_user - WHERE user_id = (SELECT auth.uid()) -- no join -) -); - -CREATE POLICY "Users can delete records belonging to their teams" ON test_table -FOR DELETE TO authenticated -USING ( -team_id IN ( - SELECT team_id - FROM team_user - WHERE user_id = (SELECT auth.uid()) -- no join -) -); -\`\`\` - -**5. Always specify roles to prevent unnecessary policy execution:** -\`\`\`sql --- Always use TO clause to limit which roles the policy applies to -CREATE POLICY "Users can view their own records" ON rls_test -FOR SELECT TO authenticated -USING ((SELECT auth.uid()) = user_id); - -CREATE POLICY "Users can insert their own records" ON rls_test -FOR INSERT TO authenticated -WITH CHECK ((SELECT auth.uid()) = user_id); - -CREATE POLICY "Users can update their own records" ON rls_test -FOR UPDATE TO authenticated -USING ((SELECT auth.uid()) = user_id) -WITH CHECK ((SELECT auth.uid()) = user_id); - -CREATE POLICY "Users can delete their own records" ON rls_test -FOR DELETE TO authenticated -USING ((SELECT auth.uid()) = user_id); -\`\`\` - -## Supabase Storage RLS - -Supabase Storage integrates with RLS on the \`storage.objects\` table[7]: - -\`\`\`sql --- Allow authenticated users to upload to their folder -CREATE POLICY "User folder uploads" ON storage.objects -FOR INSERT TO authenticated -WITH CHECK ( - bucket_id = 'user-uploads' - AND (storage.foldername(name))[1] = (SELECT auth.uid())::text -); - --- Allow users to view their own files -CREATE POLICY "User file access" ON storage.objects -FOR SELECT TO authenticated -USING ( - bucket_id = 'user-uploads' - AND (storage.foldername(name))[1] = (SELECT auth.uid())::text -); -\`\`\` - -## Common Pitfalls and Solutions - -### 1. Auth Context Issues - -**Problem**: \`auth.uid()\` returns NULL in server-side contexts. - -**Solution**: Ensure proper JWT token is passed to Supabase client: -\`\`\`javascript -// In Edge Functions or server-side code -const supabaseClient = createClient( - process.env.SUPABASE_URL, - process.env.SUPABASE_ANON_KEY, - { - global: { - headers: { - Authorization: req.headers.get('Authorization') - } - } - } -); -\`\`\` - -### 2. Role Confusion - -**Problem**: User appears authenticated but has 'anon' role in JWT[9]. - -**Solution**: Verify proper session management and token refresh: -\`\`\`javascript -// Check session validity -const { data: { session } } = await supabase.auth.getSession(); -if (!session) { - // Redirect to login -} -\`\`\` - -### 3. Security Definer Function Exposure - -**Problem**: Security definer functions exposed via API can leak data. - -**Solution**: Either move to custom schema or revoke execution permissions: -\`\`\`sql --- Option 1: Revoke permissions -REVOKE EXECUTE ON FUNCTION sensitive_function() FROM anon, authenticated; - --- Option 2: Create in custom schema (not exposed) -CREATE SCHEMA private; -CREATE FUNCTION private.sensitive_function() -RETURNS ... SECURITY DEFINER ...; -\`\`\` - -## Best Practices for Supabase - -1. **Always enable RLS on public schema tables**[1][12] -2. **Use \`(SELECT auth.uid())\` pattern for performance**[5][6] -3. **Create indexes on columns used in RLS policies** -4. **Use custom claims in JWT for complex authorization**[13] -5. **Test policies with different user contexts** -6. **Monitor query performance with RLS enabled**[5][14] -7. **Use security definer functions responsibly**[10][11] -8. **Leverage Supabase's built-in roles appropriately**[4] - -## Critical RLS Syntax Rules - -1. **Policy structure must follow exact order:** -\`\`\`sql -CREATE POLICY "policy name" ON table_name -FOR operation -- must come before TO clause -TO role_name -- must come after FOR clause (one or more roles) -USING (condition) -WITH CHECK (condition); -\`\`\` - -2. **Multiple operations require separate policies:** -\`\`\`sql --- INCORRECT: Cannot specify multiple operations -CREATE POLICY "bad policy" ON profiles -FOR INSERT, DELETE -- This will fail -TO authenticated; - --- CORRECT: Separate policies for each operation -CREATE POLICY "Profiles can be created" ON profiles -FOR INSERT TO authenticated -WITH CHECK (true); - -CREATE POLICY "Profiles can be deleted" ON profiles -FOR DELETE TO authenticated -USING (true); -\`\`\` - -3. **Always specify the TO clause:** -\`\`\`sql --- INCORRECT: Missing TO clause -CREATE POLICY "Users access own data" ON user_documents -FOR ALL USING ((SELECT auth.uid()) = user_id); - --- CORRECT: Include TO clause and separate operations -CREATE POLICY "Users can view own data" ON user_documents -FOR SELECT TO authenticated -USING ((SELECT auth.uid()) = user_id); - -CREATE POLICY "Users can insert own data" ON user_documents -FOR INSERT TO authenticated -WITH CHECK ((SELECT auth.uid()) = user_id); - -CREATE POLICY "Users can update own data" ON user_documents -FOR UPDATE TO authenticated -USING ((SELECT auth.uid()) = user_id) -WITH CHECK ((SELECT auth.uid()) = user_id); - -CREATE POLICY "Users can delete own data" ON user_documents -FOR DELETE TO authenticated -USING ((SELECT auth.uid()) = user_id); -\`\`\` - -4. **Operation-specific clause requirements:** -- SELECT: Only USING clause, never WITH CHECK -- INSERT: Only WITH CHECK clause, never USING -- UPDATE: Both USING and WITH CHECK clauses -- DELETE: Only USING clause, never WITH CHECK - -## Example: Complete Supabase Multi-Tenant Setup - -\`\`\`sql --- Enable RLS on all tables +-- Enable RLS ALTER TABLE customers ENABLE ROW LEVEL SECURITY; -ALTER TABLE orders ENABLE ROW LEVEL SECURITY; -ALTER TABLE products ENABLE ROW LEVEL SECURITY; - --- Helper function for tenant access -CREATE OR REPLACE FUNCTION get_user_tenant() -RETURNS uuid -LANGUAGE sql -SECURITY DEFINER -STABLE -AS $$ - SELECT tenant_id FROM user_profiles - WHERE auth_user_id = auth.uid(); -$$; - --- Revoke public execution +-- Helper function +CREATE OR REPLACE FUNCTION get_user_tenant() RETURNS uuid LANGUAGE sql SECURITY DEFINER STABLE AS $$ SELECT tenant_id FROM user_profiles WHERE auth_user_id=auth.uid(); $$; REVOKE EXECUTE ON FUNCTION get_user_tenant() FROM anon, authenticated; - --- Create tenant isolation policies -CREATE POLICY "Tenant customers select" ON customers -FOR SELECT TO authenticated -USING (tenant_id = get_user_tenant()); - -CREATE POLICY "Tenant customers insert" ON customers -FOR INSERT TO authenticated -WITH CHECK (tenant_id = get_user_tenant()); - -CREATE POLICY "Tenant customers update" ON customers -FOR UPDATE TO authenticated -USING (tenant_id = get_user_tenant()) -WITH CHECK (tenant_id = get_user_tenant()); - -CREATE POLICY "Tenant customers delete" ON customers -FOR DELETE TO authenticated -USING (tenant_id = get_user_tenant()); - -CREATE POLICY "Tenant orders select" ON orders -FOR SELECT TO authenticated -USING (tenant_id = get_user_tenant()); - -CREATE POLICY "Tenant orders insert" ON orders -FOR INSERT TO authenticated -WITH CHECK (tenant_id = get_user_tenant()); - -CREATE POLICY "Tenant orders update" ON orders -FOR UPDATE TO authenticated -USING (tenant_id = get_user_tenant()) -WITH CHECK (tenant_id = get_user_tenant()); - -CREATE POLICY "Tenant orders delete" ON orders -FOR DELETE TO authenticated -USING (tenant_id = get_user_tenant()); - -CREATE POLICY "Tenant products select" ON products -FOR SELECT TO authenticated -USING (tenant_id = get_user_tenant()); - -CREATE POLICY "Tenant products insert" ON products -FOR INSERT TO authenticated -WITH CHECK (tenant_id = get_user_tenant()); - -CREATE POLICY "Tenant products update" ON products -FOR UPDATE TO authenticated -USING (tenant_id = get_user_tenant()) -WITH CHECK (tenant_id = get_user_tenant()); - -CREATE POLICY "Tenant products delete" ON products -FOR DELETE TO authenticated -USING (tenant_id = get_user_tenant()); - --- Admin override using custom claims -CREATE POLICY "Admin customers select" ON customers -FOR SELECT TO authenticated -USING ((auth.jwt() ->> 'user_role') = 'admin'); - -CREATE POLICY "Admin customers insert" ON customers -FOR INSERT TO authenticated -WITH CHECK ((auth.jwt() ->> 'user_role') = 'admin'); - -CREATE POLICY "Admin customers update" ON customers -FOR UPDATE TO authenticated -USING ((auth.jwt() ->> 'user_role') = 'admin') -WITH CHECK ((auth.jwt() ->> 'user_role') = 'admin'); - -CREATE POLICY "Admin customers delete" ON customers -FOR DELETE TO authenticated -USING ((auth.jwt() ->> 'user_role') = 'admin'); - --- Performance indexes +-- Policies +CREATE POLICY "Tenant read" ON customers FOR SELECT TO authenticated USING (tenant_id=get_user_tenant()); +CREATE POLICY "Tenant write" ON customers FOR INSERT TO authenticated WITH CHECK (tenant_id=get_user_tenant()); +-- Index CREATE INDEX idx_customers_tenant ON customers(tenant_id); -CREATE INDEX idx_orders_tenant ON orders(tenant_id); -CREATE INDEX idx_products_tenant ON products(tenant_id); + +## Complex RLS +- Use \`search_docs\` to search the Supabase documentation for Row Level Security to learn more about complex RLS patterns \`\`\` + +--- + +> For all: Keep policies atomic & explicit, use proper roles, index wisely, and always check user context. Any advanced structure (e.g. RBAC, multitenancy) should use helper functions and claims, and be thoroughly tested in all access scenarios. ` export const EDGE_FUNCTION_PROMPT = ` @@ -781,104 +208,129 @@ Deno.serve(async (req: Request) => { ` export const PG_BEST_PRACTICES = ` -# Postgres Best Practices: - -## SQL Style: - - Generated SQL must be valid Postgres SQL. - - Always use double apostrophes for escaped single quotes (e.g., 'Night''s watch'). - - Always use semicolons at the end of SQL statements. - - Use \`vector(384)\` for embedding/vector related queries. - - Prefer \`text\` over \`varchar\`. - - Prefer \`timestamp with time zone\` over \`date\`. - - Feel free to suggest corrections for suspected typos in user input. - - We do not need pgcrypto extension for generating UUIDs - -## Object Generation: -- **Auth Schema**: The \`auth.users\` table stores user authentication data. Create a \`public.profiles\` table linked to \`auth.users\` (via user_id referencing auth.users.id) for user-specific public data. Do not create a new 'users' table. Never suggest creating a view to retrieve information directly from \`auth.users\`. +Developer: # Postgres Best Practices + +## SQL Style Guidelines +- All generated SQL must be valid for Postgres. +- Always escape single quotes within strings using double apostrophes (e.g., \`'Night''s watch'\`). +- Terminate each SQL statement with a semicolon (\`;\`). +- For embeddings or vector queries, use \`vector(384)\`. +- Prefer \`text\` instead of \`varchar\`. +- Prefer \`timestamp with time zone\` over the \`date\` type. +- Suggest corrections for suspected typos in the user input. +- Do **not** use the \`pgcrypto\` extension for generating UUIDs (unnecessary). + +## Task Workflow +Begin with a concise checklist (3-7 bullets) of sub-tasks you will perform before generating outputs. Keep the checklist conceptual, not implementation-level. + +## Object Creation +- **Auth Schema**: + - Use the \`auth.users\` table for user authentication data. + - Create a \`public.profiles\` table linked to \`auth.users\` via \`user_id\` referencing \`auth.users.id\` for user-specific public data. + - Do **not** create a new \`users\` table. + - Never suggest creating a view that selects directly from \`auth.users\`. + - **Tables**: - - Ensure tables have a primary key, preferably \`id bigint primary key generated always as identity\`. - - Enable Row Level Security (RLS) on all new tables (\`enable row level security\`). Inform the user they need to add policies. - - Prefer defining foreign key references within the \`CREATE TABLE\` statement. - - If a foreign key is created, also generate a separate \`CREATE INDEX\` statement for the foreign key column(s) to optimize joins. - - **Foreign Tables**: Create foreign tables in a schema named \`private\` (create the schema if it doesn't exist). Explain the security risk (RLS bypass) and link to https://supabase.com/docs/guides/database/database-advisors?queryGroups=lint&lint=0017_foreign_table_in_api. + - All tables must have a primary key, preferably \`id bigint primary key generated always as identity\`. + - Enable Row Level Security (RLS) on all new tables with \`enable row level security\`; inform users that they need to add policies. + - Define foreign key references within the \`CREATE TABLE\` statement. + - Whenever a foreign key is used, generate a separate \`CREATE INDEX\` statement for the foreign key column(s) to improve performance on joins. + - **Foreign Tables**: Place foreign tables in a schema named \`private\` (create the schema if needed). Explain the security risk (RLS bypass) and include a link: https://supabase.com/docs/guides/database/database-advisors?queryGroups=lint&lint=0017_foreign_table_in_api. + - **Views**: - - Include \`with (security_invoker=on)\` immediately after \`CREATE VIEW view_name\`. - - **Materialized Views**: Create materialized views in the \`private\` schema (create if needed). Explain the security risk (RLS bypass) and link to https://supabase.com/docs/guides/database/database-advisors?queryGroups=lint&lint=0016_materialized_view_in_api. + - Add \`with (security_invoker=on)\` immediately after \`CREATE VIEW view_name\`. + - **Materialized Views**: Store materialized views in the \`private\` schema (create if needed). Explain the security risk (RLS bypass) and reference: https://supabase.com/docs/guides/database/database-advisors?queryGroups=lint&lint=0016_materialized_view_in_api. + - **Extensions**: - - Install extensions in the \`extensions\` schema or a dedicated schema, **never** in \`public\`. + - Always install extensions in the \`extensions\` schema or a dedicated schema, never in \`public\`. + - **RLS Policies**: - - First, retrieve the schema information using \`list_tables\` and \`list_extensions\` tools. - - **Key RLS Rules**: - - Use only CREATE POLICY or ALTER POLICY queries - - Always use "auth.uid()" instead of "current_user" - - SELECT policies should always have USING but not WITH CHECK - - INSERT policies should always have WITH CHECK but not USING - - UPDATE policies should always have WITH CHECK and most often have USING - - DELETE policies should always have USING but not WITH CHECK - - Always specify the target role using the \`TO\` clause (e.g., \`TO authenticated\`, \`TO anon\`, \`TO authenticated, anon\`) - - Avoid using \`FOR ALL\`. Instead create separate policies for each operation (SELECT, INSERT, UPDATE, DELETE) - - Policy names should be short but detailed text explaining the policy, enclosed in double quotes - - Discourage \`RESTRICTIVE\` policies and encourage \`PERMISSIVE\` policies + - Retrieve schema information first (using \`list_tables\` and \`list_extensions\` and \`list_policies\` tools). + - Before using any tool, briefly state the tool's purpose and inputs required. + - After each tool call, validate the result in 1-2 lines and decide on next steps, self-correcting if validation fails. + - **Key Policy Rules:** + - Only use \`CREATE POLICY\` or \`ALTER POLICY\` statements. + - Always use \`auth.uid()\` (never \`current_user\`). + - For SELECT, use \`USING\` (not \`WITH CHECK\`). + - For INSERT, use \`WITH CHECK\` (not \`USING\`). + - For UPDATE, use \`WITH CHECK\`; \`USING\` is recommended for most cases. + - For DELETE, use \`USING\` (not \`WITH CHECK\`). + - Specify the target role(s) using the \`TO\` clause (e.g., \`TO authenticated\`, \`TO anon\`, \`TO authenticated, anon\`). + - Do not use \`FOR ALL\`—create separate policies for SELECT, INSERT, UPDATE, and DELETE. + - Policy names should be concise, descriptive text, enclosed in double quotes. + - Avoid \`RESTRICTIVE\` policies; favor \`PERMISSIVE\` policies. + - **Database Functions**: - - Use \`security definer\` for functions returning type \`trigger\`; otherwise, default to \`security invoker\`. - - Set the search path configuration: \`set search_path = ''\` within the function definition. - - Use \`create or replace function\` when possible. + - Use \`security definer\` for functions that return \`trigger\`; otherwise, default to \`security invoker\`. + - Set \`search_path\` within the function definition: \`set search_path = ''\`. + - Use \`create or replace function\` whenever possible. ` export const GENERAL_PROMPT = ` -# Goals -You are a Supabase Postgres expert. Your goals are to help people manage their Supabase project via: - - Writing SQL queries - - Writing Edge Functions - - Debugging issues - - Checking the status of the project +Developer: # Role and Objective +- Act as a Supabase Postgres expert, assisting users in managing their Supabase projects efficiently. + +# Instructions +- Provide support by: + - Writing SQL queries + - Creating Edge Functions + - Debugging issues + - Monitoring project status # Tools - - Always attempt to use tools like \`list_tables\` and \`list_extensions\` and \`list_edge_functions\` before answering to gather contextual information if available that will help inform your response. - - Tools are only available to you, the user cannot use them, so do not suggest they use them - - The user may not have access to these tools based on their organization settings +- Before forming a response, utilize available tools such as \`list_tables\`, \`list_extensions\`, and \`list_edge_functions\` to gather relevant context whenever possible. +- Before any tool call, briefly state the purpose of the call and the minimal required inputs. +- These tools are exclusively for your use; do not suggest or imply that users can access or operate them. +- Tool usage is limited to tools listed above; for read-only or information-gathering actions, call automatically, but for potentially destructive operations, seek explicit user confirmation before proceeding. +- Be aware that tool access may be restricted depending on the user's organization settings. + +# Plan +- Begin with a concise checklist (3-7 bullets) summarizing your steps before completing significant multi-step tasks; keep items conceptual, not implementation-level. + +# Output Format +- Always integrate findings from the tools seamlessly into your responses for better accuracy and context. +- After tool usage, briefly validate the result and determine the next step or adjust if needed. + +# Searching Docs +- Use \`search_docs\` to search the Supabase documentation for relevant information when the question is about Supabase features or functionality or complex database operations ` export const CHAT_PROMPT = ` -# Response Style: - - Be **direct and concise**. Focus on delivering the essential information. - - Prefer lists over tables to display information - - Limit use of emojis +Developer: # Response Style +- Be direct and concise. Provide only essential information. +- Use lists to present information; do not use tables for formatting. +- Minimize use of emojis. # Response Format ## Markdown - - Conform to CommonMark specification - - Use a clear heading hierarchy (H1–H4) without skipping levels when useful. - - Use bold text only to highlight important information - - **Never** use tables to display information - -# Rename Chat**: - - **Always call \`rename_chat\` before you respond at the start of the conversation** with a 2-4 word descriptive name. Examples: "User Authentication Setup", "Sales Data Analysis", "Product Table Creation"**. - -# Query rendering**: - - **Always call the \`display_query\` tool to render sql queries. You do not need to write the query yourself. ie Do not use markdown code blocks.** - - Before using display_query, explain the query in natural language. - - READ ONLY: Use \`display_query\` with \`sql\` and \`label\`. If results may be visualized, also provide \`view\` ('table' or 'chart'), \`xAxis\`, and \`yAxis\`. - - The user can run the query from the UI when you use display_query. - - Use \`display_query\` in the natural flow of the conversation. **Do not output the query in markdown** - - WRITE/DDL (INSERT, UPDATE, DELETE, CREATE, ALTER, DROP): Use \`display_query\` with \`sql\` and \`label\`. If using RETURNING (or otherwise returning visualizable data), also provide \`view\`, \`xAxis\`, and \`yAxis\`. - - If multiple, separate queries are needed, call \`display_query\` once per distinct query. - -# Edge functions**: - - **Always use \`display_edge_function\` to render Edge Function code instead of markdown code blocks** - - Use \`display_edge_function\` with the function \`name\` and TypeScript code to propose an Edge Function. Only use this to display Edge Function code (not logs or other content). - - The user can deploy the function directly from the dashboard when you use display_edge_function - -# Checking health - - Use \`get_advisors\` to check for any issues with the project. - - If the user does not have access to the \`get_advisors\` tool, they will have to use the Supabase dashboard to check for issues - -# Checking health - - Use \`get_advisors\` to check for any issues with the project. - - If the user does not have access to the \`get_advisors\` tool, they will have to use the Supabase dashboard to check for issues - -# Safety**: - - For destructive queries (e.g., DROP TABLE, DELETE without WHERE), ask for confirmation before generating the SQL with \`display_query\`. +- Follow the CommonMark specification. +- Use a logical heading hierarchy (H1–H4), maintaining order without skipping levels. +- Use bold text exclusively to emphasize key information. +- Do not use tables for displaying information under any circumstances. + +# Chat Naming +- At the start of each conversation, always invoke \`rename_chat\` with a descriptive 2–4 word name. Examples: "User Authentication Setup", "Sales Data Analysis", "Product Table Creation". + +# SQL Query Display +- Before using any tool, state the purpose of the tool call and the minimal required inputs. +- Always utilize the \`display_query\` tool to render SQL queries that user needs to see. Never show queries in markdown code blocks. +- Briefly describe in natural language what each query does before calling \`display_query\`. +- For READ-ONLY queries: Use \`display_query\` with parameters \`sql\` and \`label\`. If results are suitable for visualization, also provide \`view\` (as 'table' or 'chart'), \`xAxis\`, and \`yAxis\`. +- For WRITE/DDL queries (INSERT, UPDATE, DELETE, CREATE, ALTER, DROP): Use \`display_query\` with \`sql\` and \`label\`. If the result can be visualized, also provide \`view\`, \`xAxis\`, and \`yAxis\`. +- If multiple queries are needed, call \`display_query\` separately for each query and validate each result in 1–2 lines before proceeding. +- Integrate \`display_query\` naturally into responses. Never present queries in markdown format. +- After executing a destructive query, summarize the outcome and confirm next actions or self-correct as needed. + +# Edge Functions +- Always display Edge Function code using \`display_edge_function\`, never in markdown code blocks. +- Use \`display_edge_function\` with the function's \`name\` and TypeScript code when proposing an Edge Function. Only use this for Edge Function source code, not for logs or other content. +- Once displayed, users can deploy the function directly from the dashboard. + +# Project Health Checks +- Use \`get_advisors\` to identify project issues. If this tool is unavailable, instruct users to check the Supabase dashboard for issues. + +# Safety for Destructive Queries +- For destructive commands (e.g., DROP TABLE, DELETE without WHERE clause), always require explicit user confirmation before generating and displaying the SQL using \`display_query\`. Validate confirmation prior to execution. ` export const OUTPUT_ONLY_PROMPT = ` diff --git a/apps/studio/package.json b/apps/studio/package.json index 33def8d179e1d..4320f6d587b29 100644 --- a/apps/studio/package.json +++ b/apps/studio/package.json @@ -5,7 +5,6 @@ "scripts": { "preinstall": "npx only-allow pnpm", "dev": "next dev --turbopack -p 8082", - "dev:secrets:pull": "AWS_PROFILE=supa-dev node ../../scripts/getSecrets.js -n local/studio", "build": "next build && ./../../scripts/upload-static-assets.sh", "start": "next start", "lint": "next lint", diff --git a/apps/studio/pages/api/ai/code/complete.ts b/apps/studio/pages/api/ai/code/complete.ts index 43cf4d8fdc54f..03d64a7866d52 100644 --- a/apps/studio/pages/api/ai/code/complete.ts +++ b/apps/studio/pages/api/ai/code/complete.ts @@ -18,7 +18,6 @@ import { getTools } from 'lib/ai/tools' import apiWrapper from 'lib/api/apiWrapper' import { queryPgMetaSelfHosted } from 'lib/self-hosted' import { NextApiRequest, NextApiResponse } from 'next' -import { z } from 'zod/v4' export const maxDuration = 60 @@ -47,7 +46,6 @@ async function handler(req: NextApiRequest, res: NextApiResponse) { const accessToken = authorization?.replace('Bearer ', '') let aiOptInLevel: AiOptInLevel = 'disabled' - let isLimited = false if (!IS_PLATFORM) { aiOptInLevel = 'schema' @@ -55,17 +53,24 @@ async function handler(req: NextApiRequest, res: NextApiResponse) { if (IS_PLATFORM && orgSlug && authorization && projectRef) { // Get organizations and compute opt in level server-side - const { aiOptInLevel: orgAIOptInLevel, isLimited: orgAILimited } = await getOrgAIDetails({ + const { aiOptInLevel: orgAIOptInLevel } = await getOrgAIDetails({ orgSlug, authorization, projectRef, }) aiOptInLevel = orgAIOptInLevel - isLimited = orgAILimited } - const { model, error: modelError, supportsCachePoint } = await getModel(projectRef, isLimited) + // For code completion, we always use the limited model + const { + model, + error: modelError, + promptProviderOptions, + } = await getModel({ + provider: 'openai', + routingKey: projectRef, + }) if (modelError) { return res.status(500).json({ error: modelError.message }) @@ -111,14 +116,7 @@ async function handler(req: NextApiRequest, res: NextApiResponse) { { role: 'system', content: system, - ...(supportsCachePoint && { - providerOptions: { - bedrock: { - // Always cache the system prompt (must not contain dynamic content) - cachePoint: { type: 'default' }, - }, - }, - }), + ...(promptProviderOptions && { providerOptions: promptProviderOptions }), }, { role: 'assistant', diff --git a/apps/studio/pages/api/ai/feedback/classify.ts b/apps/studio/pages/api/ai/feedback/classify.ts index c27a76c3d4d2d..ccebe72a43b0b 100644 --- a/apps/studio/pages/api/ai/feedback/classify.ts +++ b/apps/studio/pages/api/ai/feedback/classify.ts @@ -28,7 +28,10 @@ export async function handlePost(req: NextApiRequest, res: NextApiResponse) { } try { - const { model, error: modelError } = await getModel() + const { model, error: modelError } = await getModel({ + provider: 'openai', + routingKey: 'feedback', + }) if (modelError) { return res.status(500).json({ error: modelError.message }) diff --git a/apps/studio/pages/api/ai/onboarding/design.ts b/apps/studio/pages/api/ai/onboarding/design.ts index 7a386321fc72e..9e12fb0993989 100644 --- a/apps/studio/pages/api/ai/onboarding/design.ts +++ b/apps/studio/pages/api/ai/onboarding/design.ts @@ -64,7 +64,10 @@ const wrapper = (req: NextApiRequest, res: NextApiResponse) => export default wrapper async function handlePost(req: NextApiRequest, res: NextApiResponse) { - const { model, error: modelError } = await getModel() + const { model, error: modelError } = await getModel({ + provider: 'openai', + routingKey: 'onboarding', + }) if (modelError) { return res.status(500).json({ error: modelError.message }) diff --git a/apps/studio/pages/api/ai/sql/cron-v2.ts b/apps/studio/pages/api/ai/sql/cron-v2.ts index 1ed03dccc6073..000106e276c0c 100644 --- a/apps/studio/pages/api/ai/sql/cron-v2.ts +++ b/apps/studio/pages/api/ai/sql/cron-v2.ts @@ -34,7 +34,10 @@ export async function handlePost(req: NextApiRequest, res: NextApiResponse) { } try { - const { model, error: modelError } = await getModel() + const { model, error: modelError } = await getModel({ + provider: 'openai', + routingKey: 'cron', + }) if (modelError) { return res.status(500).json({ error: modelError.message }) @@ -79,7 +82,6 @@ export async function handlePost(req: NextApiRequest, res: NextApiResponse) { Here is the user's prompt: ${prompt} `, - temperature: 0, }) return res.json(result.object.cron_expression) diff --git a/apps/studio/pages/api/ai/sql/generate-v4.ts b/apps/studio/pages/api/ai/sql/generate-v4.ts index 1bf5038bef38b..91540460a04b0 100644 --- a/apps/studio/pages/api/ai/sql/generate-v4.ts +++ b/apps/studio/pages/api/ai/sql/generate-v4.ts @@ -117,7 +117,17 @@ async function handlePost(req: NextApiRequest, res: NextApiResponse) { } } - const { model, error: modelError, supportsCachePoint } = await getModel(projectRef, isLimited) // use project ref as routing key + const { + model, + error: modelError, + promptProviderOptions, + providerOptions, + } = await getModel({ + provider: 'openai', + model: 'gpt-5', + routingKey: projectRef, + isLimited, + }) if (modelError) { return res.status(500).json({ error: modelError.message }) @@ -165,14 +175,7 @@ async function handlePost(req: NextApiRequest, res: NextApiResponse) { { role: 'system', content: system, - ...(supportsCachePoint && { - providerOptions: { - bedrock: { - // Always cache the system prompt (must not contain dynamic content) - cachePoint: { type: 'default' }, - }, - }, - }), + ...(promptProviderOptions && { providerOptions: promptProviderOptions }), }, { role: 'assistant', @@ -199,11 +202,13 @@ async function handlePost(req: NextApiRequest, res: NextApiResponse) { model, stopWhen: stepCountIs(5), messages: coreMessages, + ...(providerOptions && { providerOptions }), tools, abortSignal: abortController.signal, }) result.pipeUIMessageStreamToResponse(res, { + sendReasoning: true, onError: (error) => { if (error == null) { return 'unknown error' diff --git a/apps/studio/pages/api/ai/sql/title-v2.ts b/apps/studio/pages/api/ai/sql/title-v2.ts index b96cf00f35acf..ed552d1030e32 100644 --- a/apps/studio/pages/api/ai/sql/title-v2.ts +++ b/apps/studio/pages/api/ai/sql/title-v2.ts @@ -39,7 +39,10 @@ export async function handlePost(req: NextApiRequest, res: NextApiResponse) { } try { - const { model, error: modelError } = await getModel() + const { model, error: modelError } = await getModel({ + provider: 'openai', + routingKey: 'sql', + }) if (modelError) { return res.status(500).json({ error: modelError.message }) @@ -55,7 +58,6 @@ export async function handlePost(req: NextApiRequest, res: NextApiResponse) { The description should describe why this table was created (eg. "Table to track todos") or what the query does. `, - temperature: 0, }) return res.json(result.object) diff --git a/apps/studio/pages/sign-in-sso.tsx b/apps/studio/pages/sign-in-sso.tsx index 9003722132c70..cad02ec4b3908 100644 --- a/apps/studio/pages/sign-in-sso.tsx +++ b/apps/studio/pages/sign-in-sso.tsx @@ -1,8 +1,16 @@ import { SignInSSOForm } from 'components/interfaces/SignIn/SignInSSOForm' import SignInLayout from 'components/layouts/SignInLayout/SignInLayout' +import { UnknownInterface } from 'components/ui/UnknownInterface' +import { useIsFeatureEnabled } from 'hooks/misc/useIsFeatureEnabled' import type { NextPageWithLayout } from 'types' const SignInSSOPage: NextPageWithLayout = () => { + const signInWithSSOEnabled = useIsFeatureEnabled('dashboard_auth:sign_in_with_sso') + + if (!signInWithSSOEnabled) { + return + } + return ( <>
diff --git a/apps/studio/pages/sign-in.tsx b/apps/studio/pages/sign-in.tsx index 62f383d27773e..db421158ce828 100644 --- a/apps/studio/pages/sign-in.tsx +++ b/apps/studio/pages/sign-in.tsx @@ -5,9 +5,12 @@ import { useEffect } from 'react' import { LastSignInWrapper } from 'components/interfaces/SignIn/LastSignInWrapper' import { SignInForm } from 'components/interfaces/SignIn/SignInForm' +import { SignInWithCustom } from 'components/interfaces/SignIn/SignInWithCustom' import { SignInWithGitHub } from 'components/interfaces/SignIn/SignInWithGitHub' import { AuthenticationLayout } from 'components/layouts/AuthenticationLayout' import SignInLayout from 'components/layouts/SignInLayout/SignInLayout' +import { useCustomContent } from 'hooks/custom-content/useCustomContent' +import { useIsFeatureEnabled } from 'hooks/misc/useIsFeatureEnabled' import { IS_PLATFORM } from 'lib/constants' import type { NextPageWithLayout } from 'types' import { Button } from 'ui' @@ -15,6 +18,25 @@ import { Button } from 'ui' const SignInPage: NextPageWithLayout = () => { const router = useRouter() + const { + dashboardAuthSignInWithGithub: signInWithGithubEnabled, + dashboardAuthSignInWithSso: signInWithSsoEnabled, + dashboardAuthSignInWithEmail: signInWithEmailEnabled, + dashboardAuthSignUp: signUpEnabled, + } = useIsFeatureEnabled([ + 'dashboard_auth:sign_in_with_github', + 'dashboard_auth:sign_in_with_sso', + 'dashboard_auth:sign_in_with_email', + 'dashboard_auth:sign_up', + ]) + + const { dashboardAuthCustomProvider: customProvider } = useCustomContent([ + 'dashboard_auth:custom_provider', + ]) + + const showOrDivider = + (signInWithGithubEnabled || signInWithSsoEnabled || customProvider) && signInWithEmailEnabled + useEffect(() => { if (!IS_PLATFORM) { // on selfhosted instance just redirect to projects page @@ -25,45 +47,58 @@ const SignInPage: NextPageWithLayout = () => { return ( <>
- - - + + )} + + {showOrDivider && ( +
+
+
+
+
+ or +
+
+ )} + {signInWithEmailEnabled && } +
+ + {signUpEnabled && ( +
+
+ Don't have an account?{' '} - Continue with SSO + Sign Up Now - - - -
-
-
-
-
- or
- -
- -
-
- Don't have an account?{' '} - - Sign Up Now - -
-
+ )} ) } diff --git a/apps/studio/pages/sign-up.tsx b/apps/studio/pages/sign-up.tsx index c60e96d342b5e..5fbaeb116eb81 100644 --- a/apps/studio/pages/sign-up.tsx +++ b/apps/studio/pages/sign-up.tsx @@ -3,22 +3,37 @@ import Link from 'next/link' import { SignInWithGitHub } from 'components/interfaces/SignIn/SignInWithGitHub' import { SignUpForm } from 'components/interfaces/SignIn/SignUpForm' import SignInLayout from 'components/layouts/SignInLayout/SignInLayout' +import { UnknownInterface } from 'components/ui/UnknownInterface' +import { useIsFeatureEnabled } from 'hooks/misc/useIsFeatureEnabled' import type { NextPageWithLayout } from 'types' const SignUpPage: NextPageWithLayout = () => { + const { + dashboardAuthSignUp: signUpEnabled, + dashboardAuthSignInWithGithub: signInWithGithubEnabled, + } = useIsFeatureEnabled(['dashboard_auth:sign_up', 'dashboard_auth:sign_in_with_github']) + + if (!signUpEnabled) { + return + } + return ( <>
- - -
-
-
-
-
- or -
-
+ {signInWithGithubEnabled && ( + <> + + +
+
+
+
+
+ or +
+
+ + )}
diff --git a/packages/common/enabled-features/enabled-features.json b/packages/common/enabled-features/enabled-features.json index 7c7ee99b442e0..0f2a3178fb6e9 100644 --- a/packages/common/enabled-features/enabled-features.json +++ b/packages/common/enabled-features/enabled-features.json @@ -24,6 +24,11 @@ "billing:all": true, + "dashboard_auth:sign_up": true, + "dashboard_auth:sign_in_with_github": true, + "dashboard_auth:sign_in_with_sso": true, + "dashboard_auth:sign_in_with_email": true, + "database:replication": true, "database:roles": true, diff --git a/packages/common/enabled-features/enabled-features.schema.json b/packages/common/enabled-features/enabled-features.schema.json index ae6eeb10c655c..13325bae07f74 100644 --- a/packages/common/enabled-features/enabled-features.schema.json +++ b/packages/common/enabled-features/enabled-features.schema.json @@ -86,6 +86,23 @@ "description": "Enable the billing settings page" }, + "dashboard_auth:sign_up": { + "type": "boolean", + "description": "Enable the sign up page in the dashboard" + }, + "dashboard_auth:sign_in_with_github": { + "type": "boolean", + "description": "Enable the sign in with github provider" + }, + "dashboard_auth:sign_in_with_sso": { + "type": "boolean", + "description": "Enable the sign in with sso provider" + }, + "dashboard_auth:sign_in_with_email": { + "type": "boolean", + "description": "Enable the sign in with email/password provider" + }, + "database:replication": { "type": "boolean", "description": "Enable the database replication page" @@ -235,6 +252,10 @@ "authentication:show_sort_by_phone", "authentication:show_user_type_filter", "billing:all", + "dashboard_auth:sign_up", + "dashboard_auth:sign_in_with_github", + "dashboard_auth:sign_in_with_sso", + "dashboard_auth:sign_in_with_email", "database:replication", "database:roles", "docs:self-hosting",