New:Introducing Socket Scanning for VS Code Marketplace Extensions.Learn more →
Get Started

@supabase/middleware

Package Overview
Dependencies
Maintainers
15
Versions
9
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@supabase/middleware

Composable, type-safe middleware for Web Fetch handlers. A middleware is a (config, handler) wrapper that runs against the inbound Request, contributes a typed key to ctx, and either short-circuits or falls through — the same shape across every runtime an

Source
npmnpm
Version
0.2.0
Version published
Weekly downloads
109K
82.48%
Maintainers
15
Weekly downloads
 
Created
Source

@supabase/middleware

License: MIT Package pkg.pr.new Docs

Composable, type-safe middleware for Web Fetch handlers.

Status: public alpha (0.1). The core engine and API are still settling — expect breaking changes before a stable 1.0. Follow releases for changes.

A middleware is a withFoo function. Call it with just the config — withFoo(config) — to get an Entry: a typed placeholder that carries the middleware's key, prerequisites, and contribution as phantom types. Pass a flat array of entries to pipeline with a final handler; pipeline folds the array into nested calls at runtime and every entry's contribution lands on ctx in order. No registry, no app.use(), no nesting.

import { pipeline } from '@supabase/middleware'
import { withCors } from '@supabase/middleware/cors'
import { withFeatureFlag } from '@supabase/middleware/feature-flag'

export default {
  fetch: pipeline(
    [
      withCors({}),
      withFeatureFlag({ name: 'beta', evaluate: (req) => req.headers.has('x-beta') }),
    ],
    async (_req, ctx) => Response.json({ flag: ctx.featureFlag.name }),
  ),
}

