Supabase AI can provide more relevant answers if you choose to share different levels of
- data. This feature is powered by Amazon Bedrock which does not store or log your prompts
- and completions, nor does it use them to train AWS models or distribute them to third
- parties. This is an organization-wide setting, so please select the level of data you
- are comfortable sharing.
+ data. This feature is powered by third-party AI providers. This is an organization-wide
+ setting, so please select the level of data you are comfortable sharing.
+
+
+ For organizations with HIPAA compliance enabled in their Supabase configuration, any
+ consented information will only be shared with third-party AI providers with whom
+ Supabase has established a Business Associate Agreement (BAA).
diff --git a/apps/studio/components/interfaces/Organization/GeneralSettings/OptInToOpenAIToggle.tsx b/apps/studio/components/interfaces/Organization/GeneralSettings/OptInToOpenAIToggle.tsx
index 33197145ab31a..0cee6710c139c 100644
--- a/apps/studio/components/interfaces/Organization/GeneralSettings/OptInToOpenAIToggle.tsx
+++ b/apps/studio/components/interfaces/Organization/GeneralSettings/OptInToOpenAIToggle.tsx
@@ -27,28 +27,22 @@ export const OptInToOpenAIToggle = () => {
className="flex flex-col gap-y-4 text-sm text-foreground-light"
>
- Supabase AI utilizes Amazon Bedrock ("Bedrock"), a service designed with a strong focus
- on data privacy and security.
+ Supabase AI utilizes third-party AI providers designed with a strong focus on data
+ privacy and security.
- Amazon Bedrock does not store or log your prompts and completions. This data is not used
- to train any AWS models and is not distributed to third parties or model providers.
- Model providers do not have access to Amazon Bedrock logs or customer prompts and
- completions.
+ By default, only schema data is shared with third-party AI providers. This is not
+ retained by them nor used as training data. With your permission, Supabase may also
+ share customer-generated prompts, database data, and project logs with these providers.
+ This information is used solely to generate responses to your queries and is not
+ retained by the providers or used to train their models.
- By default, no information is shared with Bedrock unless you explicitly provide consent.
- With your permission, Supabase may share customer-generated prompts, database schema,
- database data, and project logs with Bedrock. This information is used solely to
- generate responses to your queries and is not retained by Bedrock or used to train their
- foundation models.
-
-
-
- If you are a HIPAA Covered Entity, please note that Bedrock is HIPAA eligible, and
- Supabase has a Business Associate Agreement in place covering this use.
+ For organizations with HIPAA compliance enabled in their Supabase configuration, any
+ consented information will only be shared with third-party AI providers with whom
+ Supabase has established a Business Associate Agreement (BAA).
diff --git a/apps/studio/components/interfaces/Organization/Usage/TotalUsage.tsx b/apps/studio/components/interfaces/Organization/Usage/TotalUsage.tsx
index 3995d22d4a92d..aa0e359d484d9 100644
--- a/apps/studio/components/interfaces/Organization/Usage/TotalUsage.tsx
+++ b/apps/studio/components/interfaces/Organization/Usage/TotalUsage.tsx
@@ -1,5 +1,6 @@
import { useMemo } from 'react'
+import { useBreakpoint } from 'common'
import AlertError from 'components/ui/AlertError'
import ShimmeringLoader from 'components/ui/ShimmeringLoader'
import {
@@ -41,6 +42,7 @@ export const TotalUsage = ({
endDate,
currentBillingCycleSelected,
}: ComputeProps) => {
+ const isMobile = useBreakpoint('md')
const isUsageBillingEnabled = subscription?.usage_billing_enabled
const { billingAll } = useIsFeatureEnabled(['billing:all'])
@@ -58,7 +60,7 @@ export const TotalUsage = ({
})
// When the user filters by project ref or selects a custom timeframe, we only display usage+project breakdown, but no costs/limits
- const showRelationToSubscription = currentBillingCycleSelected && projectRef === 'all-projects'
+ const showRelationToSubscription = currentBillingCycleSelected && !projectRef
const hasExceededAnyLimits =
showRelationToSubscription &&
@@ -184,19 +186,12 @@ export const TotalUsage = ({
)}
)}
-
+
{sortedBillingMetrics.map((metric, i) => {
- const isLastBillingMetric = i === sortedBillingMetrics.length - 1
- const isLastInRow = isLastBillingMetric && computeMetrics.length === 0
-
return (
{
return (
)
})}
+
+ {!isMobile && (sortedBillingMetrics.length + computeMetrics.length) % 2 === 1 && (
+
+ )}
)}
diff --git a/apps/studio/components/interfaces/Organization/Usage/Usage.tsx b/apps/studio/components/interfaces/Organization/Usage/Usage.tsx
index 728c45750b90f..e1a51f973873f 100644
--- a/apps/studio/components/interfaces/Organization/Usage/Usage.tsx
+++ b/apps/studio/components/interfaces/Organization/Usage/Usage.tsx
@@ -34,7 +34,14 @@ import { TotalUsage } from './TotalUsage'
const Usage = () => {
const { slug, projectRef } = useParams()
const [dateRange, setDateRange] = useState
()
- const [selectedProjectRef, setSelectedProjectRef] = useState('all-projects')
+ const [selectedProjectRefInputValue, setSelectedProjectRefInputValue] = useState<
+ string | undefined
+ >('all-projects')
+
+ // [Alaister] 'all-projects' is not a valid project ref, it's just used as an extra
+ // state for the select input. As such we need to remove it for the selected project ref
+ const selectedProjectRef =
+ selectedProjectRefInputValue === 'all-projects' ? undefined : selectedProjectRefInputValue
const canReadSubscriptions = useCheckPermissions(
PermissionAction.BILLING_READ,
@@ -58,7 +65,7 @@ const Usage = () => {
useEffect(() => {
if (projectRef && isSuccess && orgProjects !== undefined) {
if (orgProjects.find((project) => project.ref === projectRef)) {
- setSelectedProjectRef(projectRef)
+ setSelectedProjectRefInputValue(projectRef)
}
}
// [Joshen] Since we're already looking at isSuccess
@@ -164,10 +171,10 @@ const Usage = () => {
/>
{
- if (value === 'all-projects') setSelectedProjectRef('all-projects')
- else setSelectedProjectRef(value)
+ if (value === 'all-projects') setSelectedProjectRefInputValue('all-projects')
+ else setSelectedProjectRefInputValue(value)
}}
>
@@ -227,7 +234,7 @@ const Usage = () => {
- {selectedProjectRef && selectedProjectRef !== 'all-projects' ? (
+ {selectedProjectRef ? (
{
const hasSSLCertificate =
settings?.inserted_at !== undefined && new Date(settings.inserted_at) >= new Date('2021-04-30')
+ const { sslCertificateUrl: sslCertificateUrlTemplate } = useCustomContent(['ssl:certificate_url'])
+ const sslCertificateUrl = useMemo(
+ () => template(sslCertificateUrlTemplate ?? '')({ env }),
+ [sslCertificateUrlTemplate, env]
+ )
+
useEffect(() => {
if (!isLoading && sslEnforcementConfiguration) {
setIsEnforced(initialIsEnforced)
@@ -183,11 +191,7 @@ const SSLConfiguration = () => {
) : (
}>
-
- Download certificate
-
+ Download certificate
)}
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 (
+
+ Continue with {providerName}
+
+ )
+}
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 (
<>
-
-
- }>
+ {customProvider && }
+ {signInWithGithubEnabled && }
+ {signInWithSsoEnabled && (
+
+ }
+ >
+
+ Continue with SSO
+
+
+
+ )}
+
+ {showOrDivider && (
+
+ )}
+ {signInWithEmailEnabled && }
+
+
+ {signUpEnabled && (
+
+
+
Don't have an account? {' '}
- Continue with SSO
+ Sign Up Now
-
-
-
-
-
-
-
-
-
- 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 (
<>
-
-
-
+ {signInWithGithubEnabled && (
+ <>
+
+
+
+ >
+ )}
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",