mirror of
https://github.com/supabase/supabase.git
synced 2026-10-06 18:05:11 +03:00
125 lines
5.8 KiB
TypeScript
125 lines
5.8 KiB
TypeScript
import { routes, type Redirect, type VercelConfig } from '@vercel/config/v1'
|
|
|
|
import {
|
|
getMaintenanceRedirects,
|
|
PLATFORM_REDIRECTS,
|
|
SELF_HOSTED_REDIRECTS,
|
|
SHARED_REDIRECTS,
|
|
type StudioRedirect,
|
|
} from './redirects.shared'
|
|
import { getSecurityHeaders } from './security-headers'
|
|
|
|
// STUDIO_FRAMEWORK gates the TanStack Start deploy. When the env var is
|
|
// unset (the default — used by the Next.js prod deploy) this file returns
|
|
// an empty `VercelConfig` so Vercel honours the dashboard-configured
|
|
// Next.js preset untouched. Vercel reads `vercel.ts` regardless of the
|
|
// framework preset (per vercel.com/docs/project-configuration —
|
|
// `vercel.ts`'s `framework` field overrides the dashboard preset), so a
|
|
// no-op early return is the only way to keep `framework: null` and the
|
|
// headers below from clobbering the Next build. Set
|
|
// `STUDIO_FRAMEWORK=tanstack` on the TanStack Vercel project to opt in.
|
|
const isTanstack = process.env.STUDIO_FRAMEWORK === 'tanstack'
|
|
|
|
// Routing lives in Nitro's Build Output config (`.vercel/output/config.json`,
|
|
// shaped by scripts/vercel-spa-routes.ts). Vercel applies this file's
|
|
// `redirects`/`headers` ahead of it, which is also why no `rewrites` belong
|
|
// here: a catch-all rewrite at this layer swallows the API routes.
|
|
const basePath = process.env.NEXT_PUBLIC_BASE_PATH ?? ''
|
|
|
|
// Headers for a given prefix ('' for root, or a base path like '/dashboard').
|
|
// Run once per prefix and concatenated so no rule is hand-duplicated.
|
|
function headersFor(prefix: string) {
|
|
return [
|
|
// Security headers for every response. The Next build sets these via
|
|
// next.config.ts `headers()`; the TanStack build serves a static shell
|
|
// from the CDN, so they live at the edge. Matches next.config's `/(.*?)`
|
|
// block (CSP, X-Frame-Options, HSTS, etc.).
|
|
{ source: `${prefix}/(.*)`, headers: getSecurityHeaders() },
|
|
// Deployment metadata sets its own cache policy based on lookup success.
|
|
// All other API paths retain the private default, including nested paths.
|
|
routes.cacheControl(`${prefix}/api/((?!get-deployment-commit/?$).*)`, {
|
|
private: true,
|
|
noStore: true,
|
|
}),
|
|
routes.cacheControl(`${prefix}/_serverFn/(.*)`, { private: true, noStore: true }),
|
|
// Hashed chunks are covered by Nitro (`/_vercel/immutable/*`, immutable).
|
|
// Static images and favicons aren't content-hashed, so they can't be
|
|
// `immutable`, but they change rarely — mirror next.config's
|
|
// `cache-control` for these paths (img: max-age=2592000 = 30 days,
|
|
// favicon: max-age=86400 = 1 day).
|
|
routes.cacheControl(`${prefix}/img/(.*)`, { public: true, maxAge: '30days' }),
|
|
routes.cacheControl(`${prefix}/favicon/(.*)`, { public: true, maxAge: '1day' }),
|
|
// Deno declarations are public TypeScript assets. Keep this override
|
|
// scoped to /deno so dynamic response types remain handler-owned.
|
|
routes.header(`${prefix}/deno/(.*)\\.ts`, [{ key: 'content-type', value: 'text/typescript' }]),
|
|
]
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Redirects — entries live in `redirects.shared.ts`, consumed by both
|
|
// `next.config.ts` and this file. Next auto-prepends `basePath` to its
|
|
// redirects; Vercel doesn't, so we apply it here.
|
|
// ---------------------------------------------------------------------------
|
|
|
|
function applyBasePath(r: StudioRedirect): Redirect {
|
|
if (!basePath) return r
|
|
const prefix = (path: string) =>
|
|
path.startsWith('/') ? (path === '/' ? basePath : `${basePath}${path}`) : path
|
|
return { ...r, source: prefix(r.source), destination: prefix(r.destination) }
|
|
}
|
|
|
|
function buildRedirects(): Redirect[] {
|
|
const isPlatform = process.env.NEXT_PUBLIC_IS_PLATFORM === 'true'
|
|
const maintenance = process.env.MAINTENANCE_MODE === 'true'
|
|
const conditional = isPlatform ? PLATFORM_REDIRECTS : SELF_HOSTED_REDIRECTS
|
|
|
|
// Bare-domain bounce to the basePath when one is configured. Source
|
|
// stays literally `/` (NOT prefixed) so the entry-point redirect fires.
|
|
const basePathBounce: Redirect[] = basePath
|
|
? [{ source: '/', destination: basePath, permanent: false }]
|
|
: []
|
|
|
|
return [
|
|
...conditional.map(applyBasePath),
|
|
...SHARED_REDIRECTS.map(applyBasePath),
|
|
...basePathBounce,
|
|
...getMaintenanceRedirects(maintenance).map(applyBasePath),
|
|
]
|
|
}
|
|
|
|
function buildTanstackConfig(): VercelConfig {
|
|
// Vercel's Flags Explorer probes `/.well-known/vercel/flags` and expects
|
|
// JSON. next.config.ts proxies it to supabase.com's endpoint and forces
|
|
// `content-type: application/json`; mirror that here. It lives at the domain
|
|
// root (once, NOT per prefix) because next.config's rewrite sets
|
|
// `basePath: false` — well-known URLs are at the root regardless of the
|
|
// app's basePath.
|
|
const wellKnownFlags = '/.well-known/vercel/flags'
|
|
|
|
return {
|
|
// Nitro's Build Output directory is what gets deployed; no framework
|
|
// preset should route on top of it.
|
|
framework: null,
|
|
redirects: buildRedirects(),
|
|
rewrites: [routes.rewrite(wellKnownFlags, `https://supabase.com${wellKnownFlags}`)],
|
|
headers: [
|
|
routes.header(wellKnownFlags, [{ key: 'content-type', value: 'application/json' }]),
|
|
// When a base path is configured, emit both the prefixed and root rule
|
|
// sets (prefixed first so it wins for explicit /dashboard/* hits, root
|
|
// as a fallback for bare-domain traffic).
|
|
...(basePath ? [basePath, ''] : ['']).flatMap(headersFor),
|
|
],
|
|
}
|
|
}
|
|
|
|
// Empty config = no overrides; Vercel falls back to the dashboard preset.
|
|
const passthrough: VercelConfig = {}
|
|
|
|
export const config: VercelConfig = isTanstack ? buildTanstackConfig() : passthrough
|
|
|
|
// Belt-and-braces: local @vercel/config CLI reads module.default, but the
|
|
// docs claim Vercel's platform looks for a named `config` export. Export
|
|
// both so whichever path runs wins.
|
|
// eslint-disable-next-line no-restricted-exports
|
|
export default config
|