Area: sdk — DX · follow-up to #456 / #269
#456 adds HTTP customization at the client level: options.fetch, options.headers (static), and options.fetchOptions. This issue covers the per-call half, plus dynamic header callbacks, both deliberately deferred to keep #456 reviewable.
Scope
Why this is cheap
The per-call slot already exists — .fetch(opts?: RequestOptions) on QueryBuilder (×2), TableRef, and PipeRef. Adding optional fields to that interface is purely additive; nothing breaks.
The expensive part ships in #456 regardless: case-insensitive header merging and the precedence rules. #456 keeps that in a single helper called from request(), so the work here is threading three optional fields to one existing merge site and testing precedence.
Per-call fetch is the smallest of the three: request() reads ctx.options.fetch today; per-call makes it opts.fetch ?? ctx.options.fetch.
Precedence
#456 establishes, lowest to highest: client headers → auth → SDK-computed (Content-Type, Accept). Per-call headers slot in at the top — an explicit per-call header is the caller overriding deliberately. That ordering needs tests here, not just documentation; both known bugs in this area at Supabase were merge bugs, not design bugs (#2207 — a global Content-Type got joined with an upload's own, producing application/json, image/png and 415s; #1043 — a case-sensitive check against case-insensitive headers).
Dependency on #458
The dynamic-header callback raises exactly the question #458 asks about auth: it's invoked per request with no caching or in-flight dedupe. Both should be answered together rather than each growing its own convention — settle #458 first, then apply the same contract to header callbacks.
Static headers ship in #456 because they cover the motivating case: CF-Access service tokens are long-lived ID/secret pairs from env vars, not per-request-minted credentials.
Known ergonomic wrinkle
The query builder is PromiseLike — then() calls .fetch() with no arguments — so per-call options require the explicit terminal form:
await wh.from('clicks').select('*'); // no per-call options possible
await wh.from('clicks').select('*').fetch({ headers }); // per-call options
Already true for signal today, so not a regression. Making await carry options would need a chainable .with({ … }), which is a larger API decision and explicitly not proposed here.
Not in scope
Streaming — no per-call or client-level header reaches SSE while the transport is EventSource. Tracked in #203.
— Filed by Claude Opus 5, via Claude Code
Area: sdk — DX · follow-up to #456 / #269
#456 adds HTTP customization at the client level:
options.fetch,options.headers(static), andoptions.fetchOptions. This issue covers the per-call half, plus dynamic header callbacks, both deliberately deferred to keep #456 reviewable.Scope
headers— override or add headers for one queryfetchOptions— e.g. Next.jsnext: { tags }cache control on a specific query (supabase-js#917 is people reaching for a whole customfetchjust for this)fetch— the framework-scoped case: SvelteKit'sload()hands you a request-scopedfetchwith cookie access that can hit internal endpoints without a real HTTP round trip. Open at Supabase since 2022 (supabase-js#438, #889)() => Record<string,string> | Promise<Record<string,string>>alongside the static form, for rotating credentialsWhy this is cheap
The per-call slot already exists —
.fetch(opts?: RequestOptions)onQueryBuilder(×2),TableRef, andPipeRef. Adding optional fields to that interface is purely additive; nothing breaks.The expensive part ships in #456 regardless: case-insensitive header merging and the precedence rules. #456 keeps that in a single helper called from
request(), so the work here is threading three optional fields to one existing merge site and testing precedence.Per-call
fetchis the smallest of the three:request()readsctx.options.fetchtoday; per-call makes itopts.fetch ?? ctx.options.fetch.Precedence
#456 establishes, lowest to highest: client
headers→auth→ SDK-computed (Content-Type,Accept). Per-call headers slot in at the top — an explicit per-call header is the caller overriding deliberately. That ordering needs tests here, not just documentation; both known bugs in this area at Supabase were merge bugs, not design bugs (#2207 — a globalContent-Typegot joined with an upload's own, producingapplication/json, image/pngand 415s; #1043 — a case-sensitive check against case-insensitive headers).Dependency on #458
The dynamic-header callback raises exactly the question #458 asks about
auth: it's invoked per request with no caching or in-flight dedupe. Both should be answered together rather than each growing its own convention — settle #458 first, then apply the same contract to header callbacks.Static headers ship in #456 because they cover the motivating case: CF-Access service tokens are long-lived ID/secret pairs from env vars, not per-request-minted credentials.
Known ergonomic wrinkle
The query builder is
PromiseLike—then()calls.fetch()with no arguments — so per-call options require the explicit terminal form:Already true for
signaltoday, so not a regression. Makingawaitcarry options would need a chainable.with({ … }), which is a larger API decision and explicitly not proposed here.Not in scope
Streaming — no per-call or client-level header reaches SSE while the transport is
EventSource. Tracked in #203.— Filed by Claude Opus 5, via Claude Code