Motivation
Surfaced while doing the contains()→matches() rename (#617). The Supabase wrapper (EncryptedQueryBuilderImpl in packages/stack-supabase/src/query-builder.ts) is not a facade today — it is a deferred builder that hand-re-implements a curated set of ~28 postgrest-js methods (eq, neq, gt, gte, lt, lte, like, ilike, contains, in, is, filter, not, or, match, order, limit, range, select, insert, update, upsert, delete, single, maybeSingle, csv, abortSignal, throwOnError). Each records an operation; execute() replays it onto the real supabase query builder, encrypting operands for encrypted columns.
There is no Proxy and no catch-all delegation. Consequences:
- Any postgrest method we didn't re-declare is unreachable through the wrapper —
overlaps, textSearch/fts, rangeGt/rangeLt/rangeGte/rangeLte/rangeAdjacent, containedBy, likeAllOf/likeAnyOf, explain, etc. This affects plaintext columns too, not just encrypted ones: a dev can't do a native operation on a plaintext column if we happened not to hand-write it.
- We lose supabase-js's own generated-type checking on the methods/columns we didn't re-implement — the wrapper's types are only as complete as what we hand-wrote.
- Every new postgrest method upstream adds is invisible until someone hand-ports it.
Proposed change
Turn the wrapper into a true delegating proxy facade:
- Delegate the entire postgrest surface by default (e.g. via a
Proxy, or an explicit passthrough layer) so any method/column supabase-js supports works on plaintext columns unchanged, with supabase-js's own typings preserved.
- Hand-write only the encrypted-specific behaviour: the EQL-aware overloads of the operators that must encrypt operands or decrypt results (
eq/neq/gt/…/in/order/select/insert/update/matches), plus the guards that reject nonsensical operations on encrypted columns.
- Keep the typed gradient: declared
schemas → per-column compile-time types; undeclared → untyped passthrough with runtime guards.
Testing requirement
Each proxied/delegated method should ideally have a test confirming it reaches the underlying supabase client correctly (operands passed through untouched for plaintext columns; encrypted columns still encrypt/decrypt). A proxy that silently forwards is easy to get subtly wrong (argument shape, this binding, chainability, the deferred-then contract), and the current hand-written surface at least has per-method coverage — the facade must not regress that. Enumerate the delegated surface and assert each member is callable and forwards, so a future supabase-js change or a proxy bug is caught rather than shipped.
Risks / notes
- The deferred-execution model (record now, replay + encrypt at
await) must be preserved through the proxy — encryption happens at emit, not at call time.
Proxy + the thenable/PromiseLike contract needs care (don't accidentally intercept then/catch/finally).
- Chainability (
this-returning methods) and the lock-context/audit chain must survive delegation.
Follow-up to #617.
Motivation
Surfaced while doing the
contains()→matches()rename (#617). The Supabase wrapper (EncryptedQueryBuilderImplinpackages/stack-supabase/src/query-builder.ts) is not a facade today — it is a deferred builder that hand-re-implements a curated set of ~28 postgrest-js methods (eq,neq,gt,gte,lt,lte,like,ilike,contains,in,is,filter,not,or,match,order,limit,range,select,insert,update,upsert,delete,single,maybeSingle,csv,abortSignal,throwOnError). Each records an operation;execute()replays it onto the real supabase query builder, encrypting operands for encrypted columns.There is no
Proxyand no catch-all delegation. Consequences:overlaps,textSearch/fts,rangeGt/rangeLt/rangeGte/rangeLte/rangeAdjacent,containedBy,likeAllOf/likeAnyOf,explain, etc. This affects plaintext columns too, not just encrypted ones: a dev can't do a native operation on a plaintext column if we happened not to hand-write it.Proposed change
Turn the wrapper into a true delegating proxy facade:
Proxy, or an explicit passthrough layer) so any method/column supabase-js supports works on plaintext columns unchanged, with supabase-js's own typings preserved.eq/neq/gt/…/in/order/select/insert/update/matches), plus the guards that reject nonsensical operations on encrypted columns.schemas→ per-column compile-time types; undeclared → untyped passthrough with runtime guards.Testing requirement
Each proxied/delegated method should ideally have a test confirming it reaches the underlying supabase client correctly (operands passed through untouched for plaintext columns; encrypted columns still encrypt/decrypt). A proxy that silently forwards is easy to get subtly wrong (argument shape,
thisbinding, chainability, the deferred-then contract), and the current hand-written surface at least has per-method coverage — the facade must not regress that. Enumerate the delegated surface and assert each member is callable and forwards, so a future supabase-js change or a proxy bug is caught rather than shipped.Risks / notes
await) must be preserved through the proxy — encryption happens at emit, not at call time.Proxy+ the thenable/PromiseLikecontract needs care (don't accidentally interceptthen/catch/finally).this-returning methods) and the lock-context/audit chain must survive delegation.Follow-up to #617.