diff --git a/apps/docs/public/humans.txt b/apps/docs/public/humans.txt
index 964214306f51e..78de85cb5b91b 100644
--- a/apps/docs/public/humans.txt
+++ b/apps/docs/public/humans.txt
@@ -273,6 +273,7 @@ Sana Cordeaux
Sasi Kanumuri
Sara Read
Satya Rohith Gannamanedi
+Sean Geoghegan
Sean Oliver
Sean Romberg
Sean Thompson
diff --git a/apps/studio/TANSTACK_MIGRATION.md b/apps/studio/TANSTACK_MIGRATION.md
index e41b2b124dde4..0fdd44ca2006c 100644
--- a/apps/studio/TANSTACK_MIGRATION.md
+++ b/apps/studio/TANSTACK_MIGRATION.md
@@ -122,6 +122,7 @@ These are the layout-only TanStack files. Most hold a single product layout comp
- [x] A `routes/_app/org/$slug/index.tsx` ← `pages/org/[slug]/index.tsx`
- [x] A `routes/_app/org/$slug/apps.tsx` ← `pages/org/[slug]/apps.tsx`
- [x] A `routes/_app/org/$slug/audit.tsx` ← `pages/org/[slug]/audit.tsx`
+- [x] A `routes/_app/org/$slug/audit-log-drains.tsx` ← `pages/org/[slug]/audit-log-drains.tsx` (wraps in OrganizationSettingsLayout inline)
- [x] A `routes/_app/org/$slug/billing.tsx` ← `pages/org/[slug]/billing.tsx`
- [x] A `routes/_app/org/$slug/documents.tsx` ← `pages/org/[slug]/documents.tsx`
- [x] A `routes/_app/org/$slug/general.tsx` ← `pages/org/[slug]/general.tsx`
@@ -170,6 +171,7 @@ These are the layout-only TanStack files. Most hold a single product layout comp
- [x] A `routes/project/$ref/database/functions.tsx` ← `pages/project/[ref]/database/functions.tsx`
- [x] A `routes/project/$ref/database/indexes.tsx` ← `pages/project/[ref]/database/indexes.tsx`
- [x] A `routes/project/$ref/database/migrations.tsx` ← `pages/project/[ref]/database/migrations.tsx`
+- [x] A `routes/project/$ref/database/policies.tsx` ← `pages/project/[ref]/database/policies.tsx`
- [x] A `routes/project/$ref/database/roles.tsx` ← `pages/project/[ref]/database/roles.tsx`
- [x] A `routes/project/$ref/database/settings.tsx` ← `pages/project/[ref]/database/settings.tsx`
- [x] A `routes/project/$ref/database/types.tsx` ← `pages/project/[ref]/database/types.tsx`
@@ -192,7 +194,6 @@ These are the layout-only TanStack files. Most hold a single product layout comp
- [x] A `routes/project/$ref/auth/overview.tsx` ← `pages/project/[ref]/auth/overview.tsx`
- [x] A `routes/project/$ref/auth/users.tsx` ← `pages/project/[ref]/auth/users.tsx`
-- [x] A `routes/project/$ref/auth/policies.tsx` ← `pages/project/[ref]/auth/policies.tsx`
- [x] A `routes/project/$ref/auth/providers.tsx` ← `pages/project/[ref]/auth/providers.tsx` (sets `skipAuthLayout: true`, wraps in `AuthProvidersLayout` directly)
- [x] A `routes/project/$ref/auth/mfa.tsx` ← `pages/project/[ref]/auth/mfa.tsx`
- [x] A `routes/project/$ref/auth/hooks.tsx` ← `pages/project/[ref]/auth/hooks.tsx`
@@ -258,6 +259,7 @@ These are the layout-only TanStack files. Most hold a single product layout comp
- [x] A `routes/project/$ref/logs/dedicated-pooler-logs.tsx` ← `pages/project/[ref]/logs/dedicated-pooler-logs.tsx`
- [x] A `routes/project/$ref/logs/edge-functions-logs.tsx` ← `pages/project/[ref]/logs/edge-functions-logs.tsx`
- [x] A `routes/project/$ref/logs/edge-logs.tsx` ← `pages/project/[ref]/logs/edge-logs.tsx`
+- [x] A `routes/project/$ref/logs/multigres-logs.tsx` ← `pages/project/[ref]/logs/multigres-logs.tsx`
- [x] A `routes/project/$ref/logs/pg-upgrade-logs.tsx` ← `pages/project/[ref]/logs/pg-upgrade-logs.tsx`
- [x] A `routes/project/$ref/logs/pgcron-logs.tsx` ← `pages/project/[ref]/logs/pgcron-logs.tsx`
- [x] A `routes/project/$ref/logs/pooler-logs.tsx` ← `pages/project/[ref]/logs/pooler-logs.tsx`
@@ -278,6 +280,7 @@ These are the layout-only TanStack files. Most hold a single product layout comp
- [x] A `routes/project/$ref/observability/auth.tsx` ← `pages/project/[ref]/observability/auth.tsx`
- [x] A `routes/project/$ref/observability/database.tsx` ← `pages/project/[ref]/observability/database.tsx`
- [x] A `routes/project/$ref/observability/api-overview.tsx` ← `pages/project/[ref]/observability/api-overview.tsx`
+- [x] A `routes/project/$ref/observability/connections.tsx` ← `pages/project/[ref]/observability/connections.tsx`
- [x] A `routes/project/$ref/observability/edge-functions.tsx` ← `pages/project/[ref]/observability/edge-functions.tsx`
- [x] A `routes/project/$ref/observability/postgrest.tsx` ← `pages/project/[ref]/observability/postgrest.tsx`
- [x] A `routes/project/$ref/observability/query-insights.tsx` ← `pages/project/[ref]/observability/query-insights.tsx`
@@ -298,6 +301,7 @@ These are the layout-only TanStack files. Most hold a single product layout comp
- [x] A `routes/project/$ref/settings/addons.tsx` ← `pages/project/[ref]/settings/addons.tsx`
- [x] A `routes/project/$ref/settings/api.tsx` ← `pages/project/[ref]/settings/api.tsx` (sets `skipSettingsLayout: true` — page is a useEffect redirect)
- [x] A `routes/project/$ref/settings/dashboard.tsx` ← `pages/project/[ref]/settings/dashboard.tsx`
+- [x] A `routes/project/$ref/settings/code-configuration.tsx` ← `pages/project/[ref]/settings/code-configuration.tsx`
- [x] A `routes/project/$ref/settings/infrastructure/index.tsx` ← `pages/project/[ref]/settings/infrastructure.tsx`
- [x] A `routes/project/$ref/settings/infrastructure/replica/$replicaId.tsx` ← `pages/project/[ref]/settings/infrastructure/replica/[replicaId].tsx`
- [x] A `routes/project/$ref/settings/integrations.tsx` ← `pages/project/[ref]/settings/integrations.tsx`
@@ -322,7 +326,7 @@ These are the layout-only TanStack files. Most hold a single product layout comp
- [x] A `routes/project/$ref/sql/index.tsx` ← `pages/project/[ref]/sql/index.tsx`
- [x] A `routes/project/$ref/sql/$id.tsx` ← `pages/project/[ref]/sql/[id].tsx`
- [x] A `routes/project/$ref/sql/templates.tsx` ← `pages/project/[ref]/sql/templates.tsx`
-- [x] A `routes/project/$ref/sql/quickstarts.tsx` ← `pages/project/[ref]/sql/quickstarts.tsx`
+- [x] A `routes/project/$ref/sql/examples.tsx` ← `pages/project/[ref]/sql/examples.tsx`
### Project shell — `/editor/*`
@@ -335,6 +339,7 @@ These are the layout-only TanStack files. Most hold a single product layout comp
- [x] A `routes/project/$ref/explorer/index.tsx` ← `pages/project/[ref]/explorer/index.tsx`
- [x] A `routes/project/$ref/explorer/notebook/$id.tsx` ← `pages/project/[ref]/explorer/notebook/[id].tsx`
- [x] A `routes/project/$ref/explorer/chat/$id.tsx` ← `pages/project/[ref]/explorer/chat/[id].tsx`
+- [x] A `routes/project/$ref/explorer/query/$id.tsx` ← `pages/project/[ref]/explorer/query/[id].tsx`
### Auth shell — `/sign-in`, `/sign-up`, etc.
@@ -356,6 +361,7 @@ These are the layout-only TanStack files. Most hold a single product layout comp
- [x] A `routes/redeem.tsx` ← `pages/redeem.tsx` (RedeemCreditsLayout)
- [x] A `routes/logout.tsx` ← `pages/logout.tsx`
- [x] A `routes/maintenance.tsx` ← `pages/maintenance.tsx`
+- [x] A `routes/verify-email.tsx` ← `pages/verify-email.tsx`
### Error pages (handled at root)
diff --git a/apps/studio/components/interfaces/Workers/WorkerDetail/WorkerDetail.tsx b/apps/studio/components/interfaces/Workers/WorkerDetail/WorkerDetail.tsx
index 88e8de4d71b45..cca4bac374e18 100644
--- a/apps/studio/components/interfaces/Workers/WorkerDetail/WorkerDetail.tsx
+++ b/apps/studio/components/interfaces/Workers/WorkerDetail/WorkerDetail.tsx
@@ -118,7 +118,7 @@ export const WorkerDetail = () => {
{worker.imageVersion !== undefined && (
- Image {worker.imageVersion}
+ Version {worker.imageVersion}
)}
diff --git a/apps/studio/components/interfaces/Workers/WorkerDetail/WorkerOverviewTab.tsx b/apps/studio/components/interfaces/Workers/WorkerDetail/WorkerOverviewTab.tsx
index 39ee28eb50900..dc38d030041a5 100644
--- a/apps/studio/components/interfaces/Workers/WorkerDetail/WorkerOverviewTab.tsx
+++ b/apps/studio/components/interfaces/Workers/WorkerDetail/WorkerOverviewTab.tsx
@@ -18,7 +18,7 @@ import { LISTENING_PORT, WORKERS_REGION_LABEL } from '../Workers.constants'
import type { Worker } from '../Workers.types'
import { formatSize, getRuntimeMeta } from '../Workers.utils'
import { buildWorkerCliCommands } from '../workerSnippets'
-import { WorkerSnippetTabs } from '../WorkerSnippetTabs'
+import { WORKER_CALL_TABS, WorkerSnippetTabs } from '../WorkerSnippetTabs'
import { CLI_NAME } from '@/lib/constants/workers'
interface WorkerOverviewTabProps {
@@ -156,7 +156,7 @@ export const WorkerOverviewTab = ({ worker }: WorkerOverviewTabProps) => {
{worker.imageVersion !== undefined && (
-
+
{worker.imageVersion}
@@ -215,9 +215,7 @@ export const WorkerOverviewTab = ({ worker }: WorkerOverviewTabProps) => {
How to call
-
- Call the worker over its gateway URL. Pass your project API key as a bearer token.
-
+ Call the worker over its gateway URL.
@@ -229,7 +227,7 @@ export const WorkerOverviewTab = ({ worker }: WorkerOverviewTabProps) => {
access: worker.access,
instances: worker.declaredInstances,
}}
- tabs={['cli', 'js', 'python']}
+ tabs={WORKER_CALL_TABS}
/>
diff --git a/apps/studio/components/interfaces/Workers/WorkerSnippetTabs.tsx b/apps/studio/components/interfaces/Workers/WorkerSnippetTabs.tsx
index 08f181338de2a..d97a36255e45d 100644
--- a/apps/studio/components/interfaces/Workers/WorkerSnippetTabs.tsx
+++ b/apps/studio/components/interfaces/Workers/WorkerSnippetTabs.tsx
@@ -7,12 +7,19 @@ import { buildWorkerSnippets, type WorkerSnippetInput } from './workerSnippets'
import CopyButton from '@/components/ui/CopyButton'
import { useProjectSettingsV2Query } from '@/data/config/project-settings-v2-query'
-export type WorkerSnippetTab = 'ai' | 'config' | 'cli' | 'js' | 'python'
+export type WorkerSnippetTab = 'ai' | 'config' | 'cli' | 'curl' | 'js' | 'python'
+
+export const WORKER_CALL_TABS = [
+ 'curl',
+ 'js',
+ 'python',
+] as const satisfies readonly WorkerSnippetTab[]
const TAB_LABEL: Record = {
ai: 'AI Prompt',
config: 'config.toml',
cli: 'CLI',
+ curl: 'cURL',
js: 'JavaScript',
python: 'Python',
}
@@ -21,13 +28,14 @@ const TAB_ICON: Record = {
ai: Sparkles,
config: FileCode,
cli: Terminal,
+ curl: Terminal,
js: FileCode,
python: FileCode,
}
interface WorkerSnippetTabsProps {
input: Omit
- tabs?: [WorkerSnippetTab, ...WorkerSnippetTab[]]
+ tabs?: readonly [WorkerSnippetTab, ...WorkerSnippetTab[]]
className?: string
}
@@ -45,6 +53,7 @@ export const WorkerSnippetTabs = ({ input, tabs = ['cli'], className }: WorkerSn
ai: snippets.aiPrompt,
config: snippets.configToml,
cli: snippets.cli,
+ curl: snippets.curl,
js: snippets.javascript,
python: snippets.python,
}
diff --git a/apps/studio/components/interfaces/Workers/workerSnippets.test.ts b/apps/studio/components/interfaces/Workers/workerSnippets.test.ts
index 7070d5f014db6..77f434a143731 100644
--- a/apps/studio/components/interfaces/Workers/workerSnippets.test.ts
+++ b/apps/studio/components/interfaces/Workers/workerSnippets.test.ts
@@ -11,16 +11,21 @@ const input = (overrides: Partial[0]> = {
describe('buildWorkerSnippets', () => {
it('points the invoke examples at the worker URL', () => {
- const { javascript, python } = buildWorkerSnippets(input({ name: 'embed' }))
+ const { curl, javascript, python } = buildWorkerSnippets(input({ name: 'embed' }))
const url = 'https://abcdefgh.supabase.co/workers/v1/embed'
+ expect(curl).toContain(`'${url}'`)
expect(javascript).toContain(`'${url}'`)
expect(python).toContain(`"${url}"`)
})
it('leaves a placeholder invoke URL until the project settings resolve', () => {
- const { javascript } = buildWorkerSnippets(input({ endpoint: undefined }))
- expect(javascript).toContain('[YOUR WORKER URL]')
+ const { curl } = buildWorkerSnippets(input({ endpoint: undefined }))
+ expect(curl).toContain('[YOUR WORKER URL]')
+ })
+
+ it('does not require authorization for the CLI invoke example', () => {
+ expect(buildWorkerSnippets(input()).curl).not.toContain('Authorization')
})
it('asks for the anon key to invoke a public worker and the service role key for a private one', () => {
diff --git a/apps/studio/components/interfaces/Workers/workerSnippets.ts b/apps/studio/components/interfaces/Workers/workerSnippets.ts
index de89c173050a4..7cc337d332c39 100644
--- a/apps/studio/components/interfaces/Workers/workerSnippets.ts
+++ b/apps/studio/components/interfaces/Workers/workerSnippets.ts
@@ -17,6 +17,7 @@ export interface WorkerSnippets {
aiPrompt: string
configToml: string
cli: string
+ curl: string
javascript: string
python: string
}
@@ -46,6 +47,12 @@ export function buildWorkerSnippets(input: WorkerSnippetInput): WorkerSnippets {
...(input.access === 'private' ? [`# note: the CLI can only deploy public workers today`] : []),
].join('\n')
+ const curl = [
+ `curl --request POST '${url}' \\`,
+ ` --header 'Content-Type: application/json' \\`,
+ ` --data '{"name":"world"}'`,
+ ].join('\n')
+
const configBlock = [
`[${CLI_NAME}.${name}]`,
`runtime = "${runtime}"`,
@@ -102,7 +109,7 @@ export function buildWorkerSnippets(input: WorkerSnippetInput): WorkerSnippets {
`print(res.json())`,
].join('\n')
- return { aiPrompt, configToml, cli, javascript, python }
+ return { aiPrompt, configToml, cli, curl, javascript, python }
}
export interface WorkerCliCommand {
diff --git a/apps/studio/redirects.shared.test.ts b/apps/studio/redirects.shared.test.ts
index 5667989470f00..fa08c3a640af0 100644
--- a/apps/studio/redirects.shared.test.ts
+++ b/apps/studio/redirects.shared.test.ts
@@ -157,3 +157,62 @@ describe('matchRedirect query/hash preservation', () => {
).toBeNull()
})
})
+
+describe('matchRedirect maintenance mode', () => {
+ it('sends every other path to /maintenance when enabled', () => {
+ expect(
+ matchRedirect({
+ pathname: '/project/abc/editor',
+ search: {},
+ isPlatform: true,
+ maintenanceMode: true,
+ })
+ ).toEqual({ destination: '/maintenance', permanent: false })
+ })
+
+ it('carries query and hash onto /maintenance', () => {
+ expect(
+ matchRedirect({
+ pathname: '/project/abc/editor',
+ search: { a: '1' },
+ isPlatform: true,
+ maintenanceMode: true,
+ hash: 'section',
+ })
+ ).toEqual({ destination: '/maintenance?a=1#section', permanent: false })
+ })
+
+ it('leaves /maintenance and /img reachable when enabled', () => {
+ expect(
+ matchRedirect({
+ pathname: '/maintenance',
+ search: {},
+ isPlatform: true,
+ maintenanceMode: true,
+ })
+ ).toBeNull()
+ expect(
+ matchRedirect({
+ pathname: '/img/supabase-logo.svg',
+ search: {},
+ isPlatform: true,
+ maintenanceMode: true,
+ })
+ ).toBeNull()
+ })
+
+ it('bounces /maintenance back to / when disabled', () => {
+ expect(matchRedirect({ pathname: '/maintenance', search: {}, isPlatform: true })).toEqual({
+ destination: '/',
+ permanent: false,
+ })
+ expect(
+ matchRedirect({
+ pathname: '/maintenance',
+ search: {},
+ isPlatform: true,
+ maintenanceMode: false,
+ })
+ ).toEqual({ destination: '/', permanent: false })
+ })
+})
diff --git a/apps/studio/routes/__root.tsx b/apps/studio/routes/__root.tsx
index 4d1c42e7c4556..9c2e251ef9e81 100644
--- a/apps/studio/routes/__root.tsx
+++ b/apps/studio/routes/__root.tsx
@@ -139,6 +139,12 @@ const IS_NON_PROD_ENV =
process.env.NEXT_PUBLIC_ENVIRONMENT === 'local' ||
process.env.NEXT_PUBLIC_ENVIRONMENT === 'staging'
+// Mirrors the `MAINTENANCE_MODE` reads in `next.config.ts` and `vercel.ts`.
+// The var is unprefixed, so vite.config.ts inlines it explicitly (see the
+// define there) rather than it arriving via the NEXT_PUBLIC_ sweep — that
+// keeps the toggle a single build-time env var across all three runtimes.
+const IS_MAINTENANCE_MODE = process.env.MAINTENANCE_MODE === 'true'
+
// Keep dev-only components out of the production bundle.
const IS_DEV_TOOLBAR_ENABLED = IS_NON_PROD_ENV
@@ -329,6 +335,7 @@ export const Route = createRootRouteWithContext()({
pathname: location.pathname,
search: location.search as Record,
isPlatform: IS_PLATFORM,
+ maintenanceMode: IS_MAINTENANCE_MODE,
hash: location.hash,
})
if (!match) return
diff --git a/apps/studio/vite.config.ts b/apps/studio/vite.config.ts
index a238efe54a371..b7bd19442e17f 100644
--- a/apps/studio/vite.config.ts
+++ b/apps/studio/vite.config.ts
@@ -602,6 +602,19 @@ export default defineConfig(({ command, mode }) => {
}
}
+ // `MAINTENANCE_MODE` gates the "redirect everything to /maintenance" rule.
+ // It's deliberately unprefixed, and the other two consumers both read it at
+ // BUILD time: `next.config.ts` reads it in `redirects()`, which Next bakes
+ // into `routes-manifest.json` during `next build`, and `vercel.ts` reads it
+ // while emitting `vercel.json`. So flipping maintenance has always meant a
+ // rebuild/redeploy, never just a server restart. Inline it here on the same
+ // terms so the isomorphic `beforeLoad` in `routes/__root.tsx` — which
+ // mirrors those rules for the TanStack runtime — can read it on the client
+ // too, without self-hosters having to set a second, NEXT_PUBLIC_-prefixed
+ // var. Falls back to `''` (not left undefined) so the browser bundle never
+ // ends up with a bare `process.env` reference.
+ publicEnvDefines['process.env.MAINTENANCE_MODE'] = JSON.stringify(env.MAINTENANCE_MODE ?? '')
+
// Sentry init (lib/sentry-client-options.ts, reached via router.tsx) reads
// these at runtime in the browser. When a var is unset it gets no define
// entry above, which would leave a literal `process.env.*` in the built