136 Commits

Author SHA1 Message Date
Katerina Skroumpelou bf1ca09b59 fix: name the key a bare secret/publishable mode rejected (#155) server-v1.6.1-rc.143 2026-09-11 19:11:10 +03:00
Raúl Barroso 06da558971 docs: add MCP server guide and surface the OAuth entry point in the skill (#154)
* docs: add MCP server guide and surface the OAuth entry point in the skill

docs/mcp.md explains the pipeline form for MCP servers
(withOAuthProtectedResource before the withSupabase gate), how the
public URLs are derived on Edge Functions and configured elsewhere, the
Supabase Auth prerequisites, the inline-handler inference gotcha, and the
stateless limits.

The agent skill gains the @supabase/server/oauth-protected-resource entry
point and a docs-table row pointing at the new page.

docs(mcp): drop the private sandbox link, match the guide title

docs(mcp): address review on the MCP guide

- CLI 2.117.0 injects SUPABASE_FUNCTION_SLUG only; SUPABASE_PUBLIC_URL is
  honored when set but not set by the CLI.
- The asymmetric-key requirement comes from withSupabase verifying user
  JWTs against the JWKS, not from OAuth 2.1.
- Document OAuthProtectedResourceConfig, UrlOption, fromSupabaseUrl and the
  escape hatches in api-reference.md, and link there.
- Show withSupabase<Database> and a writing tool, the onerror hook for
  factory errors, the OPTIONS preflight row, the root-path-without-slug
  case, and that the one-shot limits belong to the handler, not the library.

* docs: list mcp.md in README and complete the metadata row

---------

Co-authored-by: Katerina Skroumpelou <sk.katherine@gmail.com>
2026-09-11 17:06:18 +02:00
supabase-releaser[bot] 0be895115a chore(main): release server 1.6.0 (#151)
Co-authored-by: supabase-releaser[bot] <223506987+supabase-releaser[bot]@users.noreply.github.com>
server-v1.6.0
2026-09-09 15:12:16 +03:00
Katerina Skroumpelou 11b02f37ab fix: recognize Supabase edge runtime via SB_EXECUTION_ID (#153)
* fix: recognize Supabase edge runtime via SB_EXECUTION_ID

* fix: report detected runtime in MISSING_RESOURCE_SERVER
server-v1.6.0-rc.140
2026-09-09 15:09:44 +03:00
dependabot[bot] b0c6fac08e chore(deps): bump the npm_and_yarn group across 1 directory with 2 updates (#152)
Bumps the npm_and_yarn group with 1 update in the / directory: [hono](https://github.com/honojs/hono).


Updates `hono` from 4.12.34 to 4.13.5
- [Release notes](https://github.com/honojs/hono/releases)
- [Commits](https://github.com/honojs/hono/compare/v4.12.34...v4.13.5)

Updates `@vitest/mocker` from 4.1.8 to 5.0.0
- [Release notes](https://github.com/vitest-dev/vitest/releases)
- [Changelog](https://github.com/vitest-dev/vitest/blob/main/docs/releases.md)
- [Commits](https://github.com/vitest-dev/vitest/commits/v5.0.0/packages/mocker)

---
updated-dependencies:
- dependency-name: hono
  dependency-version: 4.13.5
  dependency-type: direct:development
  dependency-group: npm_and_yarn
- dependency-name: "@vitest/mocker"
  dependency-version: 5.0.0
  dependency-type: indirect
  dependency-group: npm_and_yarn
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-09-09 14:12:07 +03:00
claude[bot] 8e2d1f0ecb docs: clarify that legacy API keys and HS256 JWTs are not supported (#150)
Add a short section to docs/auth-modes.md and a README FAQ entry stating
that old-format anon/service_role JWT-style API keys and legacy HS256
user JWTs are rejected, and that projects must migrate to the new
sb_publishable_*/sb_secret_* key format and JWT signing keys to use
this library.


Claude-Session: https://claude.ai/code/session_013Xa3WdzF2qAy1ndnvaGQVK

Co-authored-by: kallebysantos <kalleby_santos@hotmail.com>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-09 14:10:57 +03:00
Matt Johnston edfb411dd7 fix(types): only accept 'none' as the last entry of AuthConfig (#148)
* fix: only accept 'none' as the last auth mode

`auth` took `AuthModeWithKey[]`, which let `'none'` sit anywhere in the
list. It matches every request, so any mode after it is unreachable, and
a list of just `['none']` says nothing that a bare `auth: 'none'` doesn't
— both were accepted silently.

`AuthConfig` moves that rule into the type: a list is one or more
credentialed modes, optionally followed by `'none'`. The optional-user
form `['user', 'none']` is unaffected, which is what the e2e apps and the
docs use.

The deprecated `allow` keeps the looser element type so code mid-migration
still compiles, so `requiresCredentials` still scans the whole list rather
than checking the last entry.

* fix: name the unwrapped single mode in AuthConfig

AuthConfig reached the single-mode form through AuthModeWithKey, which
also carries 'none'. Splitting it into `'none' | CredentialedAuthMode`
makes the union say what it accepts — a bare mode, an ordered list, or
'none' — rather than leaving a reader to subtract 'none' from one member
and find it again in another.

The type is equivalent to what it replaced, so no call site moves; what
changes is that the unwrapped form is now stated, tested and documented
instead of inherited.
server-v1.6.0-rc.137
2026-09-09 14:09:13 +03:00
Katerina Skroumpelou 65f870565a feat: compose withSupabase as a pipeline entry (#145)
* feat: compose withSupabase as a pipeline entry

* fix: fixes after review

* fix: more fixes from review
server-v1.6.0-rc.136
2026-09-09 14:06:45 +03:00
Katerina Skroumpelou d6cd8a6627 chore: add workflow to block merging (#147) 2026-09-04 17:37:35 +01:00
Katerina Skroumpelou 68c51898cf chore: prettier-ignore jsr.json (#144) 2026-09-04 11:40:15 +01:00
supabase-releaser[bot] 9b68323928 chore(main): release server 1.5.3 (#143)
Co-authored-by: supabase-releaser[bot] <223506987+supabase-releaser[bot]@users.noreply.github.com>
server-v1.5.3
2026-09-03 17:56:04 +03:00
Katerina Skroumpelou 96243afa5c chore: release 1.5.3 (#142)
Release-As: 1.5.3
server-v1.5.3-rc.132
2026-09-03 17:53:09 +03:00
Katerina Skroumpelou f3274c7d14 docs: mark middleware and oauth surfaces alpha (#141) 2026-09-03 17:46:49 +03:00
Katerina Skroumpelou f0edea164e chore: install latest supabase-js (#140) 2026-09-02 16:49:39 +03:00
supabase-releaser[bot] f6ca6af904 chore(main): release server 1.5.2 (#137)
Co-authored-by: supabase-releaser[bot] <223506987+supabase-releaser[bot]@users.noreply.github.com>
server-v1.5.2
2026-09-01 18:40:52 +03:00
Katerina Skroumpelou c2b48723f5 docs: label the middleware option alpha (#138) 2026-09-01 18:39:52 +03:00
Katerina Skroumpelou c231b95e94 fix: attach error listeners to pooled pg connections (#136) server-v1.5.2-rc.127 2026-09-01 18:37:08 +03:00
supabase-releaser[bot] d08a6f5775 chore(main): release server 1.5.1 (#134)
Co-authored-by: supabase-releaser[bot] <223506987+supabase-releaser[bot]@users.noreply.github.com>
server-v1.5.1
2026-08-31 18:51:45 +03:00
Katerina Skroumpelou b9e38740e4 chore: rewind phantom 1.5.0 version files (#135)
Release-As: 1.5.1
server-v1.5.1-rc.125
2026-08-31 18:39:15 +03:00
Katerina Skroumpelou 0eed2d8a28 chore: force release 1.5.1 (#133)
Release-As: 1.5.1
server-v1.5.1-rc.124
2026-08-31 18:10:27 +03:00
supabase-releaser[bot] 6cb7d062ef chore(main): release server 1.5.0 (#121)
Co-authored-by: supabase-releaser[bot] <223506987+supabase-releaser[bot]@users.noreply.github.com>
2026-08-31 17:36:43 +03:00
Katerina Skroumpelou d60afcfa11 fix: require node >=22 to match @supabase/middleware (#132) server-v1.5.0-rc.122 2026-08-31 17:28:02 +03:00
Matt Johnston b8b086c021 feat(errors): add self-identifying errors with hints and diagnostics (#130)
* feat: specific, self-identifying errors with hints and diagnostics

Nearly every failure returned `{ message: "Invalid credentials", code:
"INVALID_CREDENTIALS" }` — naming neither the cause nor the library it
came from.

Provenance. All errors now share a `SupabaseServerError` base carrying
`source: "@supabase/server"`, a `[@supabase/server]` message prefix (the
convention `deprecation.ts` already used for warnings), a `docs` link to
the matching `docs/error-handling.md` section, an optional `hint`, and
non-sensitive `details`. `toJSON()` renders the wire payload and is picked
up by `JSON.stringify`, so logging no longer yields `{}`. One
`errorResponse()` helper renders it everywhere, repeating the code in an
`x-supabase-server-error` header and adding that to
`Access-Control-Expose-Headers` so cross-origin callers can read it.
Top-level `message` and `code` are unchanged, so existing consumers and
the adapters keep working.

Diagnosis. `verifyUserJwt` now returns *why* a token failed instead of
`null`, and the mode chain records why each mode fell through, so the
final error names the real cause: `MISSING_CREDENTIALS`,
`INVALID_API_KEY`, `INVALID_JWT`, plus `JWKS_NOT_CONFIGURED`,
`JWKS_FETCH_FAILED` and `NO_KEYS_CONFIGURED` for states where no request
could ever have succeeded. `INVALID_CREDENTIALS` stays exported as the
fallback. Hints cover the mistakes people actually make — a secret key
sent to a publishable-only endpoint, a legacy anon/service_role key, an
`Authorization` header without the `Bearer` scheme, a JWT with no `kid`,
an expired token, a JWKS from the wrong project.

The middleware that answer directly get the same treatment rather than
their own hand-rolled bodies: `withClaims` / `withRequiredClaims` report
`MISSING_JWKS`, `MISSING_CREDENTIALS`, `INVALID_JWT`;
`withPostgresClient` / `withPostgresAdminClient` report
`MISSING_CONNECTION_STRING` and a catalogued `UNSUPPORTED_ROLE`.

`details` never carries key values or token payloads: API keys are
reported by prefix format, named keys by name, JWTs by `alg`/`kid` only.

Note: server misconfiguration now surfaces as 500 rather than 401. A
missing or unreachable JWKS, or an auth mode no configured key can match,
are not the caller's fault.

* feat: add `errors: { detailed: false }` to trim the error response body

`hint`, `docs`, and `details` are written for whoever is building against
the endpoint, and not everyone wants them on the wire. `errors.detailed`
(default `true`) reduces the body to `code` and `message` alone.

Provenance survives the trim: `message` keeps its `[@supabase/server]`
prefix, and the code is still sent as the `x-supabase-server-error`
header — so the error stays identifiable without the `source` field.

Response-only. The HTTP status is unaffected and the error object keeps
`hint`, `docs`, and `details` in full, so `createSupabaseContext` callers
and the framework adapters see everything.

Documented as a verbosity control rather than a security boundary — `code`
and `message` still name the failure specifically. Formatting the response
by hand via `createSupabaseContext` remains the way to disclose nothing.

* feat: distinguish UNUSABLE_CREDENTIAL from MISSING_CREDENTIALS

Review feedback on #130: the top-level code was `MISSING_CREDENTIALS`
even when a credential had arrived, just the wrong kind. `received.
authorization: 'api-key'` and the hint carried the diagnosis, but
`errors: { detailed: false }` strips both — leaving a caller who is
demonstrably sending a key staring at a bare `MISSING_CREDENTIALS`.

That mode makes the code the only thing a caller can rely on, so it has
to be true standing alone. `UNUSABLE_CREDENTIAL` (401) now covers "a
credential arrived that no accepted mode can use", partitioning the space
exactly against `MISSING_CREDENTIALS` ("nothing arrived"). It has two
shapes, named in the `message` so the diagnosis survives the trim:

  - wrong kind: an `sb_*` API key in the Authorization header
  - unreadable: wrong scheme, wrong casing, bare value, empty token

The unreadable shapes had the same defect and are fixed with it — a
`Basic` or lowercase-`bearer` header is not a missing credential either.

Classification moves into one shared `diagnoseAuthorizationHeader`, since
only the raw header separates "sent nothing" from "sent something
unreadable" and both `verifyAuth` and the `withRequiredClaims` gate need
that distinction. Previously the gate could not make it at all, so the
two disagreed on every scheme case. A parity matrix over all six header
shapes now pins gate and `withSupabase({ auth: 'user' })` to the same
status and code.

* fix: preserve error cause when client creation fails

* fix: report API keys as UNUSABLE_CREDENTIAL on user-only endpoints

Review feedback on #130: `supabase-js` sends the publishable key in both
the `apikey` and `Authorization` headers, so an unauthenticated browser
call to an `auth: 'user'` endpoint arrives with a key in each slot. The
`apikey !== 'absent'` branch in `explainFallthrough` was read first, so
the caller got `INVALID_API_KEY` — "check you are pointing at the right
Supabase project" — for a key that was never going to be looked up. With
`errors: { detailed: false }` the code is all they get, and it sent them
hunting for a key mismatch that does not exist.

`INVALID_API_KEY` means "matched none of the configured keys", which only
says something when a mode was doing that lookup. It is now gated on an
attempted `publishable` / `secret` mode; where no mode reads keys, a key
in either header is `UNUSABLE_CREDENTIAL` — not wrong, just the wrong
kind of credential. The apikey-header-only case had the same defect and
is fixed with it: "matched no key configured for auth mode(s): "user""
described a lookup that never happened.

The new diagnosis is shared as `apiKeyOnUserOnlyEndpoint`, so the
`withRequiredClaims` gate stops answering with "API keys belong in the
`apikey` header" for callers who already sent it there — that gate only
ever accepts a user JWT, so moving the key would not help. It keeps the
parity the gate is built for: an identical request, worded identically
from both paths. `ApiKeyInAuthorizationHeader` still covers the case
where the advice is right — a mixed `['user', 'publishable']` endpoint
with a key in `Authorization` alone.

* docs: add MissingConnectionStringError documentation and clarify credential error handling
server-v1.5.0-rc.121
2026-08-31 17:16:38 +03:00
Katerina Skroumpelou 0c1a2b67e4 fix: harden oauth-protected-resource header and URL edge cases (#131) server-v1.5.0-rc.120 2026-08-28 18:26:46 +03:00
Katerina Skroumpelou 9c064c58df fix: report missing JWKS as ENV_ERROR in user-mode verification (#128)
* fix: report missing JWKS as ENV_ERROR in user-mode verification

* chore: small addition
server-v1.5.0-rc.119
2026-08-27 18:05:37 +03:00
Katerina Skroumpelou 05a95b6dcb feat: add withRequiredClaims user-mode auth gate (#127)
* feat: add withRequiredClaims user-mode auth gate

* chore: add export to readme

* fix: add required-claims jsr export, note cors and sole-entry recipe
server-v1.5.0-rc.118
2026-08-27 17:44:58 +03:00
Raúl Barroso 11cce29d1a feat: withOAuthProtectedResource url config (#117)
Make the middleware work off Supabase Edge Functions. Previously it
hardcoded the Edge Functions proxy shape into its URL derivation, which is
wrong anywhere else, and took no configuration to correct it.

Adds `resourceServer` and `authorizationServer`, each a fixed string or a
function of the request, plus a `fromSupabaseUrl` adapter. Edge Functions
stays zero-config; off it, an unset option throws rather than advertising a
request-derived guess. `authorizationServer` falls back to
`SUPABASE_PUBLIC_URL` then `SUPABASE_URL` first, both rungs below the Edge
derivation so an internal gateway host can never displace the origin the
client used.

Also uses `SUPABASE_FUNCTION_SLUG` for a canonical `/functions/v1/{slug}`,
fixes three header-parsing bugs (comma-joined value, double port,
case-sensitive proto), and makes the advertised `resource` always equal the
URL the client called, as RFC 9728 §3.3 requires.
server-v1.5.0-rc.117
2026-08-26 18:47:23 +02:00
Katerina Skroumpelou 15763696d4 feat: validate middleware prereqs against Supabase context (#126)
* feat: validate middleware prereqs against Supabase context

* chore: pnpm i
server-v1.5.0-rc.116
2026-08-26 13:00:17 +03:00
Katerina Skroumpelou 996825886b docs: state that withClaims is not an auth gate (#125) 2026-08-25 15:59:53 +03:00
Katerina Skroumpelou fbb652f55e fix: construct supabaseAdmin lazily on first access (#124) server-v1.5.0-rc.114 2026-08-25 15:50:28 +03:00
Katerina Skroumpelou ed0c2c33cf refactor: one shared contract for the upstream auth keys (#123) 2026-08-25 14:16:43 +03:00
Katerina Skroumpelou f05307ff4a fix: declare pg and keep it out of the root CJS bundle (#122) server-v1.5.0-rc.112 2026-08-25 11:22:02 +03:00
Katerina Skroumpelou d36fff239c chore: rewind release 1.5.0 (#120) server-v1.5.0-rc.111 2026-08-25 10:01:59 +02:00
supabase-releaser[bot] 05ab7b9280 chore(main): release server 1.5.0 (#114)
Co-authored-by: supabase-releaser[bot] <223506987+supabase-releaser[bot]@users.noreply.github.com>
2026-08-25 10:39:51 +03:00
Katerina Skroumpelou f393c6e725 feat: flow upstream ctx types through withSupabase nesting (#119) server-v1.5.0-rc.109 2026-08-25 10:35:26 +03:00
Raúl Barroso 04831cedcc fix: migrate withOAuthProtectedResource to defineMiddleware (#116)
* refactor: withOAuthProtectedResource

* fix: address PR feedback by @mandarini
server-v1.5.0-rc.108
2026-08-25 09:51:42 +03:00
Tomás Pozo 77656bae3b feat(middleware): ship withPostgresClient and withPostgresAdminClient (#115)
* 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>
server-v1.5.0-rc.107
2026-08-24 17:52:01 +03:00
Raúl Barroso 39085907d1 feat: add withOAuthProtectedResource (#113)
* feat: add oauth-protected-resource

* fix(oauth-protected-resource): forward platform arg, wire exports

* fix(oauth-protected-resource): metadata CORS, respect 401 header

---------

Co-authored-by: Katerina Skroumpelou <sk.katherine@gmail.com>
server-v1.5.0-rc.106
2026-08-14 13:47:17 +03:00
Katerina Skroumpelou c8fd9de39e test(e2e): cover edge functions on the real Deno edge runtime (#111)
* test(e2e): cover edge functions on the real Deno edge runtime

* fix(e2e): work around CLI import-scanner crash on vendored JSDoc examples
2026-08-13 12:11:32 +03:00
Katerina Skroumpelou 2b5d4fd82e feat: withSupabase on the @supabase/middleware engine (#88)
* 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>
server-v1.5.0-rc.104
2026-08-13 12:00:19 +03:00
dependabot[bot] 0558b46368 chore(deps-dev): bump hono in the npm_and_yarn group across 1 directory (#110)
Bumps the npm_and_yarn group with 1 update in the / directory: [hono](https://github.com/honojs/hono).


Updates `hono` from 4.12.27 to 4.12.34
- [Release notes](https://github.com/honojs/hono/releases)
- [Commits](https://github.com/honojs/hono/compare/v4.12.27...v4.12.34)

---
updated-dependencies:
- dependency-name: hono
  dependency-version: 4.12.34
  dependency-type: direct:development
  dependency-group: npm_and_yarn
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-10 17:01:23 +03:00
claude[bot] 5b0f3d6e8e docs: clarify that bare secret/publishable auth matches only the 'default' key (#109)
* docs: clarify that bare secret/publishable auth matches only the 'default' key

Promote the skimmable default-key footnotes in docs/auth-modes.md to
prominent callouts that state explicitly that bare auth: 'secret' /
'publishable' matches ONLY the key named 'default' in the key set, with
no fallback to named keys. Also name the 'default' key in the README
publishable-key example comment.

Docs-only; no code behaviour change. Source-of-truth logic remains
keyName ?? 'default' in src/core/verify-credentials.ts.

* docs: name the key match in the README API-key-protected example

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-07-27 11:32:20 -05:00
dependabot[bot] b84800565c chore(deps-dev): bump hono in the npm_and_yarn group across 1 directory (#108)
Bumps the npm_and_yarn group with 1 update in the / directory: [hono](https://github.com/honojs/hono).


Updates `hono` from 4.12.25 to 4.12.27
- [Release notes](https://github.com/honojs/hono/releases)
- [Commits](https://github.com/honojs/hono/compare/v4.12.25...v4.12.27)

---
updated-dependencies:
- dependency-name: hono
  dependency-version: 4.12.27
  dependency-type: direct:development
  dependency-group: npm_and_yarn
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-22 17:18:06 +03:00
supabase-releaser[bot] 889422d3ae chore(main): release server 1.4.1 (#105)
Co-authored-by: supabase-releaser[bot] <223506987+supabase-releaser[bot]@users.noreply.github.com>
server-v1.4.1
2026-07-22 17:11:47 +03:00
Katerina Skroumpelou 9e45b42a49 fix: apply Injectable programmatically so NestJS adapter loads in raw Node (#107)
* 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
server-v1.4.1-rc.97
2026-07-22 17:08:52 +03:00
Katerina Skroumpelou 8d5c781b3b fix: add Database type override to elysia adapter (#106) server-v1.4.1-rc.96 2026-07-22 16:24:45 +03:00
Elliott Mantock ca53801cb5 fix: add hono adapter Database type override (#100)
adds generic Database type override to hono adapter
added test to ensure chained declarations preserve context type
updated docs to include alternative hono declaration
server-v1.4.1-rc.95
2026-07-22 16:07:52 +03:00
Tomás Pozo 54f869ea3d docs: clarify bare auth mode matches only the default key (#104)
* docs: clarify bare auth mode matches only the default key

Bare `auth: 'publishable'` / `auth: 'secret'` match only the `default`
key in SUPABASE_PUBLISHABLE_KEYS / SUPABASE_SECRET_KEYS. This was
documented inconsistently — two spots wrongly claimed bare `secret`
accepts any key, and most tables/TSDoc omitted the detail entirely.

- Fix incorrect "accepts any secret key" statements in security.md and
  the supabase-server skill (bare = default; `secret:*` = any)
- Name the `default` key directly in every auth-mode table (README,
  auth-modes.md, security.md, src/index.ts TSDoc)
- Add the default-key sentence to the Secret mode section to match
  Publishable, and clarify AuthMode / AuthModeWithKey TSDoc in types.ts

* docs: document default-key resolution for wildcard and client factories

Follow-up to the auth-mode default-key pass, closing two remaining gaps:

- Publishable mode section now mentions the `publishable:*` wildcard, so it
  mirrors the Secret mode sentence exactly.
- Document which key createAdminClient / createContextClient use: the named
  key when `auth.keyName` is set, otherwise `default` with a fallback to the
  first key in the set. This was untouched by docs anywhere despite being
  tested behavior, and it deliberately differs from the `secret` /
  `publishable` auth modes, which match `default` only and never fall back.
2026-07-16 07:25:21 -05:00
supabase-releaser[bot] bfd2f1a222 chore(main): release server 1.4.0 (#103)
Co-authored-by: supabase-releaser[bot] <223506987+supabase-releaser[bot]@users.noreply.github.com>
server-v1.4.0
2026-07-14 19:36:05 +03:00
Katerina Skroumpelou 297925fa11 feat: explicit cors config shape ('default' | 'disabled' | { headers }) (#102)
* feat: explicit cors config shape ('default' | 'none' | { headers })

Keeps boolean/Record forms accepted but deprecated.

* refactor: rename cors 'none' to 'disabled'

Reads more clearly as an on/off switch and matches the SDK-1149 proposal.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
server-v1.4.0-rc.92
2026-07-13 14:59:43 +03:00