pipeline returns the outermost (req, ctx) => Response — that is the fetch handler directly, no wrapper. When the runtime invokes it, the framework detects a platform argument (Deno's connection info, a Workers env) and seeds a fresh context itself, so platform values never leak into ctx — the Workers env is captured behind the importable getEnv instead. Because everything is plain Web Fetch, the same stack runs unchanged across Deno, Cloudflare Workers, Bun, and Node.

Install

# npm
npm install @supabase/middleware

# pnpm
pnpm add @supabase/middleware

# Deno / Supabase Edge Functions (no install — import directly)
import { pipeline } from "npm:@supabase/middleware"

Also published on JSR:

deno add jsr:@supabase/middleware

What's in the box

ImportWhat it does
@supabase/middlewarepipeline, defineMiddleware, getEnv, runtimeName, seedContext, and the core types: Entry, FetchHandler, Middleware, Conflict, BaseContext.
@supabase/middleware/feature-flagProvider-agnostic feature flag — admit or short-circuit per request.
@supabase/middleware/corsCORS — answers preflight and stamps response headers (the worked example of the response seam).

How it composes

Each middleware contributes one typed key to ctx. Pass entries as a flat array to pipeline — first in the array runs first on the request. Add satisfies FetchHandler on the pipeline call to anchor the types so the handler sees every upstream key ambiently:

import { pipeline, defineMiddleware } from '@supabase/middleware'
import type { FetchHandler } from '@supabase/middleware'
import { withFeatureFlag } from '@supabase/middleware/feature-flag'

// A middleware is just a `defineMiddleware` call — bundled or your own.
const withRequestId = defineMiddleware<'requestId', void, Record<never, never>, string>({
  key: 'requestId',
  run: () => async (req) => ({
    requestId: req.headers.get('x-request-id') ?? crypto.randomUUID(),
  }),
})

export default {
  fetch: pipeline(
    [
      withRequestId(),    // no config — still returns an Entry
      withFeatureFlag({ name: 'beta', evaluate: (req) => req.headers.has('x-beta') }),
    ],
    async (_req, ctx) => {
      ctx.requestId   //  from withRequestId
      ctx.featureFlag //  from withFeatureFlag — ctx holds middleware contributions, nothing else
      return new Response(null, { status: 200 })
    },
  ) satisfies FetchHandler,
}

Two type-level guarantees, with no runtime cost:

  • Collision detection. Two middleware contributing the same key fail to compile (under the satisfies FetchHandler anchor).
  • Prerequisite enforcement. A middleware can declare upstream keys it needs (e.g. a database middleware that needs jwtClaims from an upstream auth middleware). Composing it without that upstream is a type error — it can't be a bare entry. Prerequisite-declared keys type with no anchor required.

Runtime & environment

Environment access is a plain import — middleware never reach for Deno.env / process.env / a Workers bindings object directly, and ctx carries no reserved framework key:

import { getEnv, runtimeName } from '@supabase/middleware'

getEnv('SUPABASE_DB_URL') // string | undefined, resolved per host
runtimeName // 'node' | 'deno' | 'bun' | 'workerd' | … ('' when unknown) — via std-env

Host detection is delegated to std-env (which tracks the WinterCG Runtime Keys proposal), once at module load. On Cloudflare Workers, env bindings are not ambient — they arrive per request as the second fetch argument — so the entry call captures them module-scoped and getEnv reads them first, falling back to the host's global env (process.env, Deno.env). One consequence: on Workers, getEnv returns undefined at module top level, before the first request.

Supported entry signatures are (request) and (request, env). A third fetch argument — the Workers ExecutionContext (waitUntil / passThroughOnException) — is not honored: it's ignored with a one-time console.warn. The Deno target never passes one.

Request-side by default

A middleware runs before the handler. In the common case it never observes the handler's Response — no next(), no on-the-way-out mutation — so response shape stays under one owner: the handler. Response-side concerns are then plain Response work, right where they belong:

  • Errors — try/catch inside the handler.
  • Response headers / envelopes — shape the Response the handler returns.
import { withFeatureFlag } from '@supabase/middleware/feature-flag'

export default {
  fetch: withFeatureFlag(
    { name: 'beta', evaluate: (req) => req.headers.has('x-beta') },
    async (req, ctx) => {
      try {
        const body = await req.json()
        // response headers / envelope — shaped here, by the response's owner
        return Response.json(
          { flag: ctx.featureFlag.name, body },
          { headers: { 'x-powered-by': 'middleware' } },
        )
      } catch {
        return Response.json({ error: 'bad request' }, { status: 400 })
      }
    },
  ),
}

When a concern is genuinely two-sided and belongs inside a middleware rather than on the entry, reach for the response seam below.

The response seam (when a middleware really needs the way out)

Some concerns are irreducibly two-sided — timing, request-spanning cleanup, CORS (preflight in, headers out). For those, write run as an async function* instead of async. yield is the seam:

run: (config) =>
  async function* (req, ctx) {
    const start = performance.now() // request phase (before)
    const response = yield { timing: { route: req.url } } // ← contribute, then suspend
    response.headers.set('x-time', `${performance.now() - start}`) // response phase (after)
    return response
  }

The yield expression resolves to the downstream Response (typed as Response, inferred — no annotation). yield the contribution at most once — yield means "run downstream and hand me the response." To short-circuit (handler never runs), return new Response(...), exactly as a plain request-side middleware does. try/finally around the yield gives request-spanning cleanup; try/catch can turn a downstream throw into a Response.

This is the one place the "request-side" guarantee is relaxed, and writing function* is the visible, opt-in signal — the 95% plain-async path is unchanged. /cors is the worked example.

Docs

  • Composition primitives — ctx shape, conflict & prerequisite enforcement, composition rules, the response seam.
  • Authoring guide — write your own middleware with defineMiddleware (request-side and generator forms).
  • Per-middleware: feature-flag — the request-side worked example · cors — the response-seam worked example.

Full generated API reference: supabase.github.io/middleware.

License

MIT

Keywords

middleware

FAQs

Package last updated on 06 Aug 2026

Related posts