diff --git a/apps/studio/redirects.shared.test.ts b/apps/studio/redirects.shared.test.ts index 5667989470f..fa08c3a640a 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 4d1c42e7c45..9c2e251ef9e 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 a238efe54a3..b7bd19442e1 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