-
{data.label}
+
{data.label}
{data.details}
@@ -196,20 +196,22 @@ const BlankNode = () => {
const CTANode = ({ projectRef }: { projectRef: string }) => {
return (
-
+
-
-
Replicate data to destinations in real-time
- Early Access
-
+ Stream database changes to external destinations
+
+ Automatically replicate your data to external data warehouses and analytics platforms in
+ real-time. No manual exports, no lag.
+
- Replicate database changes to multiple destinations - no manual exports, no lag. Limited
- rollout for external destinations has begun, read replicas available now.
+ We are currently in private alpha and slowly
+ onboarding new customers to ensure stable data pipelines. Request access below to join the
+ waitlist. Read replicas are available now.
}>
- Request early access
+ Request alpha access
}>
diff --git a/apps/studio/components/interfaces/Storage/AnalyticsBuckets/index.tsx b/apps/studio/components/interfaces/Storage/AnalyticsBuckets/index.tsx
index 2b8dd02536dd0..e561e74c2b874 100644
--- a/apps/studio/components/interfaces/Storage/AnalyticsBuckets/index.tsx
+++ b/apps/studio/components/interfaces/Storage/AnalyticsBuckets/index.tsx
@@ -5,6 +5,7 @@ import { useState } from 'react'
import { useParams } from 'common'
import { ScaffoldHeader, ScaffoldSection, ScaffoldSectionTitle } from 'components/layouts/Scaffold'
import AlertError from 'components/ui/AlertError'
+import { AlphaNotice } from 'components/ui/AlphaNotice'
import { GenericSkeletonLoader } from 'components/ui/ShimmeringLoader'
import { useAnalyticsBucketsQuery } from 'data/storage/analytics-buckets-query'
import { AnalyticsBucket as AnalyticsBucketIcon } from 'icons'
@@ -23,7 +24,6 @@ import {
} from 'ui'
import { TimestampInfo } from 'ui-patterns'
import { Input } from 'ui-patterns/DataInputs/Input'
-import { AlphaNotice } from '../AlphaNotice'
import { EmptyBucketState } from '../EmptyBucketState'
import { CreateAnalyticsBucketModal } from './CreateAnalyticsBucketModal'
@@ -64,7 +64,10 @@ export const AnalyticsBuckets = () => {
return (
-
+
{isLoadingBuckets && }
diff --git a/apps/studio/components/interfaces/Storage/BucketsUpgradePlan.tsx b/apps/studio/components/interfaces/Storage/BucketsUpgradePlan.tsx
index 337323a4c343f..ee18861f14666 100644
--- a/apps/studio/components/interfaces/Storage/BucketsUpgradePlan.tsx
+++ b/apps/studio/components/interfaces/Storage/BucketsUpgradePlan.tsx
@@ -1,13 +1,20 @@
import { ScaffoldSection } from 'components/layouts/Scaffold'
+import { AlphaNotice } from 'components/ui/AlphaNotice'
import { UpgradePlanButton } from 'components/ui/UpgradePlanButton'
import { AnalyticsBucket as AnalyticsBucketIcon, VectorBucket as VectorBucketIcon } from 'icons'
-import { AlphaNotice } from './AlphaNotice'
import { BUCKET_TYPES } from './Storage.constants'
export const BucketsUpgradePlan = ({ type }: { type: 'analytics' | 'vector' }) => {
return (
-
+
} className="mt-2">
-
+
Share feedback
diff --git a/apps/studio/components/ui/ProductMenu/ProductMenuItem.tsx b/apps/studio/components/ui/ProductMenu/ProductMenuItem.tsx
index cee69af89c43e..83e1e51c83df4 100644
--- a/apps/studio/components/ui/ProductMenu/ProductMenuItem.tsx
+++ b/apps/studio/components/ui/ProductMenu/ProductMenuItem.tsx
@@ -38,7 +38,7 @@ const ProductMenuItem = ({
>
{name}
{label !== undefined && (
-
+
{label}
)}
diff --git a/apps/studio/data/api-keys/temp-api-keys-query.ts b/apps/studio/data/api-keys/temp-api-keys-query.ts
index 44e821a62e6c3..0f4de86ef2ff6 100644
--- a/apps/studio/data/api-keys/temp-api-keys-query.ts
+++ b/apps/studio/data/api-keys/temp-api-keys-query.ts
@@ -1,8 +1,4 @@
-import { useQuery } from '@tanstack/react-query'
-
import { handleError, post } from 'data/fetchers'
-import type { ResponseError, UseCustomQueryOptions } from 'types'
-import { apiKeysKeys } from './keys'
interface getTemporaryAPIKeyVariables {
projectRef?: string
@@ -31,23 +27,3 @@ export async function getTemporaryAPIKey(
if (error) handleError(error)
return data
}
-
-export type TemporaryAPIKeyData = Awaited>
-
-export const useTemporaryAPIKeyQuery = (
- { projectRef }: { projectRef?: string },
- {
- enabled = true,
- ...options
- }: UseCustomQueryOptions = {}
-) => {
- // The expiry time is set to 60 seconds in the API.
- const expiry = 60
- return useQuery({
- queryKey: apiKeysKeys.temporary(projectRef),
- queryFn: ({ signal }) => getTemporaryAPIKey({ projectRef, expiry }, signal),
- enabled: enabled && typeof projectRef !== 'undefined',
- refetchInterval: expiry * 1000, // convert to ms
- ...options,
- })
-}
diff --git a/apps/studio/data/config/project-endpoint-query.ts b/apps/studio/data/config/project-endpoint-query.ts
new file mode 100644
index 0000000000000..3407df5995a39
--- /dev/null
+++ b/apps/studio/data/config/project-endpoint-query.ts
@@ -0,0 +1,17 @@
+import { IS_PLATFORM } from 'common'
+import { ProjectSettingsVariables, useProjectSettingsV2Query } from './project-settings-v2-query'
+
+export const useProjectEndpointQuery = ({ projectRef }: ProjectSettingsVariables) => {
+ return useProjectSettingsV2Query(
+ { projectRef },
+ {
+ select: (data) => {
+ const protocol = data?.app_config?.protocol ?? 'https'
+ const endpoint = data?.app_config?.endpoint
+ const clientEndpoint = `${IS_PLATFORM ? 'https' : protocol}://${endpoint}`
+
+ return { endpoint: clientEndpoint }
+ },
+ }
+ )
+}
diff --git a/apps/studio/data/config/project-settings-v2-query.ts b/apps/studio/data/config/project-settings-v2-query.ts
index 4bced094db3d5..c86950615f71d 100644
--- a/apps/studio/data/config/project-settings-v2-query.ts
+++ b/apps/studio/data/config/project-settings-v2-query.ts
@@ -53,8 +53,8 @@ export const useProjectSettingsV2Query = (
queryKey: configKeys.settingsV2(projectRef),
queryFn: ({ signal }) => getProjectSettings({ projectRef }, signal),
enabled: enabled && typeof projectRef !== 'undefined',
- refetchInterval(_data) {
- const data = _data as ProjectSettingsData | undefined
+ refetchInterval: (_, query) => {
+ const data = query.state.data
const apiKeys = data?.service_api_keys ?? []
const interval =
canReadAPIKeys && data?.status !== 'INACTIVE' && apiKeys.length === 0 ? 2000 : 0
diff --git a/apps/studio/data/oauth-server-apps/keys.ts b/apps/studio/data/oauth-server-apps/keys.ts
index 34df6f8cca124..d84544f1d2eed 100644
--- a/apps/studio/data/oauth-server-apps/keys.ts
+++ b/apps/studio/data/oauth-server-apps/keys.ts
@@ -1,4 +1,5 @@
export const oauthServerAppKeys = {
// temporaryApiKey has to be added to reset the query when it changes
- list: (projectRef: string | undefined) => ['projects', projectRef, 'oauth-server-apps'] as const,
+ list: (projectRef: string | undefined, clientEndpoint: string | undefined) =>
+ ['projects', projectRef, 'oauth-server-apps', clientEndpoint] as const,
}
diff --git a/apps/studio/data/oauth-server-apps/oauth-server-app-create-mutation.ts b/apps/studio/data/oauth-server-apps/oauth-server-app-create-mutation.ts
index 231e588774836..e3b1fc0dffa37 100644
--- a/apps/studio/data/oauth-server-apps/oauth-server-app-create-mutation.ts
+++ b/apps/studio/data/oauth-server-apps/oauth-server-app-create-mutation.ts
@@ -1,24 +1,26 @@
-import { CreateOAuthClientParams, SupabaseClient } from '@supabase/supabase-js'
+import { CreateOAuthClientParams } from '@supabase/supabase-js'
import { useMutation, useQueryClient } from '@tanstack/react-query'
import { toast } from 'sonner'
import { handleError } from 'data/fetchers'
+import { createProjectSupabaseClient } from 'lib/project-supabase-client'
import type { ResponseError, UseCustomMutationOptions } from 'types'
import { oauthServerAppKeys } from './keys'
export type OAuthServerAppCreateVariables = CreateOAuthClientParams & {
- projectRef?: string
- supabaseClient?: SupabaseClient
+ projectRef: string | undefined
+ clientEndpoint: string | undefined
}
export async function createOAuthServerApp({
projectRef,
- supabaseClient,
+ clientEndpoint,
...params
}: OAuthServerAppCreateVariables) {
if (!projectRef) throw new Error('Project reference is required')
- if (!supabaseClient) throw new Error('Supabase client is required')
+ if (!clientEndpoint) throw new Error('Client endpoint is required')
+ const supabaseClient = await createProjectSupabaseClient(projectRef, clientEndpoint)
const { data, error } = await supabaseClient.auth.admin.oauth.createClient(params)
if (error) return handleError(error)
@@ -40,9 +42,9 @@ export const useOAuthServerAppCreateMutation = ({
return useMutation({
mutationFn: (vars) => createOAuthServerApp(vars),
onSuccess: async (data, variables, context) => {
- const { projectRef } = variables
+ const { projectRef, clientEndpoint } = variables
await queryClient.invalidateQueries({
- queryKey: oauthServerAppKeys.list(projectRef),
+ queryKey: oauthServerAppKeys.list(projectRef, clientEndpoint),
})
await onSuccess?.(data, variables, context)
},
diff --git a/apps/studio/data/oauth-server-apps/oauth-server-app-delete-mutation.ts b/apps/studio/data/oauth-server-apps/oauth-server-app-delete-mutation.ts
index c76af7b7621a6..ec82f0b146c9d 100644
--- a/apps/studio/data/oauth-server-apps/oauth-server-app-delete-mutation.ts
+++ b/apps/studio/data/oauth-server-apps/oauth-server-app-delete-mutation.ts
@@ -1,28 +1,29 @@
-import { SupabaseClient } from '@supabase/supabase-js'
import { useMutation, useQueryClient } from '@tanstack/react-query'
import { toast } from 'sonner'
import { handleError } from 'data/fetchers'
+import { createProjectSupabaseClient } from 'lib/project-supabase-client'
import type { ResponseError, UseCustomMutationOptions } from 'types'
import { oauthServerAppKeys } from './keys'
export type OAuthServerAppDeleteVariables = {
- clientId?: string
- projectRef?: string
- supabaseClient?: SupabaseClient
+ clientId: string | undefined
+ projectRef: string | undefined
+ clientEndpoint: string | undefined
}
export async function deleteOAuthServerApp({
projectRef,
- supabaseClient,
+ clientEndpoint,
clientId,
}: OAuthServerAppDeleteVariables) {
if (!projectRef) throw new Error('Project reference is required')
- if (!supabaseClient) throw new Error('Supabase client is required')
+ if (!clientEndpoint) throw new Error('Client endpoint is required')
if (!clientId) throw new Error('Client ID is required')
+ const supabaseClient = await createProjectSupabaseClient(projectRef, clientEndpoint)
const { data, error } = await supabaseClient.auth.admin.oauth.deleteClient(clientId)
- console.log(data, error)
+
if (error) return handleError(error)
return null
}
@@ -42,9 +43,9 @@ export const useOAuthServerAppDeleteMutation = ({
return useMutation({
mutationFn: (vars) => deleteOAuthServerApp(vars),
onSuccess: async (data, variables, context) => {
- const { projectRef } = variables
+ const { projectRef, clientEndpoint } = variables
await queryClient.invalidateQueries({
- queryKey: oauthServerAppKeys.list(projectRef),
+ queryKey: oauthServerAppKeys.list(projectRef, clientEndpoint),
})
await onSuccess?.(data, variables, context)
},
diff --git a/apps/studio/data/oauth-server-apps/oauth-server-app-regenerate-secret-mutation.ts b/apps/studio/data/oauth-server-apps/oauth-server-app-regenerate-secret-mutation.ts
index 3409aad8793ce..62f5a2e2647d7 100644
--- a/apps/studio/data/oauth-server-apps/oauth-server-app-regenerate-secret-mutation.ts
+++ b/apps/studio/data/oauth-server-apps/oauth-server-app-regenerate-secret-mutation.ts
@@ -1,26 +1,27 @@
-import { SupabaseClient } from '@supabase/supabase-js'
import { useMutation, useQueryClient } from '@tanstack/react-query'
import { toast } from 'sonner'
import { handleError } from 'data/fetchers'
+import { createProjectSupabaseClient } from 'lib/project-supabase-client'
import type { ResponseError, UseCustomMutationOptions } from 'types'
import { oauthServerAppKeys } from './keys'
export type OAuthServerAppRegenerateSecretVariables = {
- projectRef?: string
- supabaseClient?: SupabaseClient
- clientId: string
+ projectRef: string | undefined
+ clientId: string | undefined
+ clientEndpoint: string | undefined
}
export async function regenerateSecret({
projectRef,
- supabaseClient,
+ clientEndpoint,
clientId,
}: OAuthServerAppRegenerateSecretVariables) {
if (!projectRef) throw new Error('Project reference is required')
- if (!supabaseClient) throw new Error('Supabase client is required')
+ if (!clientEndpoint) throw new Error('Client endpoint is required')
if (!clientId) throw new Error('Oauth app client id is required')
+ const supabaseClient = await createProjectSupabaseClient(projectRef, clientEndpoint)
const { data, error } = await supabaseClient.auth.admin.oauth.regenerateClientSecret(clientId)
if (error) handleError(error)
@@ -50,9 +51,9 @@ export const useOAuthServerAppRegenerateSecretMutation = ({
>({
mutationFn: (vars) => regenerateSecret(vars),
onSuccess: async (data, variables, context) => {
- const { projectRef } = variables
+ const { projectRef, clientEndpoint } = variables
await queryClient.invalidateQueries({
- queryKey: oauthServerAppKeys.list(projectRef),
+ queryKey: oauthServerAppKeys.list(projectRef, clientEndpoint),
})
await onSuccess?.(data, variables, context)
},
diff --git a/apps/studio/data/oauth-server-apps/oauth-server-app-update-mutation.ts b/apps/studio/data/oauth-server-apps/oauth-server-app-update-mutation.ts
index e3d6b7bd41ee5..282205d7eb6d2 100644
--- a/apps/studio/data/oauth-server-apps/oauth-server-app-update-mutation.ts
+++ b/apps/studio/data/oauth-server-apps/oauth-server-app-update-mutation.ts
@@ -1,26 +1,29 @@
-import { UpdateOAuthClientParams, SupabaseClient } from '@supabase/supabase-js'
+import { UpdateOAuthClientParams } from '@supabase/supabase-js'
import { useMutation, useQueryClient } from '@tanstack/react-query'
import { toast } from 'sonner'
import { handleError } from 'data/fetchers'
+import { createProjectSupabaseClient } from 'lib/project-supabase-client'
import type { ResponseError, UseCustomMutationOptions } from 'types'
import { oauthServerAppKeys } from './keys'
export type OAuthServerAppUpdateVariables = UpdateOAuthClientParams & {
- clientId: string
- projectRef?: string
- supabaseClient?: SupabaseClient
+ clientId: string | undefined
+ projectRef: string | undefined
+ clientEndpoint: string | undefined
}
export async function updateOAuthServerApp({
clientId,
projectRef,
- supabaseClient,
+ clientEndpoint,
...params
}: OAuthServerAppUpdateVariables) {
if (!projectRef) throw new Error('Project reference is required')
- if (!supabaseClient) throw new Error('Supabase client is required')
+ if (!clientEndpoint) throw new Error('Client endpoint is required')
+ if (!clientId) throw new Error('Client ID is required')
+ const supabaseClient = await createProjectSupabaseClient(projectRef, clientEndpoint)
const { data, error } = await supabaseClient.auth.admin.oauth.updateClient(clientId, params)
if (error) return handleError(error)
@@ -42,9 +45,9 @@ export const useOAuthServerAppUpdateMutation = ({
return useMutation({
mutationFn: (vars) => updateOAuthServerApp(vars),
onSuccess: async (data, variables, context) => {
- const { projectRef } = variables
+ const { projectRef, clientEndpoint } = variables
await queryClient.invalidateQueries({
- queryKey: oauthServerAppKeys.list(projectRef),
+ queryKey: oauthServerAppKeys.list(projectRef, clientEndpoint),
})
await onSuccess?.(data, variables, context)
},
diff --git a/apps/studio/data/oauth-server-apps/oauth-server-apps-query.ts b/apps/studio/data/oauth-server-apps/oauth-server-apps-query.ts
index 9e8348da037b1..4fc560c00378e 100644
--- a/apps/studio/data/oauth-server-apps/oauth-server-apps-query.ts
+++ b/apps/studio/data/oauth-server-apps/oauth-server-apps-query.ts
@@ -1,14 +1,14 @@
-import { SupabaseClient } from '@supabase/supabase-js'
import { useQuery } from '@tanstack/react-query'
import { components } from 'api-types'
+import { useProjectEndpointQuery } from 'data/config/project-endpoint-query'
import { handleError } from 'data/fetchers'
+import { createProjectSupabaseClient } from 'lib/project-supabase-client'
import type { ResponseError, UseCustomQueryOptions } from 'types'
import { oauthServerAppKeys } from './keys'
export type OAuthServerAppsVariables = {
- projectRef?: string
- supabaseClient?: SupabaseClient
+ projectRef: string | undefined
page?: number
}
@@ -18,11 +18,13 @@ export type OAuthApp = components['schemas']['OAuthAppResponse']
export async function getOAuthServerApps({
projectRef,
- supabaseClient,
+ clientEndpoint,
page = 1,
-}: OAuthServerAppsVariables) {
+}: OAuthServerAppsVariables & { clientEndpoint: string | undefined }) {
if (!projectRef) throw new Error('Project reference is required')
- if (!supabaseClient) throw new Error('Supabase client is required')
+ if (!clientEndpoint) throw new Error('Client endpoint is required')
+
+ const supabaseClient = await createProjectSupabaseClient(projectRef, clientEndpoint)
const { data, error } = await supabaseClient.auth.admin.oauth.listClients({
page,
@@ -37,16 +39,21 @@ export type OAuthServerAppsData = Awaited>
export type OAuthServerAppsError = ResponseError
export const useOAuthServerAppsQuery = (
- { projectRef, supabaseClient }: OAuthServerAppsVariables,
+ { projectRef }: OAuthServerAppsVariables,
{
enabled = true,
...options
}: UseCustomQueryOptions = {}
) => {
+ const { data: endpointData } = useProjectEndpointQuery({
+ projectRef,
+ })
+ const clientEndpoint = endpointData?.endpoint
+
return useQuery({
- queryKey: oauthServerAppKeys.list(projectRef),
- queryFn: () => getOAuthServerApps({ projectRef, supabaseClient }),
- enabled: enabled && typeof projectRef !== 'undefined' && !!supabaseClient,
+ queryKey: oauthServerAppKeys.list(projectRef, clientEndpoint),
+ queryFn: () => getOAuthServerApps({ projectRef, clientEndpoint }),
+ enabled: enabled && typeof projectRef !== 'undefined' && !!clientEndpoint,
...options,
})
}
diff --git a/apps/studio/hooks/use-supabase-client-query.ts b/apps/studio/hooks/use-supabase-client-query.ts
deleted file mode 100644
index 032214560bc24..0000000000000
--- a/apps/studio/hooks/use-supabase-client-query.ts
+++ /dev/null
@@ -1,54 +0,0 @@
-import { createClient } from '@supabase/supabase-js'
-import { useQuery } from '@tanstack/react-query'
-
-import { useTemporaryAPIKeyQuery } from 'data/api-keys/temp-api-keys-query'
-import { useProjectSettingsV2Query } from 'data/config/project-settings-v2-query'
-
-const getSupabaseClient = ({
- projectRef,
- endpoint,
- temporaryApiKey,
-}: {
- projectRef?: string
- endpoint?: string
- temporaryApiKey?: string
-}) => {
- if (!projectRef) {
- return undefined
- }
- if (!endpoint) {
- return undefined
- }
-
- if (temporaryApiKey === undefined) {
- return undefined
- }
-
- const supabaseClient = createClient(endpoint, temporaryApiKey)
-
- return { supabaseClient }
-}
-
-/**
- * The client uses a temporary API key to authenticate requests. The API key expires after one hour which may cause
- * 401 errors for all requests made with the client. It's easily fixable by refreshing the page.
- */
-export const useSupabaseClientQuery = (
- { projectRef }: { projectRef?: string },
- { enabled = true, ...options } = {}
-) => {
- const { data: settings } = useProjectSettingsV2Query({ projectRef })
- const { data: temporaryApiKeyData } = useTemporaryAPIKeyQuery({ projectRef })
-
- const endpoint = settings
- ? `${settings?.app_config?.protocol ?? 'https'}://${settings?.app_config?.endpoint}`
- : undefined
- const temporaryApiKey = temporaryApiKeyData?.api_key
-
- return useQuery({
- queryKey: [projectRef, 'supabase-client', endpoint, temporaryApiKey],
- queryFn: () => getSupabaseClient({ projectRef, endpoint, temporaryApiKey }),
- enabled: enabled && typeof projectRef !== 'undefined' && !!endpoint && !!temporaryApiKey,
- ...options,
- })
-}
diff --git a/apps/studio/lib/project-supabase-client.ts b/apps/studio/lib/project-supabase-client.ts
new file mode 100644
index 0000000000000..7031cb4c6f242
--- /dev/null
+++ b/apps/studio/lib/project-supabase-client.ts
@@ -0,0 +1,28 @@
+import { createClient } from '@supabase/supabase-js'
+import { getOrRefreshTemporaryApiKey } from 'data/api-keys/temp-api-keys-utils'
+
+/**
+ * Creates a Supabase client bound to a specific project. It uses temporary API key.
+ */
+export async function createProjectSupabaseClient(projectRef: string, clientEndpoint: string) {
+ try {
+ const { apiKey } = await getOrRefreshTemporaryApiKey(projectRef)
+
+ return createClient(clientEndpoint, apiKey, {
+ auth: {
+ persistSession: false,
+ autoRefreshToken: false,
+ detectSessionInUrl: false,
+ storage: {
+ getItem: (key) => {
+ return null
+ },
+ setItem: (key, value) => {},
+ removeItem: (key) => {},
+ },
+ },
+ })
+ } catch (error) {
+ throw error
+ }
+}
diff --git a/apps/studio/pages/project/[ref]/database/replication/index.tsx b/apps/studio/pages/project/[ref]/database/replication/index.tsx
index f35eb3395b32c..ba573879ff32c 100644
--- a/apps/studio/pages/project/[ref]/database/replication/index.tsx
+++ b/apps/studio/pages/project/[ref]/database/replication/index.tsx
@@ -5,6 +5,7 @@ import { useIsETLPrivateAlpha } from 'components/interfaces/Database/Replication
import DatabaseLayout from 'components/layouts/DatabaseLayout/DatabaseLayout'
import DefaultLayout from 'components/layouts/DefaultLayout'
import { ScaffoldContainer, ScaffoldSection } from 'components/layouts/Scaffold'
+import { AlphaNotice } from 'components/ui/AlphaNotice'
import { FormHeader } from 'components/ui/Forms/FormHeader'
import { UnknownInterface } from 'components/ui/UnknownInterface'
import { useIsFeatureEnabled } from 'hooks/misc/useIsFeatureEnabled'
@@ -27,10 +28,18 @@ const DatabaseReplicationPage: NextPageWithLayout = () => {
-
+
+
Replication
+
+
+ Automatically replicate your database changes to external data warehouses and
+ analytics platforms in real-time
+
+
+
diff --git a/apps/studio/state/storage-explorer.tsx b/apps/studio/state/storage-explorer.tsx
index b0374992a6eeb..43e30e45029bb 100644
--- a/apps/studio/state/storage-explorer.tsx
+++ b/apps/studio/state/storage-explorer.tsx
@@ -1,3 +1,4 @@
+import { BlobReader, BlobWriter, ZipWriter } from '@zip.js/zip.js'
import { capitalize, chunk, compact, find, findIndex, has, isObject, uniq, uniqBy } from 'lodash'
import { createContext, PropsWithChildren, useContext, useEffect, useState } from 'react'
import { useLatest } from 'react-use'
@@ -5,8 +6,6 @@ import { toast } from 'sonner'
import * as tus from 'tus-js-client'
import { proxy, useSnapshot } from 'valtio'
-import { createClient } from '@supabase/supabase-js'
-import { BlobReader, BlobWriter, ZipWriter } from '@zip.js/zip.js'
import { LOCAL_STORAGE_KEYS } from 'common'
import {
inverseValidObjectKeyRegex,
@@ -37,7 +36,7 @@ import { convertFromBytes } from 'components/interfaces/Storage/StorageSettings/
import { InlineLink } from 'components/ui/InlineLink'
import { getOrRefreshTemporaryApiKey } from 'data/api-keys/temp-api-keys-utils'
import { configKeys } from 'data/config/keys'
-import { useProjectSettingsV2Query } from 'data/config/project-settings-v2-query'
+import { useProjectEndpointQuery } from 'data/config/project-endpoint-query'
import { ProjectStorageConfigResponse } from 'data/config/project-storage-config-query'
import { getQueryClient } from 'data/query-client'
import { deleteBucketObject } from 'data/storage/bucket-object-delete-mutation'
@@ -49,6 +48,7 @@ import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject'
import { IS_PLATFORM, PROJECT_STATUS } from 'lib/constants'
import { tryParseJson } from 'lib/helpers'
import { lookupMime } from 'lib/mime'
+import { createProjectSupabaseClient } from 'lib/project-supabase-client'
import { Button, SONNER_DEFAULT_DURATION, SonnerProgress } from 'ui'
type UploadProgress = {
@@ -76,29 +76,6 @@ if (typeof window !== 'undefined') {
abortController = new AbortController()
}
-async function createSupabaseClient(projectRef: string, clientEndpoint: string) {
- try {
- const { apiKey } = await getOrRefreshTemporaryApiKey(projectRef)
-
- return createClient(clientEndpoint, apiKey, {
- auth: {
- persistSession: false,
- autoRefreshToken: false,
- detectSessionInUrl: false,
- storage: {
- getItem: (key) => {
- return null
- },
- setItem: (key, value) => {},
- removeItem: (key) => {},
- },
- },
- })
- } catch (error) {
- throw error
- }
-}
-
function createStorageExplorerState({
projectRef,
connectionString,
@@ -345,7 +322,7 @@ function createStorageExplorerState({
const formattedPathToEmptyPlaceholderFile =
pathToFolder.length > 0 ? `${pathToFolder}/${emptyPlaceholderFile}` : emptyPlaceholderFile
- const client = await createSupabaseClient(state.projectRef, clientEndpoint)
+ const client = await createProjectSupabaseClient(state.projectRef, clientEndpoint)
await client.storage
.from(state.selectedBucket.name)
.upload(
@@ -596,7 +573,7 @@ function createStorageExplorerState({
if (data.length === 0) {
const prefixToPlaceholder = `${parentFolderPrefix}/${EMPTY_FOLDER_PLACEHOLDER_FILE_NAME}`
- const client = await createSupabaseClient(state.projectRef, clientEndpoint)
+ const client = await createProjectSupabaseClient(state.projectRef, clientEndpoint)
await client.storage
.from(state.selectedBucket.name)
.upload(prefixToPlaceholder, new File([], EMPTY_FOLDER_PLACEHOLDER_FILE_NAME))
@@ -831,11 +808,8 @@ function createStorageExplorerState({
>(async (resolve) => {
try {
// Get authenticated Supabase client for Storage API access
- const client = await createSupabaseClient(state.projectRef, clientEndpoint)
+ const client = await createProjectSupabaseClient(state.projectRef, clientEndpoint)
- if (!client) {
- throw new Error('Supabase client not available')
- }
// Use Storage API directly instead of Management API to avoid throttling
const { data, error } = await client.storage
.from(state.selectedBucket.id)
@@ -1517,10 +1491,7 @@ function createStorageExplorerState({
const toastId = showToast ? toast.loading(`Retrieving ${fileName}...`) : undefined
try {
- const client = await createSupabaseClient(state.projectRef, clientEndpoint)
- if (!client) {
- throw new Error('Supabase client not available')
- }
+ const client = await createProjectSupabaseClient(state.projectRef, clientEndpoint)
// Use Storage API directly instead of Management API to avoid throttling
const { data, error } = await client.storage
@@ -1564,10 +1535,7 @@ function createStorageExplorerState({
if (!file.path) return false
try {
- const client = await createSupabaseClient(state.projectRef, clientEndpoint)
- if (!client) {
- throw new Error('Supabase client not available')
- }
+ const client = await createProjectSupabaseClient(state.projectRef, clientEndpoint)
const { data, error } = await client.storage
.from(state.selectedBucket.id)
@@ -1902,15 +1870,10 @@ export const StorageExplorerStateContextProvider = ({ children }: PropsWithChild
const [state, setState] = useState(() => createStorageExplorerState(DEFAULT_STATE_CONFIG))
const stateRef = useLatest(state)
- const { data: settings, isSuccess: isSuccessSettings } = useProjectSettingsV2Query({
+ const { data: endpointData, isSuccess: isSuccessSettings } = useProjectEndpointQuery({
projectRef: project?.ref,
})
- const protocol = settings?.app_config?.protocol ?? 'https'
- const endpoint = settings?.app_config?.endpoint
- const resumableUploadUrl = `${IS_PLATFORM ? 'https' : protocol}://${endpoint}/storage/v1/upload/resumable`
- const clientEndpoint = `${IS_PLATFORM ? 'https' : protocol}://${endpoint}`
-
// [Joshen] JFYI opting with the useEffect here as the storage explorer state was being loaded
// before the project details were ready, hence the store kept returning project ref as undefined
// Can be verified when we're saving the storage explorer preferences into local storage, that ref is undefined
@@ -1921,6 +1884,8 @@ export const StorageExplorerStateContextProvider = ({ children }: PropsWithChild
const storeAlreadyLoaded = state.projectRef === project?.ref
if (!isPaused && hasDataReady && !storeAlreadyLoaded && isSuccessSettings) {
+ const clientEndpoint = endpointData.endpoint
+ const resumableUploadUrl = `${clientEndpoint}/storage/v1/upload/resumable`
setState(
createStorageExplorerState({
projectRef: project?.ref ?? '',
@@ -1936,9 +1901,7 @@ export const StorageExplorerStateContextProvider = ({ children }: PropsWithChild
project?.connectionString,
stateRef,
isPaused,
- resumableUploadUrl,
- protocol,
- endpoint,
+ endpointData?.endpoint,
isSuccessSettings,
])
diff --git a/apps/www/components/Pricing/PricingTableRow.tsx b/apps/www/components/Pricing/PricingTableRow.tsx
index 9e92602131832..3d85ca7adddc4 100644
--- a/apps/www/components/Pricing/PricingTableRow.tsx
+++ b/apps/www/components/Pricing/PricingTableRow.tsx
@@ -63,6 +63,7 @@ export const pricingTooltips: PricingTooltips = {
'auth.thirdPartyMAUs': {
main: 'Users who use the Supabase platform through a third-party authentication provider (Firebase Auth, Auth0 or Cognito).\nBilling is based on the sum of distinct third-party users requesting your API through the billing period. Resets every billing cycle.',
},
+
'storage.size': {
main: "The sum of all objects' size in your storage buckets.\nBilling is prorated down to the hour and will be displayed as GB-Hrs on your invoice.",
},
@@ -108,6 +109,35 @@ export const pricingTooltips: PricingTooltips = {
'security.customDomains': {
enterprise: 'Volume discounts available.',
},
+
+ 'auth.auditLogs': {
+ main: (
+
+ Auth Audit Logs provide comprehensive tracking of authentication events. Audit logs are
+ automatically captured for all authentication events and help you monitor user
+ authentication activities, detect suspicious behavior, and maintain compliance with security
+ requirements. Read more in our{' '}
+
+ docs
+
+ .
+
+ ),
+ },
+
+ 'security.platformAuditLogs': {
+ main: (
+
+ Any Platform API/Dashboard actions performed by organization members are logged
+ automatically for auditing and security purposes. Includes actions such as creating a new
+ project, inviting members or changing project settings. Read more in our{' '}
+
+ docs
+
+ .
+
+ ),
+ },
}
export const PricingTableRowDesktop = (props: any) => {
diff --git a/docker/CHANGELOG.md b/docker/CHANGELOG.md
index 15e0e8f41c044..b66438d1506a5 100644
--- a/docker/CHANGELOG.md
+++ b/docker/CHANGELOG.md
@@ -5,24 +5,43 @@ All notable changes to the Supabase self-hosted Docker configuration.
Changes are grouped by service rather than by change type. See [versions.md](./versions.md)
for complete image version history and rollback information.
-Check updates, changelogs, and release notes for each service to learn more.
+Check updates for each service to learn more.
**Note:** Configuration updates marked with "requires [...] update" are already included in the latest version of the repository. Pull the latest changes or refer to the linked PR for manual updates. After updating `docker-compose.yml`, pull latest images and recreate containers - use `docker compose down && docker compose pull && docker compose up -d`.
+---
+
## Unreleased
[...]
---
+## [2025-11-26]
+
+### Studio
+- Updated to `2025.11.26-sha-8f096b5`
+- Fixed MCP `get_advisors` tool - PR [#40783](https://github.com/supabase/supabase/pull/40783)
+- Fixed AI Assistant request schema - PR [#40830](https://github.com/supabase/supabase/pull/40830)
+- Fixed log drains page - PR [#40835](https://github.com/supabase/supabase/pull/40835)
+
+### Realtime
+- Updated to `v2.65.3` - [Release](https://github.com/supabase/realtime/releases/tag/v2.65.3)
+
+### Analytics (Logflare)
+- Updated to `v1.26.13` - [Release](https://github.com/Logflare/logflare/releases/tag/v1.26.13)
+- Fixed crashdump when `POSTGRES_BACKEND_URL` is malformed - PR [logflare#2954](https://github.com/Logflare/logflare/pull/2954)
+
+---
+
## [2025-11-25]
### Studio
- Updated to `2025.11.24-sha-d990ae8` - [Dashboard updates](https://github.com/orgs/supabase/discussions/40734)
-- Fixed Queues configuration UI and added [documentation for exposed queue schema](https://supabase.com/docs/guides/queues/expose-self-hosted-queues) - [PR #40078](https://github.com/supabase/supabase/pull/40078)
+- Fixed Queues configuration UI and added [documentation for exposed queue schema](https://supabase.com/docs/guides/queues/expose-self-hosted-queues) - PR [#40078](https://github.com/supabase/supabase/pull/40078)
+- Fixed parameterized SQL queries in MCP tools - PR [#40499](https://github.com/supabase/supabase/pull/40499)
- Fixed Studio showing paid options for log drains - [PR #40510](https://github.com/supabase/supabase/pull/40510)
-- Fixed parameterized SQL queries in MCP tools - [PR #40499](https://github.com/supabase/supabase/pull/40499)
-- Fixed AI Assistant - [PR #40654](https://github.com/supabase/supabase/pull/40654)
+- Fixed AI Assistant authentication - PR [#40654](https://github.com/supabase/supabase/pull/40654)
### Auth
- Updated to `v2.183.0` - [Changelog](https://github.com/supabase/auth/blob/master/CHANGELOG.md) | [Release](https://github.com/supabase/auth/releases/tag/v2.183.0)
@@ -37,16 +56,17 @@ Check updates, changelogs, and release notes for each service to learn more.
- Updated to `v1.69.25` - [Release](https://github.com/supabase/edge-runtime/releases/tag/v1.69.25)
### Analytics (Logflare)
-- Updated to `1.26.12` - [Release](https://github.com/Logflare/logflare/releases/tag/v1.26.12)
-- Fixed Auth logs query - [PR #2936](https://github.com/Logflare/logflare/pull/2936)
-- Fixed build configuration to prevent crashes with "Illegal instruction (core dumped)" - [PR #2942](https://github.com/Logflare/logflare/pull/2942)
+- Updated to `v1.26.12` - [Release](https://github.com/Logflare/logflare/releases/tag/v1.26.12)
+- Fixed Auth logs query - PR [logflare#2936](https://github.com/Logflare/logflare/pull/2936)
+- Fixed build configuration to prevent crashes with "Illegal instruction (core dumped)" - PR [logflare#2942](https://github.com/Logflare/logflare/pull/2942)
---
## [2025-11-17]
### Storage
-- Fixed resumable uploads for files larger than 6MB (requires `docker-compose.yml` update) - [PR #40500](https://github.com/supabase/supabase/pull/40500)
+- No image update
+- Fixed resumable uploads for files larger than 6MB (requires `docker-compose.yml` update) - PR [#40500](https://github.com/supabase/supabase/pull/40500)
---
@@ -54,8 +74,8 @@ Check updates, changelogs, and release notes for each service to learn more.
### Studio
- Updated to `2025.11.10-sha-5291fe3` - [Dashboard updates](https://github.com/orgs/supabase/discussions/40083)
-- Added log drains - [PR #28297](https://github.com/supabase/supabase/pull/28297)
-- Fixed Studio using `postgres` role instead of `supabase_admin` - [PR #39946](https://github.com/supabase/supabase/pull/39946)
+- Added log drains - PR [#28297](https://github.com/supabase/supabase/pull/28297)
+- Fixed Studio using `postgres` role instead of `supabase_admin` - PR [#39946](https://github.com/supabase/supabase/pull/39946)
### Auth
- Updated to `v2.182.1` - [Changelog](https://github.com/supabase/auth/blob/master/CHANGELOG.md#21821-2025-11-05) | [Release](https://github.com/supabase/auth/releases/tag/v2.182.1)
@@ -70,17 +90,19 @@ Check updates, changelogs, and release notes for each service to learn more.
- Updated to `v1.69.23` - [Release](https://github.com/supabase/edge-runtime/releases/tag/v1.69.23)
### Supavisor
-- Updated to `2.7.4` - [Release](https://github.com/supabase/supavisor/releases/tag/v2.7.4)
+- Updated to `v2.7.4` - [Release](https://github.com/supabase/supavisor/releases/tag/v2.7.4)
---
## [2025-11-05]
### Studio
-- Fixed Studio failing to connect to Postgres with non-default settings (requires `docker-compose.yml` update) - [PR #40169](https://github.com/supabase/supabase/pull/40169)
+- No image update
+- Fixed Studio failing to connect to Postgres with non-default settings (requires `docker-compose.yml` update) - PR [#40169](https://github.com/supabase/supabase/pull/40169)
### Realtime
-- Fixed realtime logs not showing in Studio (requires `volumes/logs/vector.yml` update) - [PR #39963](https://github.com/supabase/supabase/pull/39963)
+- No image update
+- Fixed realtime logs not showing in Studio (requires `volumes/logs/vector.yml` update) - PR [#39963](https://github.com/supabase/supabase/pull/39963)
---
@@ -88,7 +110,7 @@ Check updates, changelogs, and release notes for each service to learn more.
### Studio
- Updated to `2025.10.27-sha-85b84e0` - [Dashboard updates](https://github.com/orgs/supabase/discussions/40083)
-- Fixed broken authentication when uploading files to Storage - [PR #39829](https://github.com/supabase/supabase/pull/39829)
+- Fixed broken authentication when uploading files to Storage - PR [#39829](https://github.com/supabase/supabase/pull/39829)
### Realtime
- Updated to `v2.57.2` - [Release](https://github.com/supabase/realtime/releases/tag/v2.57.2)
@@ -107,8 +129,9 @@ Check updates, changelogs, and release notes for each service to learn more.
## [2025-10-27]
### Studio
-- Added Kong configuration for MCP server routes (requires `volumes/api/kong.yml` update) - [PR #39849](https://github.com/supabase/supabase/pull/39849)
-- Added [documentation page](https://supabase.com/docs/guides/self-hosting/enable-mcp) for MCP server configuration - [PR #39952](https://github.com/supabase/supabase/pull/39952)
+- No image update
+- Added Kong configuration for MCP server routes (requires `volumes/api/kong.yml` update) - PR [#39849](https://github.com/supabase/supabase/pull/39849)
+- Added [documentation page](https://supabase.com/docs/guides/self-hosting/enable-mcp) for MCP server configuration - PR [#39952](https://github.com/supabase/supabase/pull/39952)
---
@@ -116,7 +139,7 @@ Check updates, changelogs, and release notes for each service to learn more.
### Studio
- Updated to `2025.10.20-sha-5005fc6` - [Dashboard updates](https://github.com/orgs/supabase/discussions/39709)
-- Fixed issues with Edge Functions and cron logs not being visible in Studio - [PR #39388](https://github.com/supabase/supabase/pull/39388), [PR #39704](https://github.com/supabase/supabase/pull/39704), [PR #39711](https://github.com/supabase/supabase/pull/39711)
+- Fixed issues with Edge Functions and cron logs not being visible in Studio - PR [#39388](https://github.com/supabase/supabase/pull/39388), PR [#39704](https://github.com/supabase/supabase/pull/39704), PR [#39711](https://github.com/supabase/supabase/pull/39711)
### Realtime
- Updated to `v2.56.0` - [Release](https://github.com/supabase/realtime/releases/tag/v2.56.0)
@@ -128,17 +151,17 @@ Check updates, changelogs, and release notes for each service to learn more.
- Updated to `v0.93.0` - [Release](https://github.com/supabase/postgres-meta/releases/tag/v0.93.0)
### Edge Runtime
-- Updated to `v1.69.14` - [Release](https://github.com/supabase/edge-runtime/releases/v1.69.14)
+- Updated to `v1.69.14` - [Release](https://github.com/supabase/edge-runtime/releases/tag/v1.69.14)
### Supavisor
-- Updated to `2.7.3` - [Release](https://github.com/supabase/supavisor/releases/tag/v2.7.3)
+- Updated to `v2.7.3` - [Release](https://github.com/supabase/supavisor/releases/tag/v2.7.3)
---
## [2025-10-13]
### Analytics (Logflare)
-- Updated to `1.22.6` - [Release](https://github.com/Logflare/logflare/releases/tag/v1.22.6)
+- Updated to `v1.22.6` - [Release](https://github.com/Logflare/logflare/releases/tag/v1.22.6)
---
@@ -146,7 +169,7 @@ Check updates, changelogs, and release notes for each service to learn more.
### Studio
- Updated to `2025.10.01-sha-8460121` - [Dashboard updates](https://github.com/orgs/supabase/discussions/39709)
-- Added "local" remote MCP server - [PR #38797](https://github.com/supabase/supabase/pull/38797), [PR #39041](https://github.com/supabase/supabase/pull/39041)
+- Added "local" remote MCP server - PR [#38797](https://github.com/supabase/supabase/pull/38797), PR [#39041](https://github.com/supabase/supabase/pull/39041)
- ⚠️ Changed Studio connection method to `postgres-meta` - affects non-standard database port configurations
### Auth
@@ -165,7 +188,7 @@ Check updates, changelogs, and release notes for each service to learn more.
- Updated to `v0.91.6` - [Release](https://github.com/supabase/postgres-meta/releases/tag/v0.91.6)
### Analytics (Logflare)
-- Updated to `1.22.4` - [Release](https://github.com/Logflare/logflare/releases/tag/v1.22.4)
+- Updated to `v1.22.4` - [Release](https://github.com/Logflare/logflare/releases/tag/v1.22.4)
### Postgres
- Updated to `15.8.1.085` - [Release](https://github.com/supabase/postgres/releases/tag/15.8.1.085)
diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml
index e3c025d7f4978..034f04e88600b 100644
--- a/docker/docker-compose.yml
+++ b/docker/docker-compose.yml
@@ -11,7 +11,7 @@ services:
studio:
container_name: supabase-studio
- image: supabase/studio:2025.11.25-sha-8de52c4
+ image: supabase/studio:2025.11.26-sha-8f096b5
restart: unless-stopped
healthcheck:
test:
diff --git a/docker/versions.md b/docker/versions.md
index 3753fe091164c..0c0b2e03861c6 100644
--- a/docker/versions.md
+++ b/docker/versions.md
@@ -1,5 +1,10 @@
# Docker Image Versions
+## 2025-11-26
+- supabase/studio:2025.11.26-sha-8f096b5 (prev supabase/studio:2025.11.24-sha-d990ae8)
+- supabase/realtime:v2.65.3 (prev supabase/realtime:v2.65.2)
+- supabase/logflare:1.26.13 (prev supabase/logflare:1.26.12)
+
## 2025-11-25
- supabase/studio:2025.11.24-sha-d990ae8 (prev supabase/studio:2025.11.10-sha-5291fe3)
- supabase/gotrue:v2.183.0 (prev supabase/gotrue:v2.182.1)
diff --git a/packages/shared-data/pricing.ts b/packages/shared-data/pricing.ts
index c7507541c637f..a4f4054bc6094 100644
--- a/packages/shared-data/pricing.ts
+++ b/packages/shared-data/pricing.ts
@@ -45,7 +45,7 @@ export type FeatureKey =
| 'auth.socialOAuthProviders'
| 'auth.customSMTPServer'
| 'auth.removeSupabaseBranding'
- | 'auth.auditTrails'
+ | 'auth.auditLogs'
| 'auth.basicMFA'
| 'auth.advancedMFAPhone'
| 'auth.thirdPartyMAUs'
@@ -69,7 +69,7 @@ export type FeatureKey =
| 'realtime.messagesPerMonth'
| 'realtime.maxMessageSize'
| 'dashboard.teamMembers'
- | 'dashboard.auditTrails'
+ | 'security.platformAuditLogs'
| 'security.byoc'
| 'security.logRetention'
| 'security.logDrain'
@@ -289,8 +289,8 @@ export const pricing: Pricing = {
usage_based: false,
},
{
- key: 'auth.auditTrails',
- title: 'Audit trails',
+ key: 'auth.auditLogs',
+ title: 'Auth Audit Logs',
plans: {
free: '1 hour',
pro: '7 days',
@@ -556,17 +556,6 @@ export const pricing: Pricing = {
},
usage_based: false,
},
- {
- key: 'dashboard.auditTrails',
- title: 'Audit trails',
- plans: {
- free: false,
- pro: false,
- team: true,
- enterprise: true,
- },
- usage_based: false,
- },
],
},
security: {
@@ -606,6 +595,17 @@ export const pricing: Pricing = {
},
usage_based: true,
},
+ {
+ key: 'security.platformAuditLogs',
+ title: 'Platform Audit Logs',
+ plans: {
+ free: false,
+ pro: false,
+ team: true,
+ enterprise: true,
+ },
+ usage_based: false,
+ },
{
key: 'security.metricsEndpoint',
title: 'Metrics endpoint',
diff --git a/packages/ui/src/components/radio-group-stacked.tsx b/packages/ui/src/components/radio-group-stacked.tsx
index faa6fbce5986f..86447f8bf89f5 100644
--- a/packages/ui/src/components/radio-group-stacked.tsx
+++ b/packages/ui/src/components/radio-group-stacked.tsx
@@ -38,22 +38,22 @@ const RadioGroupStackedItem = React.forwardRef<
ref={ref}
{...props}
className={cn(
- 'flex flex-col gap-2',
- 'w-full',
- 'bg-overlay/50 disabled:opacity-50',
- 'border',
+ // Base layout and sizing
+ 'flex flex-col gap-2 w-full',
+ // Base styles
+ 'bg-overlay/50 border shadow-sm',
'first-of-type:rounded-t-lg last-of-type:rounded-b-lg',
- 'shadow-sm',
- 'enabled:hover:bg-surface-300',
- 'enabled:hover:border-foreground-muted',
- 'enabled:cursor-pointer disabled:cursor-not-allowed',
- 'hover:z-[1] focus-visible:z-[1]',
- 'data-[state=checked]:z-[1]',
+ // Disabled state
+ 'disabled:opacity-50 disabled:cursor-not-allowed',
+ // Enabled/hover states
+ 'enabled:cursor-pointer enabled:hover:bg-surface-300 enabled:hover:border-foreground-muted',
+ // Z-index for interactions
+ 'hover:z-[1] focus-visible:z-[1] data-[state=checked]:z-[1]',
+ // Checked state
'data-[state=checked]:ring-1 data-[state=checked]:ring-border',
- 'data-[state=checked]:bg-surface-300',
- 'data-[state=checked]:border-foreground-muted',
- 'transition',
- 'group',
+ 'data-[state=checked]:bg-surface-300 data-[state=checked]:border-foreground-muted',
+ // Transitions and group
+ 'transition group',
props.className
)}
>
@@ -61,20 +61,19 @@ const RadioGroupStackedItem = React.forwardRef<
{showIndicator && (
-
+
)}
@@ -82,10 +81,11 @@ const RadioGroupStackedItem = React.forwardRef<