diff --git a/apps/studio/.env b/apps/studio/.env index 6a7f9dd8585..89fbeb4f7c9 100644 --- a/apps/studio/.env +++ b/apps/studio/.env @@ -1,3 +1,5 @@ +# Default for the TanStack server (`pnpm start`); shell env and later env files override it. +PORT=8082 STUDIO_PG_META_URL=http://localhost:8000/pg POSTGRES_PASSWORD=your-super-secret-and-long-postgres-password diff --git a/apps/studio/Dockerfile b/apps/studio/Dockerfile index 9922ee29469..02f7f1ac96c 100644 --- a/apps/studio/Dockerfile +++ b/apps/studio/Dockerfile @@ -71,42 +71,20 @@ RUN mkdir -p /srv && \ cp -a apps/studio/.next/static /srv/apps/studio/.next/static && \ cp -a apps/studio/public /srv/apps/studio/public -# Compile TanStack Start (Vite) +# Compile TanStack Start (Vite + Nitro) FROM dev AS build-tanstack -# build:tanstack = vite build --mode production, then a smoke test that -# boots the server bundle so module-scope crashes fail the image build. RUN NODE_OPTIONS=--max-old-space-size=4096 pnpm --filter studio run build:tanstack -# Assemble the runtime tree at /srv. Unlike Next's standalone output, the -# Vite SSR bundle externalizes studio's dependencies and resolves them from -# node_modules at request time, so the tree is a prod-only `pnpm deploy` of -# studio (node_modules + manifest) plus the built dist/ and the runtime -# scripts. `scripts/serve.js` is the HTTP server (the same entry -# start:tanstack uses); the server.js shim gives the production stage a -# single CMD that works for both frameworks. `.env` is kept because -# serve.js loads it as the base of the runtime env cascade (container env -# vars always win over file values). -# -# --ignore-scripts: pnpm 11 hard-errors (ERR_PNPM_IGNORED_BUILDS) on -# dependency build scripts without an allowBuilds entry, and deploy turns -# the workspace packages into file: deps whose `only-allow pnpm` preinstall -# guards trip it. No lifecycle script is needed here anyway: the tree is -# fully prebuilt, and nothing in studio's prod graph is approved to build -# (allowBuilds only permits node-pty and supabase, both dev-only). -RUN pnpm --filter studio deploy --prod --legacy --ignore-scripts /srv/apps/studio && \ - cd /srv/apps/studio && \ - find . -mindepth 1 -maxdepth 1 \ - ! -name node_modules ! -name package.json ! -name scripts \ - ! -name instrument.server.mjs ! -name .env \ - -exec rm -rf {} + && \ - cp -a /app/apps/studio/dist ./dist && \ - printf "import('./scripts/serve.js')\n" > server.js - -# Boot the pruned tree exactly the way the container will run it, so a -# dependency that's runtime-imported but missing from `dependencies` -# (present only in devDependencies) fails the build here instead of -# 500ing the deployed container. -RUN cd /srv/apps/studio && node scripts/smoke-server.mjs +# Assemble the runtime tree at /srv. Nitro's node-server output is +# self-contained (`.output/server` bundles the app with its traced +# dependencies, `.output/public` holds the client assets), so no node_modules +# install is needed. The server.js shim loads `.env` (container env vars win) +# and gives the production stage one CMD for both frameworks; package.json is +# copied so the shim runs as ESM. +RUN mkdir -p /srv/apps/studio && \ + cp -a apps/studio/.output /srv/apps/studio/.output && \ + cp apps/studio/package.json apps/studio/.env /srv/apps/studio/ && \ + printf "process.loadEnvFile(new URL('.env', import.meta.url))\nawait import('./.output/server/index.mjs')\n" > /srv/apps/studio/server.js # Alias whichever framework build was selected so the production stage can # COPY from a single stage name. BuildKit only builds the selected branch. @@ -115,8 +93,8 @@ FROM build-${STUDIO_FRAMEWORK} AS build # Copy only compiled code and dependencies FROM base AS production COPY --from=build /srv ./ -# serve.js (TanStack) defaults to port 8082; pin both servers to the port -# the healthcheck and compose files expect. Next's server.js reads PORT too. +# Both servers read PORT (the TanStack `start` script defaults it to 8082); +# pin it to the port the healthcheck and compose files expect. ENV PORT=3000 EXPOSE 3000 ENTRYPOINT ["docker-entrypoint.sh"] diff --git a/apps/studio/api/server.js b/apps/studio/api/server.js deleted file mode 100644 index e999b6ac104..00000000000 --- a/apps/studio/api/server.js +++ /dev/null @@ -1,50 +0,0 @@ -// STUDIO_FRAMEWORK gates whether this function actually serves the TanStack -// SSR handler. Vercel auto-detects every file under /api as a Function -// regardless of the framework preset (vercel.com/docs/functions), so we -// can't keep this file from being deployed in the Next.js prod build — we -// just make it inert when the env var is unset. -const isTanstack = process.env.STUDIO_FRAMEWORK === 'tanstack' - -// Computed path keeps `dist/server/server.js` out of Vercel's function -// bundler's static analysis. In the Next.js prod deploy the `dist/` tree -// doesn't exist, but Vercel still bundles this file because it lives under -// `api/`. With the .join() the bundler treats the import as runtime-only -// and the missing dist/ isn't a build error. In TanStack mode the SSR -// bundle is shipped into the function via the `functions['api/server.js'] -// .includeFiles` config in vercel.ts. -const tanstackEntry = ['..', 'dist', 'server', 'server.js'].join('/') - -// Initialize server-side Sentry BEFORE the handler module is imported, so its -// instrumentation is in place when route modules evaluate. Gated to TanStack -// (the Next deploy uses instrumentation.ts / sentry.server.config.ts instead). -// Vercel functions can't use a `--import` startup flag, so we import the -// instrument module here at boot. Vercel provides env vars via process.env. -// A Sentry boot failure must never take the API down — mirror scripts/serve.js -// and fall back to the identity wrapper if init or the SDK import throws. -let wrapFetchWithSentry = (fetchHandler) => fetchHandler -if (isTanstack) { - try { - await import('../instrument.server.mjs') - } catch (err) { - console.warn('[api/server] Sentry server init skipped:', err?.message ?? err) - } - ;({ wrapFetchWithSentry } = await import('@sentry/tanstackstart-react').catch(() => ({ - wrapFetchWithSentry: (fetchHandler) => fetchHandler, - }))) -} - -const rawHandler = isTanstack - ? (await import(tanstackEntry)).default - : { fetch: () => new Response('Not Found', { status: 404 }) } - -// Wrap the fetch handler so request-scoped errors (including those swallowed -// into a 500 downstream) are captured with request context. -const handler = isTanstack - ? { ...rawHandler, fetch: wrapFetchWithSentry(rawHandler.fetch.bind(rawHandler)) } - : rawHandler - -// Vercel's Web API handler convention: export an object with `fetch(request)`. -// TanStack's server build is already shaped that way — default-export it -// verbatim and Vercel hands us a real Web Request. -// eslint-disable-next-line no-restricted-exports -export default handler diff --git a/apps/studio/data/utils/deployment-commit-query.ts b/apps/studio/data/utils/deployment-commit-query.ts index 8fe1f972feb..a0ad17b89b2 100644 --- a/apps/studio/data/utils/deployment-commit-query.ts +++ b/apps/studio/data/utils/deployment-commit-query.ts @@ -5,12 +5,10 @@ import { BASE_PATH } from '@/lib/constants' import type { ResponseError, UseCustomQueryOptions } from '@/types' export async function getDeploymentCommit() { - // Deliberately unpinned: API fetches never carry the `?dpl=` skew-protection - // pin — only built asset URLs do (TanStack, see skewProtectionDpl in - // vite.config.ts) — so Vercel's edge routes this to the LATEST deployment - // and the check can detect a newer version while the session's assets stay - // pinned. We keep the basePath URL so it still routes to studio in - // production (root `/api/*` there is the marketing site). + // Deliberately unpinned: TanStack only adds x-deployment-id to server-function + // calls (start.ts), and Nitro's session cookie is disabled. This API request + // sees the latest deployment while older assets stay in the immutable store. + // Keep the basePath so www routes this request to Studio in production. const response = await fetchHandler(`${BASE_PATH}/api/get-deployment-commit`) return (await response.json()) as { commitSha: string; commitTime: string } } diff --git a/apps/studio/hooks/use-check-latest-deploy.tsx b/apps/studio/hooks/use-check-latest-deploy.tsx index 1bf32df8664..dfa450c0793 100644 --- a/apps/studio/hooks/use-check-latest-deploy.tsx +++ b/apps/studio/hooks/use-check-latest-deploy.tsx @@ -27,9 +27,8 @@ const DeployCheckToast = ({ id }: { id: string | number }) => {