mirror of
https://github.com/supabase/supabase.git
synced 2026-10-10 11:55:05 +03:00
Deployment update checks now receive a timestamp from GitHub’s documented commit API instead of silently falling back to `unknown` when its website response changes. Valid deployment metadata is cached for ten minutes; development and failed lookups are uncached so they can recover. **Changed:** - Validate and normalize the committer date in the shared Next/TanStack handler, preserving the existing response shape and `unknown` fallback. - Exclude only the exact deployment-metadata endpoint from TanStack’s private API cache default; authenticated APIs and server functions retain it. - Keep the client update query unpinned and its existing toast threshold unchanged. **Added:** - Shared-handler tests through both Next and the TanStack adapter for dates, malformed payloads, upstream failures, and recovery. - Tests using the installed Vercel route compiler for root and `/dashboard` cache rules, security headers, and empty Next configuration. ## To test - On the TanStack preview, request `/dashboard/api/get-deployment-commit`; compare its SHA and UTC timestamp with the deployed commit’s GitHub committer date. - Repeat the request to check CDN caching. Vercel consumes `s-maxage`, so use cache-hit/age evidence as well as client-visible headers. - Confirm another API route and a server-function path retain `private, no-store`. - Open an existing project, reload it while clean, and inspect the untouched support form for metadata-related errors. Validation: 60 focused tests, Studio typecheck, scoped ESLint, formatting, knip, and both framework production builds passed. Live unauthenticated GitHub lookup with the configured API version returned the expected committer date. Full source lint ratchet also passed, excluding only generated build directories. Local TanStack browser checks passed for clean project/reload, general settings, untouched support form, and naturally emitted development metadata formatting; no errors or unexpected update toast appeared. The positive two-deployment toast and submitted support-version formatting were not exercised. Deployed native Next previews returned the exact commit SHA and expected UTC timestamp; repeated metadata requests produced CDN HIT responses (ages 26 and 100 seconds). Build logs establish that the staging preview also ran Next, so it does not validate TanStack edge header behavior. A separate preview-only redeployment with a deployment-scoped TanStack override was confirmed to run Vite. It returned the same correct SHA/timestamp, a repeat CDN cache HIT, and private, no-store on the neighboring UTC API. No project settings or production aliases were changed. Installed Vercel compiler tests cover the remaining root/base-path and server-function rules. No new environment variable, token, or dependency is required. Co-authored-by: Alaister Young <10985857+alaister@users.noreply.github.com>
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
|