mirror of
https://github.com/supabase/supabase.git
synced 2026-10-05 17:35:10 +03:00
Extracted from the TanStack Start migration (#46424) to shrink that PR. The self-hosted storage/auth API routes each constructed a module-scope admin client (`createClient(process.env.SUPABASE_URL!, process.env.SUPABASE_SERVICE_KEY!)`). Those env vars only exist on self-hosted, so eager module-scope construction is wasteful on platform and fragile on any runtime that evaluates an API module before its route is hit (constructing with `undefined` credentials throws on import). **Changed:** - Add `lib/api/self-hosted-admin.ts` — `selfHostedSupabaseAdmin`, a `Proxy` that defers `createClient(...)` until first property access (inside a handler, i.e. on self-hosted where the vars are set). - Swap **all 17** storage/auth/vector-bucket handlers from module-scope `createClient(...)` to `import { selfHostedSupabaseAdmin as supabase }`. - **Enforce it:** add an eslint `no-restricted-syntax` rule banning module-scope `createClient` in `pages/api/**` + `routes/**` (now that every flagged handler is lazy). The same eslint config block also carries an analytics-SQL boundary rule — 0 violations on master. Behaviour is unchanged (the client is still built lazily inside the handler). This is also the change that makes those routes safe under TanStack's single-handler module evaluation. ## To test - Self-hosted Studio: storage buckets/objects, vector buckets, and auth users operations work as before. ## Verification studio lint (0 errors, both rules active) ✓ · studio typecheck ✓. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Refactor** * Standardized self-hosted Supabase admin client usage across platform authentication and storage endpoints, removing per-route client setup. * Improved reliability by lazily creating the admin client only when first used. * **Chores / Tooling** * Updated ESLint rules to prevent module-scope Supabase client creation in API routes and to enforce safe analytics SQL access patterns. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: Alaister Young <10985857+alaister@users.noreply.github.com> Co-authored-by: Ali Waseem <waseema393@gmail.com>
This commit is contained in:
19 files changed
+82
-63
No files matched your search
@@ -3,6 +3,37 @@ const barrelFiles = require('eslint-plugin-barrel-files')
|
||||
const jsxA11y = require('eslint-plugin-jsx-a11y')
|
||||
const supabaseConfig = require('eslint-config-supabase/next')
|
||||
|
||||
// Analytics SQL wire boundary — see the block below for context. Shared so the
|
||||
// API/route block can re-include it (flat config replaces, not merges, a rule's
|
||||
// options when blocks overlap, so the later block must carry these forward).
|
||||
const ANALYTICS_SQL_RESTRICTED_SYNTAX = [
|
||||
{
|
||||
selector:
|
||||
"CallExpression[callee.name=/^(post|get)$/][arguments.0.value='/platform/projects/{ref}/analytics/endpoints/logs.all']",
|
||||
message:
|
||||
'Do not call the analytics logs.all endpoint directly. Route through executeAnalyticsSql in @/data/logs/execute-analytics-sql so the SafeLogSqlFragment brand is enforced at compile time.',
|
||||
},
|
||||
{
|
||||
selector:
|
||||
"CallExpression[callee.name=/^(post|get)$/][arguments.0.value='/platform/projects/{ref}/analytics/endpoints/logs.all.otel']",
|
||||
message:
|
||||
'Do not call the analytics logs.all.otel endpoint directly. Route through executeAnalyticsSql in @/data/logs/execute-analytics-sql so the SafeLogSqlFragment brand is enforced at compile time.',
|
||||
},
|
||||
]
|
||||
|
||||
// Ban constructing a Supabase client at module scope in API route files. The
|
||||
// TanStack server imports the entire route tree at boot (loadEntries), so a
|
||||
// module-scope createClient with an env var that's unset in that environment
|
||||
// (e.g. SUPABASE_URL on platform) throws on import and 500s every route — a
|
||||
// runtime-only failure that's painful to catch. Construct it lazily inside the
|
||||
// handler instead (see lib/api/self-hosted-admin.ts).
|
||||
const NO_MODULE_SCOPE_CREATE_CLIENT = {
|
||||
selector:
|
||||
":matches(Program, ExportNamedDeclaration) > VariableDeclaration > VariableDeclarator > CallExpression[callee.name='createClient']",
|
||||
message:
|
||||
'Do not construct a Supabase client at module scope in API route files — the TanStack server evaluates every route module at boot, so a missing env var (e.g. SUPABASE_URL on platform) crashes every route. Construct it lazily inside the handler (see lib/api/self-hosted-admin.ts).',
|
||||
}
|
||||
|
||||
module.exports = defineConfig([
|
||||
{ files: ['**/*.ts', '**/*.tsx'] },
|
||||
supabaseConfig,
|
||||
@@ -40,21 +71,21 @@ module.exports = defineConfig([
|
||||
{
|
||||
files: ['**/*.ts', '**/*.tsx'],
|
||||
ignores: ['data/logs/execute-analytics-sql.ts'],
|
||||
rules: {
|
||||
'no-restricted-syntax': ['error', ...ANALYTICS_SQL_RESTRICTED_SYNTAX],
|
||||
},
|
||||
},
|
||||
// API route modules are eagerly imported by the TanStack server at boot, so
|
||||
// module-scope side effects there are especially dangerous. This block also
|
||||
// re-includes the analytics selectors because flat config replaces (not
|
||||
// merges) a rule's options for overlapping files.
|
||||
{
|
||||
files: ['pages/api/**/*.ts', 'pages/api/**/*.tsx', 'routes/**/*.ts', 'routes/**/*.tsx'],
|
||||
rules: {
|
||||
'no-restricted-syntax': [
|
||||
'error',
|
||||
{
|
||||
selector:
|
||||
"CallExpression[callee.name=/^(post|get)$/][arguments.0.value='/platform/projects/{ref}/analytics/endpoints/logs.all']",
|
||||
message:
|
||||
'Do not call the analytics logs.all endpoint directly. Route through executeAnalyticsSql in @/data/logs/execute-analytics-sql so the SafeLogSqlFragment brand is enforced at compile time.',
|
||||
},
|
||||
{
|
||||
selector:
|
||||
"CallExpression[callee.name=/^(post|get)$/][arguments.0.value='/platform/projects/{ref}/analytics/endpoints/logs.all.otel']",
|
||||
message:
|
||||
'Do not call the analytics logs.all.otel endpoint directly. Route through executeAnalyticsSql in @/data/logs/execute-analytics-sql so the SafeLogSqlFragment brand is enforced at compile time.',
|
||||
},
|
||||
...ANALYTICS_SQL_RESTRICTED_SYNTAX,
|
||||
NO_MODULE_SCOPE_CREATE_CLIENT,
|
||||
],
|
||||
},
|
||||
},
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
import { createClient, SupabaseClient } from '@supabase/supabase-js'
|
||||
|
||||
// Lazy admin client for self-hosted API routes under
|
||||
// `pages/api/platform/{auth,storage}/**`. SUPABASE_URL and
|
||||
// SUPABASE_SERVICE_KEY are only set on self-hosted deployments — the
|
||||
// platform build doesn't need these env vars. But on the TanStack Start
|
||||
// server, every API route's module gets evaluated when the single function
|
||||
// handler loads, regardless of whether its URL is hit. Without a lazy
|
||||
// wrapper, constructing the client at module scope with undefined
|
||||
// credentials would crash every request on platform.
|
||||
//
|
||||
// Proxy defers client construction until a property is actually accessed,
|
||||
// which only happens inside the handler (i.e. on self-hosted where the env
|
||||
// vars are set).
|
||||
let _client: SupabaseClient | undefined
|
||||
|
||||
export const selfHostedSupabaseAdmin = new Proxy({} as SupabaseClient, {
|
||||
get(_target, prop) {
|
||||
_client ??= createClient(process.env.SUPABASE_URL!, process.env.SUPABASE_SERVICE_KEY!)
|
||||
return Reflect.get(_client, prop)
|
||||
},
|
||||
})
|
||||
@@ -1,9 +1,7 @@
|
||||
import { createClient } from '@supabase/supabase-js'
|
||||
import { NextApiRequest, NextApiResponse } from 'next'
|
||||
|
||||
import apiWrapper from '@/lib/api/apiWrapper'
|
||||
|
||||
const supabase = createClient(process.env.SUPABASE_URL!, process.env.SUPABASE_SERVICE_KEY!)
|
||||
import { selfHostedSupabaseAdmin as supabase } from '@/lib/api/self-hosted-admin'
|
||||
|
||||
export default (req: NextApiRequest, res: NextApiResponse) => apiWrapper(req, res, handler)
|
||||
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
import { createClient } from '@supabase/supabase-js'
|
||||
import { NextApiRequest, NextApiResponse } from 'next'
|
||||
|
||||
import apiWrapper from '@/lib/api/apiWrapper'
|
||||
|
||||
const supabase = createClient(process.env.SUPABASE_URL!, process.env.SUPABASE_SERVICE_KEY!)
|
||||
import { selfHostedSupabaseAdmin as supabase } from '@/lib/api/self-hosted-admin'
|
||||
|
||||
export default (req: NextApiRequest, res: NextApiResponse) => apiWrapper(req, res, handler)
|
||||
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
import { createClient } from '@supabase/supabase-js'
|
||||
import { NextApiRequest, NextApiResponse } from 'next'
|
||||
|
||||
import apiWrapper from '@/lib/api/apiWrapper'
|
||||
|
||||
const supabase = createClient(process.env.SUPABASE_URL!, process.env.SUPABASE_SERVICE_KEY!)
|
||||
import { selfHostedSupabaseAdmin as supabase } from '@/lib/api/self-hosted-admin'
|
||||
|
||||
export default (req: NextApiRequest, res: NextApiResponse) => apiWrapper(req, res, handler)
|
||||
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
import { createClient } from '@supabase/supabase-js'
|
||||
import { NextApiRequest, NextApiResponse } from 'next'
|
||||
|
||||
import apiWrapper from '@/lib/api/apiWrapper'
|
||||
|
||||
const supabase = createClient(process.env.SUPABASE_URL!, process.env.SUPABASE_SERVICE_KEY!)
|
||||
import { selfHostedSupabaseAdmin as supabase } from '@/lib/api/self-hosted-admin'
|
||||
|
||||
export default (req: NextApiRequest, res: NextApiResponse) => apiWrapper(req, res, handler)
|
||||
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
import { createClient } from '@supabase/supabase-js'
|
||||
import { NextApiRequest, NextApiResponse } from 'next'
|
||||
|
||||
import apiWrapper from '@/lib/api/apiWrapper'
|
||||
|
||||
const supabase = createClient(process.env.SUPABASE_URL!, process.env.SUPABASE_SERVICE_KEY!)
|
||||
import { selfHostedSupabaseAdmin as supabase } from '@/lib/api/self-hosted-admin'
|
||||
|
||||
export default (req: NextApiRequest, res: NextApiResponse) => apiWrapper(req, res, handler)
|
||||
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
import { createClient } from '@supabase/supabase-js'
|
||||
import { NextApiRequest, NextApiResponse } from 'next'
|
||||
|
||||
import apiWrapper from '@/lib/api/apiWrapper'
|
||||
|
||||
const supabase = createClient(process.env.SUPABASE_URL!, process.env.SUPABASE_SERVICE_KEY!)
|
||||
import { selfHostedSupabaseAdmin as supabase } from '@/lib/api/self-hosted-admin'
|
||||
|
||||
export default (req: NextApiRequest, res: NextApiResponse) => apiWrapper(req, res, handler)
|
||||
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
import { createClient } from '@supabase/supabase-js'
|
||||
import { NextApiRequest, NextApiResponse } from 'next'
|
||||
|
||||
import apiWrapper from '@/lib/api/apiWrapper'
|
||||
|
||||
const supabase = createClient(process.env.SUPABASE_URL!, process.env.SUPABASE_SERVICE_KEY!)
|
||||
import { selfHostedSupabaseAdmin as supabase } from '@/lib/api/self-hosted-admin'
|
||||
|
||||
export default (req: NextApiRequest, res: NextApiResponse) => apiWrapper(req, res, handler)
|
||||
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
import { createClient } from '@supabase/supabase-js'
|
||||
import { NextApiRequest, NextApiResponse } from 'next'
|
||||
|
||||
import apiWrapper from '@/lib/api/apiWrapper'
|
||||
|
||||
const supabase = createClient(process.env.SUPABASE_URL!, process.env.SUPABASE_SERVICE_KEY!)
|
||||
import { selfHostedSupabaseAdmin as supabase } from '@/lib/api/self-hosted-admin'
|
||||
|
||||
export default (req: NextApiRequest, res: NextApiResponse) => apiWrapper(req, res, handler)
|
||||
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
import { createClient } from '@supabase/supabase-js'
|
||||
import { NextApiRequest, NextApiResponse } from 'next'
|
||||
|
||||
import apiWrapper from '@/lib/api/apiWrapper'
|
||||
|
||||
const supabase = createClient(process.env.SUPABASE_URL!, process.env.SUPABASE_SERVICE_KEY!)
|
||||
import { selfHostedSupabaseAdmin as supabase } from '@/lib/api/self-hosted-admin'
|
||||
|
||||
export default (req: NextApiRequest, res: NextApiResponse) => apiWrapper(req, res, handler)
|
||||
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
import { createClient } from '@supabase/supabase-js'
|
||||
import { NextApiRequest, NextApiResponse } from 'next'
|
||||
|
||||
import apiWrapper from '@/lib/api/apiWrapper'
|
||||
|
||||
const supabase = createClient(process.env.SUPABASE_URL!, process.env.SUPABASE_SERVICE_KEY!)
|
||||
import { selfHostedSupabaseAdmin as supabase } from '@/lib/api/self-hosted-admin'
|
||||
|
||||
export default (req: NextApiRequest, res: NextApiResponse) => apiWrapper(req, res, handler)
|
||||
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
import { createClient } from '@supabase/supabase-js'
|
||||
import { NextApiRequest, NextApiResponse } from 'next'
|
||||
|
||||
import apiWrapper from '@/lib/api/apiWrapper'
|
||||
|
||||
const supabase = createClient(process.env.SUPABASE_URL!, process.env.SUPABASE_SERVICE_KEY!)
|
||||
import { selfHostedSupabaseAdmin as supabase } from '@/lib/api/self-hosted-admin'
|
||||
|
||||
const wrappedHandler = (req: NextApiRequest, res: NextApiResponse) => apiWrapper(req, res, handler)
|
||||
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
import { createClient } from '@supabase/supabase-js'
|
||||
import { NextApiRequest, NextApiResponse } from 'next'
|
||||
|
||||
import apiWrapper from '@/lib/api/apiWrapper'
|
||||
|
||||
const supabase = createClient(process.env.SUPABASE_URL!, process.env.SUPABASE_SERVICE_KEY!)
|
||||
import { selfHostedSupabaseAdmin as supabase } from '@/lib/api/self-hosted-admin'
|
||||
|
||||
export default (req: NextApiRequest, res: NextApiResponse) => apiWrapper(req, res, handler)
|
||||
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
import { createClient } from '@supabase/supabase-js'
|
||||
import { NextApiRequest, NextApiResponse } from 'next'
|
||||
|
||||
import apiWrapper from '@/lib/api/apiWrapper'
|
||||
|
||||
const supabase = createClient(process.env.SUPABASE_URL!, process.env.SUPABASE_SERVICE_KEY!)
|
||||
import { selfHostedSupabaseAdmin as supabase } from '@/lib/api/self-hosted-admin'
|
||||
|
||||
export default (req: NextApiRequest, res: NextApiResponse) => apiWrapper(req, res, handler)
|
||||
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
import { createClient } from '@supabase/supabase-js'
|
||||
import { NextApiRequest, NextApiResponse } from 'next'
|
||||
|
||||
import apiWrapper from '@/lib/api/apiWrapper'
|
||||
|
||||
const supabase = createClient(process.env.SUPABASE_URL!, process.env.SUPABASE_SERVICE_KEY!)
|
||||
import { selfHostedSupabaseAdmin as supabase } from '@/lib/api/self-hosted-admin'
|
||||
|
||||
// eslint-disable-next-line import/no-anonymous-default-export
|
||||
export default (req: NextApiRequest, res: NextApiResponse) => apiWrapper(req, res, handler)
|
||||
|
||||
+1
-3
@@ -1,9 +1,7 @@
|
||||
import { createClient } from '@supabase/supabase-js'
|
||||
import { NextApiRequest, NextApiResponse } from 'next'
|
||||
|
||||
import apiWrapper from '@/lib/api/apiWrapper'
|
||||
|
||||
const supabase = createClient(process.env.SUPABASE_URL!, process.env.SUPABASE_SERVICE_KEY!)
|
||||
import { selfHostedSupabaseAdmin as supabase } from '@/lib/api/self-hosted-admin'
|
||||
|
||||
// eslint-disable-next-line import/no-anonymous-default-export
|
||||
export default (req: NextApiRequest, res: NextApiResponse) => apiWrapper(req, res, handler)
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
import { createClient } from '@supabase/supabase-js'
|
||||
import { NextApiRequest, NextApiResponse } from 'next'
|
||||
|
||||
import apiWrapper from '@/lib/api/apiWrapper'
|
||||
|
||||
const supabase = createClient(process.env.SUPABASE_URL!, process.env.SUPABASE_SERVICE_KEY!)
|
||||
import { selfHostedSupabaseAdmin as supabase } from '@/lib/api/self-hosted-admin'
|
||||
|
||||
// eslint-disable-next-line import/no-anonymous-default-export
|
||||
export default (req: NextApiRequest, res: NextApiResponse) => apiWrapper(req, res, handler)
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
import { createClient } from '@supabase/supabase-js'
|
||||
import { NextApiRequest, NextApiResponse } from 'next'
|
||||
|
||||
import apiWrapper from '@/lib/api/apiWrapper'
|
||||
|
||||
const supabase = createClient(process.env.SUPABASE_URL!, process.env.SUPABASE_SERVICE_KEY!)
|
||||
import { selfHostedSupabaseAdmin as supabase } from '@/lib/api/self-hosted-admin'
|
||||
|
||||
// eslint-disable-next-line import/no-anonymous-default-export
|
||||
export default (req: NextApiRequest, res: NextApiResponse) => apiWrapper(req, res, handler)
|
||||
|
||||
Reference in new issue
Block a user