* refactor(middleware): rename withPostgres to withPostgresClient and harden it
Renames the export to sit alongside withSupabaseClient /
withSupabaseAdminClient, and extracts the pool into a shared core module so
the service-role companion can reuse it. Safe to rename now: the old name
exists only on 1.5.0-rc.* / beta, never on a stable release.
Three correctness fixes alongside it:
- The pool cache was keyed on nothing, so a second connectionString in the
same process silently queried the first database. Now keyed per string.
- The missing-connection-string 500 returned { error }, not the package's
standard { message, code }.
- An unguarded rollback in the catch could replace the caller's real error
with a connection error.
Adds unit coverage for each, plus a type-level check that composing without
an upstream jwtClaims stays a compile-time error.
* feat(middleware): add withPostgresAdminClient
Contributes ctx.postgresAdmin — a pg client that bypasses RLS, exported from
./middleware/postgres-admin. Queries run as-is under the connection-string
role: no claim injection, no role switch, no wrapping transaction.
Declares no upstream prerequisite, so unlike withPostgresClient it composes
under auth: 'secret' and auth: 'none'. Shares the pool cache with the scoped
half — same connection string, one pool. That is safe because everything the
scoped half sets is transaction-local, so a connection always returns clean.
Kept as a second middleware rather than a property on ctx.postgres:
defineMiddleware contributes exactly one ctx key, and the split keeps the RLS
bypass visible at the composition site.
* test(e2e): cover both postgres middleware against a real database
Adds /my-notes-pg and /all-notes-pg to the core Node app and the Deno edge
function, both running the identical unfiltered SELECT — one through
ctx.postgres, one through ctx.postgresAdmin. user2 sees none of user1's rows
through the scoped client and sees them through the admin one, which proves
claim injection, the role drop, and the bypass in a single contrast.
The edge function passes connectionString explicitly from E2E_DB_URL: the CLI
injects a SUPABASE_DB_URL addressing the database by container name, and
Deno's DNS resolver rejects the underscores in it. The Node app still covers
the SUPABASE_DB_URL default path.
* docs: document the postgres middleware pair
Adds docs/postgres.md covering both halves, the SQL each query runs, the two
composition paths, table grants, the RLS bypass and why it is a separate
middleware, and guidance to write policies with the auth.* helpers rather
than reading request.jwt.claim.* directly.
Wires both subpaths into typedoc entryPoints — without which neither export
reached api-docs/ — and adds README sections, Exports and env-var rows, and
api-reference entries.
* fix(middleware): discard the connection when a rollback fails
pg-pool only removes a client when release() is given a truthy argument, so
the previous release() returned a connection whose transaction could not be
unwound straight back to the pool — potentially still inside the caller's
transaction with their role set.
That was survivable while the pool served one middleware. It is not now that
withPostgresAdminClient shares it: that middleware begins no transaction and
sets up no session state, so it would silently inherit the leftover role on
the next checkout.
* fix(middleware): refuse unsupported roles instead of downgrading to anon
withPostgresClient silently mapped every role that was not 'authenticated'
to 'anon'. For a forged service_role that was the intent, but Supabase also
supports custom roles via the role claim, and RLS applies to those normally —
so a legitimate `role: manager` token was being answered with zero rows and
no indication that the role was the reason.
Now only 'authenticated' and 'anon' are assumed, and anything else
short-circuits with a 500 and code UNSUPPORTED_ROLE before the handler runs
or a connection is checked out. service_role gets a message pointing at
withPostgresAdminClient; other roles are named in the error.
Custom roles remain unsupported — the reason is that PostgREST connects as the
unprivileged authenticator, where `grant <role> to authenticator` is itself the
authorization, while we connect as postgres and have no such boundary to lean
on. Documented, and tracked separately.
Also hoists the per-request claims serialization out of the per-query path.
* docs: list every subpath in the README exports table
The table covered 8 of 13 entry points. Adding the postgres pair made the
omission look deliberate rather than incidental — a reader could reasonably
conclude withClaims has no subpath, which matters because it is the documented
prerequisite for composing withPostgresClient standalone.
* feat(middleware): make query a tagged template, add queryRaw and ident
`query` now takes a tagged template only, so every interpolation becomes a
bind parameter and can never alter the shape of the statement. `queryRaw(text,
params)` keeps the string form — it is fully safe with params, and it is the
only path that works for query builders and codegen emitting `{ sql,
parameters }`, or for SQL that has to interpolate an identifier.
Passing a plain string to `query` throws, naming `queryRaw`. The two calls
differ only in their brackets, so refusing beats reinterpreting: the string's
first character would otherwise be read as the whole template and a
one-character query would be sent.
`ident()` quotes identifiers, which can never be bind parameters — `select $1
from notes` selects a literal, not a column. It is implemented directly rather
than wrapping `pg.escapeIdentifier`: that top-level export only exists from
pg 8.11, while the peer range is `^8.0.0`, so a wrapper would be a runtime
TypeError on 8.0-8.5. It also rejects empty names and NUL bytes, which pg
passes straight through to a confusing server-side error.
`set local role` now quotes the role via `ident()`. The role is already
constrained to the SUPPORTED_ROLES allowlist, so this changes nothing today —
it keeps the interpolation safe if that list widens to the custom roles the
docstring promises.
Follows the prior art: Prisma shipped the dual overload and reversed it,
Slonik refuses plain strings outright, and postgres.js requires the tag with
`sql.unsafe` as the named escape hatch.
The e2e edge function built its query by interpolating a column list. As a
`query` tag that would have compiled to `select $1 from notes` and returned
the literal string for every row — valid SQL, wrong rows, no error. It now
uses `queryRaw`, with a comment explaining why.
* fix: refuse non-string role claims instead of downgrading to anon
* chore: keep prettier off the release-please changelog
---------
Co-authored-by: Katerina Skroumpelou <sk.katherine@gmail.com>
* feat: add plugins option to withSupabase
* fix: add plugins?: never to overload 1 to force correct overload resolution
TypeScript doesn't apply excess property checking during overload resolution,
so calls with plugins: [...] were silently matching overload 1 and typing ctx
as SupabaseContext<unknown>. Adding plugins?: never to overload 1's config
makes it definitively fail when plugins is present, falling through to the
correct overload.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* refactor: rename plugins option to middleware on withSupabase
The array holds middleware entries (per-request behavior from
defineMiddleware) — the word 'plugins' is reserved for the package-level
concept whose client namespace goes in createClient({ plugins }). One
word per concept: server-side composition is 'middleware', client-side
namespaces are 'plugins', a Plugin is the package that ships both.
PluginsCtx -> MiddlewareCtx; overload trick unchanged
(middleware?: never on overload 1).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* feat(middleware): add postgres and claims middleware entrypoints
Graduate withPostgres and withClaims out of plugin-examples into
@supabase/server/middleware/*, so the PRFAQ's built-in middleware ship
from the package instead of example-local code (SDK-1163 item 5).
- withPostgres reads claims from ctx.jwtClaims (already populated by
withSupabase), so `middleware: [withPostgres()]` works with no separate
withClaims. Keeps the RLS role-clamp and tx-local request.jwt.claims
injection. pg is an optional peer dep (Node/Deno only, not Workers).
- withClaims ships for the standalone agnostic pipeline() case
(demo-only: no signature verification).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(middleware): surface table-grants hint on 42501 in withPostgres
Append the caller-role grants hint to permission-denied errors and document
the grants requirement in the withPostgres JSDoc.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* refactor: adopt @supabase/middleware importable getEnv API
Port from the @supabase/web-middleware PR-9 preview to
@supabase/middleware at main (0641674), which dropped ctx._runtime:
- withSupabase seeds the middleware chain via seedContext() instead of
faking a { _runtime } facet (the engine now marks contexts with a
symbol, so the structural fake no longer works)
- withPostgres defaults its connection string from the importable
getEnv('SUPABASE_DB_URL') instead of ctx._runtime.getEnv
- tests use vi.stubEnv for the env fallback; withClaims tests call the
handler as a bare fetch entry
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix: add explicit Middleware types to withClaims/withPostgres exports
JSR's slow-types check requires explicit types on public API symbols;
the inferred defineMiddleware return type failed 'Verify JSR packaging'.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* chore: rename allowBuilds key to @supabase/middleware
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* chore: adopt @supabase/middleware 0.3.0 from npm
Replaces the pkg.pr.new preview build with the released package.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* feat(middleware): verify withClaims against the project JWKS
withClaims graduates from the demo-grade payload decoder to real
verification: the user-mode JWT leg of verifyCredentials moves into a
shared verifyUserJwt core (JWKS resolver caching, HS256 shared-secret
path, sb_* passthrough), used by both. No decode-only mode remains — an
invalid token short-circuits 401, a missing JWKS 500.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* feat(middleware): add client and admin-client entrypoints
withSupabaseClient contributes ctx.supabase (RLS-scoped, caller's
token) and withSupabaseAdminClient contributes ctx.supabaseAdmin,
wrapping the existing createContextClient / createAdminClient
primitives. Composed under withSupabase they read the seeded
authMode / authKeyName to mirror verified credentials exactly;
standalone they work as plain engine entries.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* refactor: compose withSupabase on the engine clients
withSupabase now runs on @supabase/middleware for every request: the
two public client middleware fold around the user's middleware array
and handler, seeded with the verified auth identity via seedContext.
The host's second fetch argument (Workers env) is forwarded so
bindings reach getEnv. Public API, ctx keys, and error shapes are
unchanged — client-construction failures keep their historical JSON
responses (phase-guarded so handler throws still propagate), and the
existing test suite passes unmodified as the parity proof.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* test: withClaims checking with different apikeys
* test: withSupabase checking middleware ctx is reflected at runtime
* refactor: keep same pattern while extracting test body
* test: withSupabase checking middleware array order impacts ctx dependency
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: Kalleby Santos <kalleby_santos@hotmail.com>
* fix: apply Injectable programmatically so NestJS adapter loads in raw Node
The @Injectable() decorator on the guard in withSupabase() shipped
untranspiled (tsdown/oxc does not lower legacy decorators), crashing
`require`/`import` of the adapter with SyntaxError under plain Node.
Apply it as Injectable()(SupabaseAuthGuard) instead, and add a raw-Node
load smoke test (pnpm smoke) in CI to catch this class of regression.
Fixes#87
* fix: fail smoke test when no entrypoints are found in exports
* test: add E2E tests for all four adapters against a local Supabase stack
Adds an e2e vitest project (SDK-1143) covering what the mocked unit tests
cannot: real GoTrue-issued JWTs verified against the live JWKS endpoint,
real Supabase client operations via supabaseAdmin, resolveEnv() reading
process.env, and imports from dist/ so packaging regressions fail here.
One scenario set (auth + data access + isolation) runs over real HTTP
against minimal Hono, H3, Elysia, and NestJS apps. Elysia runs behind a
node:http server (srvx) so CI needs no Bun. A separate E2E workflow
starts the local stack with the Supabase CLI, builds, and runs the suite.
* test: grant explicit table privileges in the e2e notes migration
Newer Supabase stacks make new tables private by default — the API roles
(anon/authenticated/service_role) no longer receive DML grants on table
creation. CI installs the latest CLI, so all supabaseAdmin queries failed
with "permission denied for table notes" while JWT scenarios passed.
Reproduced locally on CLI 2.109.1; explicit grants fix it on both old and
new stacks.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* test: align NestJS missing-body handling and cover it in the scenarios
The NestJS app silently inserted an empty note when the body was missing,
while the other three adapters returned 400 — and no scenario exercised
those 400 branches. NestJS now throws BadRequestException like the rest,
and a shared missing-body scenario keeps all four aligned.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* test: cover forged JWTs, the RLS-scoped client, and optional-auth rejection
Closes the three gaps from PR review: the garbage-token scenario failed at
header decode without ever reaching signature verification, ctx.supabase
(the RLS-scoped client) was never exercised, and nothing pinned that a
present-but-invalid token on an optional route is rejected rather than
downgraded to anonymous.
- Mint a well-formed JWT with the live JWKS kid but a wrong signing key in
global setup; every adapter must 401 it — proving signature verification
end-to-end, not just structure checks.
- Add GET /my-notes reading through ctx.supabase with no WHERE clause,
backed by a user_id = auth.uid() select policy — proving the caller's
token reaches PostgREST and Postgres RLS scopes the rows.
- Assert GET /me-optional with an invalid token → 401.
10 → 14 scenarios per adapter (56 tests).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* test: add core-wrapper app, admin-bypass proof, and sign-in readability
Addresses PR review comments:
- New fifth app on the core withSupabase(config, handler) fetch wrapper —
the exact programming model Supabase Edge Functions deploy — running the
full scenario set behind node:http. A real Deno runtime e2e via
`supabase functions serve` is tracked in SDK-1280.
- New GET /all-notes route (admin client, no filter) + scenario: user2's
request sees user1's rows through supabaseAdmin, directly proving the
admin client is not scoped to the caller's identity.
- Replace the `;({ data, error } = ...)` destructuring-reassignment in the
sign-in helper with a plain result variable.
15 scenarios × 5 apps (75 tests).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* chore(types): export common set of types for supabase server
* fix: moving peer dep type re-export to its own module path
* docs: docummenting peer deps types
* fix: creating export entry
* fix: not renaming 'AuthError'
* docs(types): improving 'supabase-js' peer description
* fix: regression, exporting 'AuthError'
---------
Co-authored-by: Kalleby Santos <kalleby_santos@hotmail.com>
* feat: add NestJS adapter
Ships `@supabase/server/adapters/nestjs`:
- `withSupabase(opts)` — class guard for `@UseGuards()` and
`useGlobalGuards()`, supporting Express and Fastify
- `@SupabaseCtx(key?, ...pipes)` — param decorator returning the full
SupabaseContext or a single field, with NestJS pipes applied to the
extracted value
- 401s thrown as `HttpException` with `{ message, code }`; the
underlying `AuthError` is exposed on `cause`
Adds `@nestjs/common` as an optional peer dep (`^10 || ^11`), wires the
new export in package.json / jsr.json / tsdown.config.ts, and enables
`experimentalDecorators` + `emitDecoratorMetadata` in tsconfig. Test
setup uses unplugin-swc via vitest.config.ts so integration tests can
boot a real Nest app on both Express and Fastify.
Docs: README quickstart + docs/adapters/nestjs.md.
* refactor(nestjs): address PR review feedback
- Scope `experimentalDecorators` + `emitDecoratorMetadata` to
`src/adapters/nestjs/tsconfig.json` (extends root) and exclude the
adapter from the root project so the options aren't enforced
repo-wide. `typecheck` now runs both projects.
- Convert `vitest.config.ts` to the `projects` syntax so the
`unplugin-swc` transform applies only to the nestjs project; the unit
project runs unchanged with esbuild.
- Throw `HttpException` (500, `unsupported_context`) instead of
returning true on non-HTTP execution contexts so misuse fails loudly
on the first request rather than silently no-op'ing on every
RPC/WebSocket message.
- Remove the "skip if context already set" branch so handler-level
guards can tighten what a global guard set. Previously the outer
(global) guard always won under Nest's global → controller → handler
order, so a stricter handler-level guard could be silently bypassed.
Tests updated; `@SupabaseCtx` decorator unchanged.
- Drop unused `CanActivate` import from integration.test.ts.
* docs(nestjs): update guard behavior section to match new semantics
The guard no longer skips when a prior context exists — it always
re-evaluates. Rewrite the doc section to match the JSDoc wording and
the inner-rejects/inner-overwrites tests.
Webhook signature verification varies too much across providers
to justify a generic helper. Each case should be handled with
the provider's own SDK (e.g., Stripe, GitHub).
* docs: add initial documentation and skills.md
* docs: apply formatting
* docs: update SKILL.md to resolve docs from package location and ship docs with npm
SKILL.md now instructs agents to find documentation in the installed
@supabase/server package (node_modules or repo root) instead of using
relative paths. Added docs/ and SKILL.md to package.json files array
so they ship with npm installs.
* docs: add missing HTTPException import in error-handling example
* docs: fix strictNullChecks issues, duplicate variables, and missing context in examples
- Add non-null assertions (!) after error guards where TS can't narrow
destructured result tuples
- Split duplicate variable declarations into separate code blocks
- Add missing imports and show where variables like `auth` come from
- Keep { data, error } destructuring pattern consistent with SDK convention
* docs: reframe as runtime-agnostic and add env auto-injection details
- getting-started: replace Edge Function framing with runtime-neutral
language, explain module worker pattern works across Deno/Bun/Workers,
add Runtimes section covering all supported environments
- webhooks: replace Deno.env with process.env for portable examples
- environment-variables: add "Auto-injected in" column distinguishing
Platform vs Local CLI, reframe section headers
- auth-modes: clean up example key values
- core-primitives: clarify "Integration with frameworks" wording
- types: simplify TSDoc for publishable/secret key descriptions
* docs: add SSR frameworks guide and update references
Add docs/ssr-frameworks.md covering the pattern for using core
primitives in Next.js, SvelteKit, Nuxt, and Remix — cookie extraction,
env bridging, JWKS caching, and a complete Next.js adapter example.
Replace the basic SSR example in core-primitives.md with a pointer
to the new dedicated doc. Add SSR row to SKILL.md routing table.
* docs: add disclaimer of new package
* docs: extend explanation on keys env vars
* docs: add platform-specific quick starts to SKILL.md
Split the single generic example into per-platform sections
(Edge Functions, Cloudflare Workers, Hono, SSR Frameworks) so
AI agents pick the correct import specifier for each runtime.
Adds npm: prefix to all Deno examples and a Deno column to the
entry points table. Also adds createSupabaseContext examples.
* docs: add server-to-server quick starts and allow:always guardrails
Add secret key auth and webhook signature verification quick starts
to SKILL.md. Add explicit decision tree for allow:'always' so AI
agents confirm with the user before leaving endpoints unprotected.
* docs: add legacy keys warning, skills install, remove webhook docs
- Add legacy keys warning to SKILL.md (avoid anon/service_role keys)
- Add AI coding skills install section to README
- Add server-to-server quick start with caller code to README
- Add runtimes, documentation table, and named secret keys to README
- Remove verifyWebhookSignature references from all docs
- Delete docs/webhooks.md (code removal in separate PR)
* docs: add verify_jwt = false note for non-user auth modes
Edge Functions require verify_jwt = false in config.toml when
using allow: public, secret, or always — otherwise the platform
rejects requests before the handler runs.
* docs: add edge function recipes and refactor env vars doc
Add recipes for function-to-function calls, pg_net from database,
Stripe webhooks, and generic webhook signature verification.
Document the @supabase/server/wrappers entry point.
Refactor environment-variables.md into Supabase vs non-Supabase sections.
* docs: add security doc covering timing-safe comparison, auth model, CORS
* docs: link auth-modes timing-safe mentions to security.md
* docs: adding 'local cli' to secrets table
This envs will be injected from cli too
* docs: setting Deno as first installation choice
* docs: adding 'verify_jwt=false' disclaimer for non-user auth
* docs: split Deno/Supabase runtime section, merge Deno/Node/Bun
* docs(skills): adding legacy code migration example
* docs(skills): explaining why legacy code should be migrated
* docs: rewrite migration section, improve skill description triggers
---------
Co-authored-by: Kalleby Santos <kalleby_santos@hotmail.com>