fix(studio): keep sharp out of the self-hosted standalone build (#50658)

## I have read the
[CONTRIBUTING.md](https://github.com/supabase/supabase/blob/master/CONTRIBUTING.md)
file.

YES

## What kind of change does this PR introduce?

Bug fix (self-hosted Studio image packaging).

## What is the current behavior?

Since the Next 16.3.5 bump, the self-hosted `next build` (`output:
'standalone'`) traces Next's optional `sharp` dependency into
`.next/standalone`. Next 16.3.5 upgraded `@vercel/nft`
(vercel/next.js#93979), so tracing now follows `module-sync` export
conditions, and Next only excludes sharp from the trace when it detects
Vercel.

Effects:

- The self-hosted standalone output carries the `sharp` and `@img/*`
native binaries, roughly 10 to 20 percent of the image.
- The slim-services Studio image fails: it loads every `.node` file
eagerly, and sharp's binary segfaults on linux/amd64 without the
matching libvips shared library.

`apps/studio` does not depend on `sharp` directly. The last Studio image
without sharp in the trace was `2026.09.14`.

## What is the new behavior?

Single-file change to `apps/studio/next.config.ts`. When
`NEXT_PUBLIC_IS_PLATFORM` is not `'true'`:

- `images.unoptimized: true`, so `next/image` renders plain `<img>` and
the server 404s `/_next/image` before sharp is ever loaded. This matches
what the TanStack build already does via `compat/next/image.tsx`.
- `outputFileTracingExcludes` drops `**/node_modules/sharp/**` and
`**/node_modules/@img/**` from the standalone trace.

Hosted Studio is unchanged: `unoptimized` stays `false`, the exclude key
is omitted, and image optimization keeps running on Vercel. Self-hosted
CSP is `frame-ancestors 'none'` only, so loading remote avatars directly
instead of through `/_next/image` does not hit a CSP rule.

Also hoists the existing `isPlatform` const from `redirects()` to module
scope.

**Why the globs start with `../../`.** Studio builds with Turbopack,
which resolves `outputFileTracingExcludes` relative to the app
directory. A plain `**/node_modules/sharp/**` becomes
`apps/studio/**/node_modules/sharp/**` and never matches the pnpm store
hoisted to the monorepo root. Each leading `../` moves the glob root up
one level (`relativize_glob` in Next's `crates/next-core/src/util.rs`),
so `../../` anchors the pattern at the repo root. The webpack path
applies the same globs unprefixed for the server trace, so this is
Turbopack-specific.

## Additional context

Considered and rejected: a Next.js issue. Next intentionally ships sharp
for self-hosted `next start` image optimization and intentionally
excludes it only on Vercel. Opting out per app is the supported path.

Test plan:

- CI: typecheck, lint, Prettier, Studio unit tests, Studio Docker Build.
- Verified on this PR with a temporary step in the Studio Docker Build
workflow that ran `find` inside the production image for
`node_modules/sharp*` and `node_modules/@img*` files. It failed on the
first commit (globs rooted at `apps/studio`, image still contained
`@img/colour`) and passed once the globs were anchored at the repo root.
The step was removed before merge.
- Hosted preview should still serve optimized images from
`/_next/image`.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

https://claude.ai/code/session_01SZHcgqB2qNqCvWsFJ9Qvo8

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **Refactor**
* Improved configuration handling for platform and self-hosted
deployments.
* Self-hosted builds now avoid bundling unnecessary image-processing
binaries, while platform deployments retain image optimization support.
  * Clarified configuration comments.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Andrew ValleteauandClaude Fable 5.1 authored and GitHub committed 2026-09-21 18:45:15 +02:00
1 parent 84e46779af
commit 6fab3bd789
1 file changed
+17 -1
+17 -1
View File
@@ -35,6 +35,8 @@ function getAssetPrefix() {
return `${SUPABASE_ASSETS_URL}/${process.env.SITE_NAME}/${process.env.VERCEL_GIT_COMMIT_SHA?.substring(0, 12) ?? 'unknown'}`
}
const isPlatform = process.env.NEXT_PUBLIC_IS_PLATFORM === 'true'
const marketplaceApiUrl = process.env.NEXT_PUBLIC_MARKETPLACE_API_URL
? new URL(process.env.NEXT_PUBLIC_MARKETPLACE_API_URL)
: null
@@ -72,7 +74,6 @@ const nextConfig = {
// auto-prepends `basePath` to source and destination on its own,
// except for the special `/` → basePath bounce below which opts out
// via `basePath: false`.
const isPlatform = process.env.NEXT_PUBLIC_IS_PLATFORM === 'true'
const maintenance = process.env.MAINTENANCE_MODE === 'true'
return [
...(isPlatform ? PLATFORM_REDIRECTS : SELF_HOSTED_REDIRECTS),
@@ -145,6 +146,9 @@ const nextConfig = {
]
},
images: {
// Self-hosted: serve plain <img> (as the TanStack shim does) so Next never
// loads sharp. Hosted Studio optimizes images on Vercel.
unoptimized: !isPlatform,
dangerouslyAllowSVG: false,
remotePatterns: [
{
@@ -183,6 +187,18 @@ const nextConfig = {
: []),
],
},
// Keep Next's optional sharp dependency out of the self-hosted standalone
// output. It is unused with `unoptimized` above, and its native binaries
// break the slim-services Studio image (sharp's .node segfaults without
// libvips). Globs resolve from `apps/studio`; `../../` anchors them at the
// repo root where pnpm hoists the store.
...(isPlatform
? {}
: {
outputFileTracingExcludes: {
'*': ['../../**/node_modules/sharp/**/*', '../../**/node_modules/@img/**/*'],
},
}),
transpilePackages: ['ui', 'ui-patterns', 'common', 'shared-data', 'api-types', 'icons'],
serverExternalPackages: ['libpg-query'],
turbopack: {