diff --git a/.github/workflows/studio-docker-build.yml b/.github/workflows/studio-docker-build.yml index 409d2b9edbd..dcd33805726 100644 --- a/.github/workflows/studio-docker-build.yml +++ b/.github/workflows/studio-docker-build.yml @@ -36,3 +36,8 @@ jobs: - name: Build if: steps.filter.outputs.studio == 'true' run: docker build . -f apps/studio/Dockerfile --target production -t supabase-studio:local --build-arg NEXT_PUBLIC_STUDIO_AUTH_MODE=supabase --no-cache + # Reuses the shared stages (deps/dev) from the first build's layer + # cache; only the tanstack build/runtime stages run fresh. + - name: Build (tanstack) + if: steps.filter.outputs.studio == 'true' + run: docker build . -f apps/studio/Dockerfile --target production -t supabase-studio:local-tanstack --build-arg NEXT_PUBLIC_STUDIO_AUTH_MODE=supabase --build-arg STUDIO_FRAMEWORK=tanstack diff --git a/apps/studio/Dockerfile b/apps/studio/Dockerfile index 237b543b41c..9922ee29469 100644 --- a/apps/studio/Dockerfile +++ b/apps/studio/Dockerfile @@ -1,13 +1,22 @@ # To be run in the root of the turbo monorepo # NOTE: It's highly recommended to use the new builder, Buildkit. https://docs.docker.com/build/buildkit/ ## USAGE: -# Build: docker build . -f apps/studio/Dockerfile --target production -t studio:latest +# Build (Next): docker build . -f apps/studio/Dockerfile --target production -t studio:latest +# Build (TanStack): docker build . -f apps/studio/Dockerfile --target production -t studio:latest --build-arg STUDIO_FRAMEWORK=tanstack # Run: docker run -p 3000:3000 supabase/studio # Deploy: docker push supabase/studio:latest # Clean build: # docker builder prune # docker build . -f apps/studio/Dockerfile --target production -t studio:latest --no-cache +# Which framework's build ends up in the image. This is the same variable +# scripts/dispatch.js keys on for the dev/build/start scripts, so +# `--build-arg STUDIO_FRAMEWORK=tanstack` is the docker spelling of the +# switch used everywhere else. Framework selection happens at image build +# time (the two runtimes need different build outputs and dependency +# trees), not at container start. +ARG STUDIO_FRAMEWORK=next + FROM node:22-slim AS base ENV PNPM_HOME="/pnpm" ENV PATH="$PNPM_HOME:$PATH" @@ -50,15 +59,65 @@ EXPOSE 8082 CMD ["pnpm", "dev:studio"] # Compile Next.js -FROM dev AS builder - +FROM dev AS build-next RUN pnpm --filter studio exec next build +# Assemble the runtime tree at /srv in the layout the production stage +# serves from: Next's self-contained standalone output is the app root, +# with the static assets and public/ laid alongside it. +RUN mkdir -p /srv && \ + cp -a apps/studio/.next/standalone/. /srv/ && \ + mkdir -p /srv/apps/studio/.next && \ + cp -a apps/studio/.next/static /srv/apps/studio/.next/static && \ + cp -a apps/studio/public /srv/apps/studio/public + +# Compile TanStack Start (Vite) +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 + +# Alias whichever framework build was selected so the production stage can +# COPY from a single stage name. BuildKit only builds the selected branch. +FROM build-${STUDIO_FRAMEWORK} AS build + # Copy only compiled code and dependencies FROM base AS production -COPY --from=builder /app/apps/studio/public ./apps/studio/public -COPY --from=builder /app/apps/studio/.next/standalone ./ -COPY --from=builder /app/apps/studio/.next/static ./apps/studio/.next/static +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. +ENV PORT=3000 EXPOSE 3000 ENTRYPOINT ["docker-entrypoint.sh"] HEALTHCHECK --interval=5s --timeout=5s --retries=3 CMD node -e "fetch('http://localhost:3000/api/platform/profile').then((r) => {if (r.status !== 200) throw new Error(r.status)})" diff --git a/apps/studio/package.json b/apps/studio/package.json index 3a409431595..8bc27056365 100644 --- a/apps/studio/package.json +++ b/apps/studio/package.json @@ -61,9 +61,11 @@ "@monaco-editor/react": "catalog:", "@next/bundle-analyzer": "16.2.3", "@number-flow/react": "^0.3.2", + "@radix-ui/react-use-escape-keydown": "^1.1.1", "@sentry/nextjs": "catalog:", "@sentry/react": "^10.27.0", "@sentry/tanstackstart-react": "catalog:", + "@smithy/property-provider": "^4.0.4", "@std/path": "npm:@jsr/std__path@^1.0.8", "@stripe/react-stripe-js": "6.1.0", "@stripe/stripe-js": "9.1.0", @@ -91,6 +93,7 @@ "ai": "^6.0.174", "ai-commands": "workspace:*", "awesome-debounce-promise": "^2.1.0", + "braintrust": "^3.9.0", "class-variance-authority": "^0.7.1", "common": "workspace:*", "common-tags": "^1.8.2", @@ -112,6 +115,7 @@ "idb": "^8.0.2", "ip-num": "^1.5.1", "json-logic-js": "^2.0.2", + "libpg-query": "17.6.0", "lodash": "catalog:", "lodash-es": "catalog:", "lucide-react": "^0.436.0", @@ -164,8 +168,6 @@ "@faker-js/faker": "^9.9.0", "@graphql-codegen/cli": "5.0.5", "@graphql-typed-document-node/core": "^3.2.0", - "@radix-ui/react-use-escape-keydown": "^1.1.1", - "@smithy/property-provider": "^4.0.4", "@tailwindcss/vite": "4.2.4", "@tanstack/devtools-vite": "^0.6.0", "@testing-library/dom": "^10.0.0", @@ -193,7 +195,6 @@ "@vitest/ui": "catalog:", "api-types": "workspace:*", "autoevals": "^0.0.132", - "braintrust": "^3.9.0", "common": "workspace:*", "config": "workspace:*", "date-fns": "^2.30.0", @@ -205,7 +206,6 @@ "graphql-ws": "5.14.1", "import-in-the-middle": "^2.0.0", "jsdom-testing-mocks": "^1.13.1", - "libpg-query": "17.6.0", "magic-string": "^0.30.21", "msw": "^2.3.0", "next-router-mock": "^0.9.13", diff --git a/apps/studio/vite.config.ts b/apps/studio/vite.config.ts index 7300e86046c..29998d6d8a2 100644 --- a/apps/studio/vite.config.ts +++ b/apps/studio/vite.config.ts @@ -670,6 +670,15 @@ export default defineConfig(({ command, mode }) => { server: { port: 3000, }, + preview: { + // The prerender step (@tanstack/start-plugin-core) boots `vite preview` + // on an ephemeral port and crawls the first resolved URL. With the + // default host (`localhost`) the server can bind the IPv6 loopback + // while the crawler's fetch connects to 127.0.0.1 — split name + // resolution that ECONNREFUSEDs the whole prerender inside docker + // build containers. Pin both sides to IPv4 loopback. + host: '127.0.0.1', + }, resolve: { tsconfigPaths: true, alias: [ diff --git a/package.json b/package.json index 7ccfdf72a22..69710876339 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,7 @@ "build": "turbo run build", "build:studio": "turbo run build --filter=studio", "build:studio:docker": "docker build . -f apps/studio/Dockerfile --target production -t supabase-studio:local --build-arg NEXT_PUBLIC_STUDIO_AUTH_MODE=supabase --no-cache", + "build:studio:docker:tanstack": "docker build . -f apps/studio/Dockerfile --target production -t supabase-studio:local-tanstack --build-arg NEXT_PUBLIC_STUDIO_AUTH_MODE=supabase --build-arg STUDIO_FRAMEWORK=tanstack --no-cache", "build:design-system": "turbo run build --filter=design-system", "build:docs": "turbo run build --filter=docs", "clean": "turbo run clean --parallel && rimraf -G .turbo/cache && pnpm purge", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 774c859a789..4a09e2af934 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -951,6 +951,9 @@ importers: '@number-flow/react': specifier: ^0.3.2 version: 0.3.2(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@radix-ui/react-use-escape-keydown': + specifier: ^1.1.1 + version: 1.1.1(@types/react@19.2.14)(react@19.2.6) '@sentry/nextjs': specifier: 'catalog:' version: 10.59.0(@opentelemetry/core@2.8.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.8.0(@opentelemetry/api@1.9.1))(encoding@0.1.13)(next@16.2.6(@babel/core@7.29.7(supports-color@8.1.1))(@opentelemetry/api@1.9.1)(@playwright/test@1.59.1)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(sass@1.77.4))(react@19.2.6)(supports-color@8.1.1)(vite@8.0.16(@types/node@22.13.14)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.77.4)(terser@5.39.0)(tsx@4.22.4)(yaml@2.9.0))(webpack@5.105.4(esbuild@0.28.1)) @@ -960,6 +963,9 @@ importers: '@sentry/tanstackstart-react': specifier: 'catalog:' version: 10.59.0(encoding@0.1.13)(react@19.2.6)(rollup@4.60.3)(supports-color@8.1.1)(vite@8.0.16(@types/node@22.13.14)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.77.4)(terser@5.39.0)(tsx@4.22.4)(yaml@2.9.0)) + '@smithy/property-provider': + specifier: ^4.0.4 + version: 4.0.4 '@std/path': specifier: npm:@jsr/std__path@^1.0.8 version: '@jsr/std__path@1.0.8' @@ -1044,6 +1050,9 @@ importers: awesome-debounce-promise: specifier: ^2.1.0 version: 2.1.0 + braintrust: + specifier: ^3.9.0 + version: 3.9.0(@aws-sdk/credential-provider-web-identity@3.972.38)(supports-color@8.1.1)(zod@3.25.76) class-variance-authority: specifier: ^0.7.1 version: 0.7.1 @@ -1107,6 +1116,9 @@ importers: json-logic-js: specifier: ^2.0.2 version: 2.0.2 + libpg-query: + specifier: 17.6.0 + version: 17.6.0 lodash: specifier: ^4.18.1 version: 4.18.1 @@ -1258,12 +1270,6 @@ importers: '@jridgewell/remapping': specifier: ^2.3.5 version: 2.3.5 - '@radix-ui/react-use-escape-keydown': - specifier: ^1.1.1 - version: 1.1.1(@types/react@19.2.14)(react@19.2.6) - '@smithy/property-provider': - specifier: ^4.0.4 - version: 4.0.4 '@tailwindcss/vite': specifier: 4.2.4 version: 4.2.4(vite@8.0.16(@types/node@22.13.14)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.77.4)(terser@5.39.0)(tsx@4.22.4)(yaml@2.9.0)) @@ -1345,9 +1351,6 @@ importers: autoevals: specifier: ^0.0.132 version: 0.0.132(ws@8.21.0) - braintrust: - specifier: ^3.9.0 - version: 3.9.0(@aws-sdk/credential-provider-web-identity@3.972.38)(supports-color@8.1.1)(zod@3.25.76) date-fns: specifier: ^2.30.0 version: 2.30.0 @@ -1375,9 +1378,6 @@ importers: jsdom-testing-mocks: specifier: ^1.13.1 version: 1.13.1 - libpg-query: - specifier: 17.6.0 - version: 17.6.0 magic-string: specifier: ^0.30.21 version: 0.30.21 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 95dd48fb645..079705c1466 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -56,16 +56,27 @@ catalog: allowBuilds: '@parcel/watcher': false '@sentry/cli': false + '@supabase/build-icons@file:packages/build-icons': set this to true or false + '@supabase/pg-meta@file:packages/pg-meta': set this to true or false + ai-commands@file:packages/ai-commands: set this to true or false + api-types@file:packages/api-types: set this to true or false + common@file:packages/common: set this to true or false + config@file:packages/config: set this to true or false contentlayer2: false core-js: false + dev-tools@file:packages/dev-tools: set this to true or false es5-ext: false esbuild: false + icons@file:packages/icons: set this to true or false libpg-query: false msw: false node-pty: true protobufjs: false + shared-data@file:packages/shared-data: set this to true or false sharp: false supabase: true + ui-patterns@file:packages/ui-patterns: set this to true or false + ui@file:packages/ui: set this to true or false minimumReleaseAge: 4320