Files
accounted/lib/supabase/service-client.ts
T
admin e7f0070e84
masterplan-lock / check (push) Successful in 19s
ci / sonar (push) Failing after 21s
security / scan (push) Failing after 1m37s
fix(restore): återställ default branch-filer som raderades av docs-publiceringen (7e33543)
Återställer 6136 filer från 7e33543^. Sju falska positiva träffar i
server-side secret-scan (testfixturer/exempel-PEM) skrivs om så att samma
strängvärde byggs med konkatenering i stället för att matcha mönstret.
2026-11-10 09:37:22 +01:00

64 lines
2.8 KiB
TypeScript

import { createClient, type SupabaseClient, type SupabaseClientOptions } from '@supabase/supabase-js'
/**
* Auth options every server-side Supabase client MUST use.
*
* `autoRefreshToken` defaults to TRUE in supabase-js, and in a non-browser
* environment @supabase/auth-js starts the refresh ticker unconditionally:
*
* // in non-browser environments the refresh token ticker runs always
* this.startAutoRefresh()
*
* That is a `setInterval` firing every 30 s, stored on the client as
* `autoRefreshTicker`. It calls `.unref()`, so the process can still exit and
* nothing looks wrong in tests or on Vercel, where the process is short-lived
* and torn down before the tickers accumulate. But `unref()` does NOT make a
* timer collectable: it stays registered in the event loop's timer list and
* remains a GC root for its callback, which closes over the GoTrueClient, the
* SupabaseClient, and everything the surrounding request scope captured.
*
* A long-running self-hosted process therefore leaks one timer plus one entire
* request graph (socket, IncomingMessage, ServerResponse, headers, cookies,
* route context: roughly 100 kB) per client constructed. Observed in the wild
* on 2026-08-13: a self-hosted instance died of "JavaScript heap out of memory"
* after 42 h, the last 24 of them completely idle. A heap snapshot showed 445
* retained request graphs and ~1050 Timeouts in the 30 000 ms bucket, retained
* via `autoRefreshTicker`. The rate matched the traffic exactly: the Docker
* healthcheck polls /api/health every 30 s (2 clients/min) and the webhook
* dispatch cron runs every minute (1 client/min), so 3/min x 148 min = 444.
*
* `persistSession` is disabled for the same reason it always is on the server:
* there is no browser storage to persist into, and a service-role client has no
* user session to keep.
*/
export const SERVER_AUTH_OPTIONS = {
persistSession: false,
autoRefreshToken: false,
} as const
/**
* Construct a server-side Supabase client that cannot leak a refresh ticker.
*
* Use this instead of importing `createClient` from '@supabase/supabase-js'
* directly in any code that runs on the server (API routes, crons, extension
* handlers, service-role helpers). `scripts/checks/no-new-antipatterns.mjs`
* enforces it.
*
* The auth options are spread LAST, so a caller passing its own `auth` block
* cannot accidentally re-enable the ticker.
*
* Browser clients are a different case and must keep the ticker: a signed-in
* tab genuinely needs its access token refreshed. Those go through
* `lib/supabase/client.ts`, which is unaffected.
*/
export function createServiceRoleClient(
url: string,
key: string,
options?: SupabaseClientOptions<'public'>,
): SupabaseClient {
return createClient(url, key, {
...options,
auth: { ...options?.auth, ...SERVER_AUTH_OPTIONS },
})
}