mirror of
https://github.com/supabase/supabase.git
synced 2026-10-05 01:15:03 +03:00
fix(studio): honor MAINTENANCE_MODE in the TanStack runtime (#48616)
## What kind of change does this PR introduce? Bug fix. ## What is the current behavior? Fixes #48559 (diagnosed by @ayaangazali) The TanStack Start runtime never applies maintenance mode. `matchRedirect` in `apps/studio/redirects.shared.ts` takes a `maintenanceMode` flag, and both other consumers wire it from the environment: - `apps/studio/next.config.ts` — `process.env.MAINTENANCE_MODE === 'true'` - `apps/studio/vercel.ts` — same The TanStack call site in `apps/studio/routes/__root.tsx` passed only `pathname`, `search`, `isPlatform` and `hash`, so `maintenanceMode` fell back to its `= false` default. With `MAINTENANCE_MODE=true` on a TanStack deploy that produced two wrong behaviors: 1. No path redirected to `/maintenance` — the app served normally during maintenance. 2. Because the flag read false, the "not in maintenance" branch still applied and sent `/maintenance` → `/`, making `routes/maintenance.tsx` unreachable. Mainly affects self-hosted / Node-server TanStack deploys; the platform deploy is covered by the Vercel edge layer, which does wire the flag. ## What is the new behavior? The TanStack runtime honors `MAINTENANCE_MODE` the same way the Next runtime and the edge config do. **Design note.** The issue asked whether this needs a new `NEXT_PUBLIC_` variable or server-side plumbing, since both would change deployment configuration for self-hosters. Neither is needed. `MAINTENANCE_MODE` is already a *build-time* variable in both existing consumers — Next bakes `redirects()` into `routes-manifest.json` during `next build`, and `vercel.ts` reads it while emitting `vercel.json`. Toggling maintenance has always required a rebuild, never just a server restart. And `vite.config.ts` isn't bound by Next's "only `NEXT_PUBLIC_`" rule: it controls `define` directly, and already re-exposes unprefixed `VERCEL_*` vars the same way. So the existing unprefixed variable is inlined at build time, giving exact parity with **no new env var and no config change for self-hosters**. Three changes: 1. `vite.config.ts` — inline `process.env.MAINTENANCE_MODE` into the bundle. Falls back to `''` rather than being left undefined, so the browser bundle never ends up with a bare `process.env` reference (the failure mode the file already guards against for the Sentry vars). 2. `routes/__root.tsx` — read it into `IS_MAINTENANCE_MODE` and pass it to `matchRedirect`. 3. `redirects.shared.test.ts` — 4 tests for the maintenance branches of `matchRedirect`, which had no coverage at all. `turbo.jsonc` already lists `MAINTENANCE_MODE` under the build task's `env`, so cache invalidation is correct for the Vite build too — no change needed. No README or docs change either, since the env contract is unchanged. ## Additional context Verified end-to-end, not just by unit test. **Browser repro** — built SPA served via `scripts/serve.js`, driven in headless Chromium: | `MAINTENANCE_MODE=true` | lands on | | | --- | --- | --- | | `/project/default` | `/maintenance` | fixes behavior 1 | | `/` | `/maintenance` | | | `/maintenance` | `/maintenance` | fixes behavior 2 | The maintenance page renders real content ("Under Maintenance — We are currently improving our services…"), so the route is genuinely reachable. | control, var unset | lands on | | | --- | --- | --- | | `/project/default` | `/project/default` | normal routing intact | | `/` | `/project/default` | root redirect intact | | `/maintenance` | `/project/default` | correctly bounces away | **Bundle inspection** — the flag compiles to a literal `true` with the variable set and `false` without it, confirming the define reaches the client. **Shell prerender** — checked explicitly, since the maintenance-on rule is a catch-all. Builds with `MAINTENANCE_MODE=true` prerender the SPA shell and pass the post-build smoke test; the prerenderer crawls `/` and the root `beforeLoad` redirect does not fire during shell generation, so no guard is required. **Checks** — 20 unit tests pass, typecheck 8/8, ESLint ratchet passes, Prettier clean. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit - **New Features** - Added maintenance-mode routing for unavailable pages. - Preserves query parameters and URL fragments during redirects. - Allows access to maintenance and image paths while maintenance mode is active. - Automatically returns visitors to the home page when maintenance mode is disabled. - Maintenance behavior is controlled by the deployment configuration. - **Tests** - Added coverage for maintenance-mode redirects, URL preservation, and exceptions. <!-- end of auto-generated comment: release notes by coderabbit.ai --> Co-authored-by: Alaister Young <10985857+alaister@users.noreply.github.com>
This commit is contained in:
1 parent
4ab1a6cbd2
commit
cd34776be1
3 files changed
+79
No files matched your search
@@ -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 })
|
||||
})
|
||||
})
|
||||
@@ -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<RouterContext>()({
|
||||
pathname: location.pathname,
|
||||
search: location.search as Record<string, string | string[] | undefined>,
|
||||
isPlatform: IS_PLATFORM,
|
||||
maintenanceMode: IS_MAINTENANCE_MODE,
|
||||
hash: location.hash,
|
||||
})
|
||||
if (!match) return
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in new issue
Block a user