fix(supabase): stop server clients leaking a 30s refresh ticker per request (#1612)
* fix(supabase): stop server clients leaking a 30s refresh ticker per request
`autoRefreshToken` defaults to true in supabase-js, and off-browser
@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. It calls unref(), so the process
still exits, tests pass, and Vercel never notices because the process is
torn down long before the tickers accumulate. But unref() does not make a
timer collectable: it stays registered in the event loop and remains a GC
root for its callback, which closes over the GoTrueClient, the
SupabaseClient, and the whole request scope around it.
A long-running self-hosted instance therefore leaks one timer plus one
entire request graph (socket, IncomingMessage, ServerResponse, headers,
route context: ~100 kB) per client constructed. One died of "JavaScript
heap out of memory" after 42 h, the last 24 of them completely idle. The
heap snapshot showed 445 retained request graphs and ~1050 Timeouts in
the 30 000 ms bucket, retained via `autoRefreshTicker`, and the rate
matched the traffic exactly: the Docker healthcheck polls /api/health
every 30 s and the webhook dispatch cron runs every minute, so
3 clients/min x 148 min = 444.
- new lib/supabase/service-client.ts: createServiceRoleClient() applies
SERVER_AUTH_OPTIONS, spread LAST so a caller passing its own auth block
cannot re-enable the ticker
- 22 call sites migrated; only booking-templates/sync/cron had ever
passed the options itself
- guard 9 in no-new-antipatterns.mjs fails CI on any new value import of
supabase-js's createClient outside the wrapper; type-only imports are
fine. Verified to fail on a deliberate regression and pass once fixed
- browser clients untouched: a signed-in tab genuinely needs the refresh,
and lib/supabase/client.ts is built on createBrowserClient anyway
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(checks): catch namespace imports in the leaky-supabase-client guard
The guard only matched named imports, so
import * as sb from '@supabase/supabase-js'
sb.createClient(url, key)
reached createClient through member access without ever naming it, and
passed. Verified against the real script before and after: the shape is
flagged now, and `import type * as sb` still passes.
Namespace value imports are treated as leaky outright rather than tracking
member access, which keeps the check a regex over source text with no new
dependency.
Review also suggested excluding *.test.tsx alongside *.test.ts. Skipped: the
repo has no .test.tsx files, and all four sibling checks in this file use
`.test.ts`. Diverging in one of them would read as an accident; if such files
appear, all four should change together.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
4921d1da5e
commit
43a71aec3c
@@ -946,6 +946,8 @@ One line per decision: `[YYYY-MM-DD] <decision>: <why>`. Appended by agents and
|
||||
[2026-08-13] Kontantmetoden year-end VAT supersedes the 2026-08-06 VAT-reporting premise: BAS 2618/2628/2638 and 2648 feed the final declaration, reverse-charge purchases include both VAT sides and their basis, and only the mechanical day-one reversal is excluded from later VAT periods. Skatteverket requires unpaid invoice VAT in the final period and warns against reporting it twice after year end.
|
||||
[2026-08-13] Per-account VAT treatment is class-aware and explicit values override the static BAS mapping; SIE #SRU and #KTYP never supply it because they encode tax-return fields and account class, not momsdeklaration treatment.
|
||||
[2026-08-13] Per-account VAT treatment is class-aware; explicit values extend custom accounts while canonical accounts keep their static BAS momsdeklaration mapping. SIE #SRU and #KTYP never supply it because they encode tax-return fields and account class, not momsdeklaration treatment. VMB carries no default account rate because its VAT base is the margin, not gross sales.
|
||||
[2026-08-13] AGI needs two SKV scopes, not one: added `agdredovisningperiod` (hanteraredovisningsperiod: kvittenser/las) alongside `agd` (inlamning) in the per-flow DEFAULT_SCOPES, after a real prod filing signed fine and then 403'd on "Hamta kvittens". Pinned with a scope-set regression test since this is the third scope-cleanup casualty; the AGIPanel missing-scope banner now checks both, because a token with `agd` alone fails only at the last step.
|
||||
[2026-08-14] Server-side Supabase clients now go through createServiceRoleClient() (lib/supabase/service-client.ts) instead of importing supabase-js's createClient directly. autoRefreshToken defaults to true and auth-js starts the 30 s refresh ticker unconditionally off-browser; the ticker calls unref(), so the process still exits and nothing fails in CI or on Vercel, where the process is short-lived. But unref() does not make a timer collectable: it stays a GC root for its callback and retains the GoTrueClient, the SupabaseClient and the entire request scope captured around it (socket, IncomingMessage, ServerResponse, headers, route context; roughly 100 kB per client). A self-hosted instance died of heap exhaustion after 42 h, the last 24 completely idle, with a heap snapshot showing 445 retained request graphs and ~1050 Timeouts in the 30 000 ms bucket retained via autoRefreshTicker; the rate matched traffic exactly (healthcheck every 30 s plus the per-minute webhook dispatch cron = 3 clients/min x 148 min = 444). 22 call sites were migrated; only app/api/settings/booking-templates/sync/cron/route.ts had ever passed the options. SERVER_AUTH_OPTIONS is spread LAST inside the helper so a caller passing its own auth block cannot re-enable the ticker, and check 9 in scripts/checks/no-new-antipatterns.mjs fails CI on any new value import of createClient (type-only imports are fine). Browser clients are deliberately untouched: a signed-in tab genuinely needs the refresh, and lib/supabase/client.ts uses @supabase/ssr's createBrowserClient anyway.
|
||||
[2026-08-13] Correct the shipped VAT-treatment constraint in a new migration: the predecessor PR reached main while the replacement review was active, so the immutable original migration stays byte-identical and the class-aware vocabulary is enforced additively with NOT VALID plus validation.
|
||||
[2026-08-13] Issue #1457 supersedes the stale canonical-account exception above: an explicit per-account VAT treatment always overrides BAS fallback, while a reverse-charge treatment derives the editable 25 percent standard rate only when no booking default exists; class 5-6 SIE accounts require review when a treatment is actually suggested, not for every ordinary expense account.
|
||||
[2026-08-13] AGI needs two SKV scopes, not one: added `agdredovisningperiod` (hanteraredovisningsperiod: kvittenser/las) alongside `agd` (inlamning) in the per-flow DEFAULT_SCOPES, after a real prod filing signed fine and then 403'd on "Hamta kvittens". Pinned with a scope-set regression test since this is the third scope-cleanup casualty; the AGIPanel missing-scope banner now checks both, because a token with `agd` alone fails only at the last step.
|
||||
|
||||
Reference in New Issue
Block a user