Skip to content

feat: add middleware option to withSupabase#88

Draft
mandarini wants to merge 8 commits into
mainfrom
feat/plugins-option
Draft

feat: add middleware option to withSupabase#88
mandarini wants to merge 8 commits into
mainfrom
feat/plugins-option

Conversation

@mandarini

@mandarini mandarini commented Jul 1, 2026

Copy link
Copy Markdown
Collaborator

Summary

Adds a middleware option to withSupabase so that @supabase/middleware entries can compose around the handler after the Supabase context is established — plus the first two first-party middleware, withPostgres and withClaims.

Naming: the array holds middleware (per-request behavior), so that's its name. The word plugins is reserved for the package-level concept — a Plugin's middleware goes in this array, its client namespace goes in createClient({ plugins }) (supabase-js#2485). One word per concept.

import { withSupabase } from '@supabase/server'
import { withPostgres } from '@supabase/server/middleware/postgres'

export default {
  fetch: withSupabase(
    { auth: 'user', middleware: [withPostgres()] },
    async (req, ctx) => {
      ctx.supabase      // from @supabase/server — already present when the middleware runs
      ctx.postgres      // from withPostgres — RLS-scoped SQL as the caller
      const posts = await ctx.postgres.query('select id, title from posts limit 10')
      return Response.json({ posts })
    },
  ),
}

The middleware receive ctx.supabase, ctx.jwtClaims, etc. already populated — they run after the Supabase context is created, not before. This is the key difference from wrapping withSupabase itself.

Architecture

End user
  → withSupabase({ middleware: [withPostgres(), withRateLimit(cfg)] }, handler)
  → never sees pipeline()

withSupabase internals (this PR)
  → reduceRight(middleware, handler) — same fold as pipeline()
  → seeds the chain's context via seedContext() from @supabase/middleware
  → only code that ever composes entries

Middleware author (e.g. withPostgres)
  → defineMiddleware({ key, run })
  → env access via the importable getEnv() — no reserved ctx keys
  → never calls pipeline()

What changed

  • src/with-supabase.ts — two named overloads (no-middleware / with-middleware) so TypeScript can infer the handler's ctx type concretely. MiddlewareCtx<Entries> accumulates the entries' key contributions via a recursive conditional type. At runtime, entries are folded right-to-left with reduceRight; the merged context is marked via seedContext() so engine entries recognise it as upstream (the engine no longer has a ctx._runtime facet — env access is the importable getEnv)
  • src/middleware/postgres (@supabase/server/middleware/postgres) — withPostgres: contributes ctx.postgres, an RLS-scoped pg client. Every query runs in its own transaction that injects the caller's claims (request.jwt.claims) and drops to their role, PostgREST-style — transaction-local, so nothing leaks onto the pooled connection; the role is clamped to authenticated/anon. Connection string defaults to getEnv('SUPABASE_DB_URL'). Includes a table-grants hint on 42501 errors
  • src/middleware/claims (@supabase/server/middleware/claims) — withClaims: contributes ctx.jwtClaims for standalone pipelines outside withSupabase (demo-grade: decodes without signature verification, documented as such)
  • src/with-supabase.test.ts + middleware tests — composition after Supabase context, short-circuit before handler, array order, CORS with middleware present, claim injection/role clamp/rollback/42501 hint for withPostgres
  • package.json — adds @supabase/middleware as a dependency (preview build of main: pkg.pr.new/supabase/middleware/@supabase/middleware@0641674) + the two new subpath exports
  • Both middleware exports carry explicit Middleware<...> type annotations (JSR slow-types requires explicit public API types)

Try it

npm i https://pkg.pr.new/@supabase/server@88

Notes

  • The existing withSupabase(config, handler) API is unchanged — overload 1 is identical to the current signature
  • MiddlewareCtx<Entries> mirrors pipeline's internal Accumulate type without depending on the internal export
  • A full implementation would widen the prerequisite-validation seed in MiddlewareCtx to include SupabaseContext, so middleware that declare In prerequisites on supabase or jwtClaims get compile-time checking. Left as a follow-up
  • The internal rewrite of withSupabase as composed middleware (withSupabaseClient / withSupabaseAdminClient) lands in this PR before merge — public API unchanged

Merge order

  • Blocked by supabase/web-middleware#9 — merged (and the engine repo/package renamed to @supabase/middleware, now including the importable-getEnv runtime API from middleware#12). Remaining before merge: land the withSupabase-as-middleware internal rewrite (see Notes)
  • Not a dependency: feat(supabase): add plugins option to createClient supabase-js#2485 is the client-side counterpart (flat plugins array on createClient — the package-level namespaces; this PR's array is the package's middleware); no code dependency in either direction
  • All three run together in plugin-examples

@pkg-pr-new

pkg-pr-new Bot commented Jul 1, 2026

Copy link
Copy Markdown

Open in StackBlitz

npm i https://pkg.pr.new/@supabase/server@88

commit: 16b75f8

@mandarini
mandarini force-pushed the feat/plugins-option branch from 0e747ac to ad449a9 Compare July 1, 2026 16:55
@mandarini mandarini changed the title feat: add plugins option to withSupabase feat: add middleware option to withSupabase Jul 2, 2026
@mandarini
mandarini force-pushed the feat/plugins-option branch 2 times, most recently from 9e402e1 to 44b05da Compare July 9, 2026 08:14
mandarini and others added 6 commits July 21, 2026 14:32
…lution

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>
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>
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>
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>
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>
@mandarini
mandarini force-pushed the feat/plugins-option branch from fa1d063 to ac091c2 Compare July 21, 2026 11:34
mandarini and others added 2 commits July 21, 2026 14:35
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>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant