2 октября на мероприятии Supabase Select мы представили @supabase/middleware — компонуемое и типобезопасное промежуточное ПО для обработчиков Web Fetch. Теперь вы можете один раз написать логику для каждого запроса и запускать её в любой среде выполнения, поддерживающей fetch. Это же промежуточное ПО работает внутри таких фреймворков, как Hono, H3, Elysia, NestJS и TanStack Start. Чтобы использовать его во фреймворке, достаточно скопировать небольшой файл-мост в свой проект. @supabase/middleware распространяется бесплатно по лицензии MIT и создано для разработчиков на TypeScript, которые пишут обработчики fetch.
@supabase/server с мая закрывает задачу настройки: один оберточный компонент предоставляет вашему обработчику клиент, ограниченный областью видимости вызывающего абонента, клиент с правами администратора, а также проверенные идентификационные данные и утверждения (claims). Однако ваш обработчик по-прежнему несет в себе собственную логику для каждого запроса: проверку флагов, обработку CORS, определение того, что нужно запросу до выполнения вашего кода. Эта логика остается «запертой» в обработчике, где вы её написали.
При переходе на другую среду выполнения вам приходится писать её заново. При переходе на фреймворк вы переписываете её как промежуточное ПО этого фреймворка, и она остается там. Начиная второй проект, вы копируете файл, или ваш ИИ-помощник пишет его с нуля, каждый раз немного иначе. Каждая версия — это новый риск допустить ошибку в безопасности.
Мы хотели, чтобы эта логика находилась в одном месте и следовала за вашим обработчиком везде. Поэтому мы создали Supabase Middleware. Вы определяете каждую часть один раз, компонуете их в нужном порядке и используете в любой среде выполнения fetch или внутри фреймворка.
Вот как это выглядит в обработчике. Настройка из @supabase/server и логика, которую вы добавляете поверх, становятся элементами одного списка.
_12
import { pipeline } from '@supabase/middleware'
_12
import { withCors } from '@supabase/middleware/cors'
_12
import { withSupabaseClient } from '@supabase/server/middleware/client'
_12
import { withRequiredClaims } from '@supabase/server/middleware/required-claims'
_12
_12
export default {
_12
fetch: pipeline([withCors({}), withRequiredClaims(), withSupabaseClient()], async (req, ctx) => {
_12
// ctx.supabase ограничено областью видимости вызывающего абонента
_12
const { data } = await ctx.supabase.from('todos').select()
_12
return Response.json(data)
_12
_12
Запрос проходит через элементы в порядке очереди. withCors отвечает на предварительный запрос (preflight) и добавляет заголовки CORS к ответам. withRequiredClaims проверяет JWT вызывающего абонента; если он недействителен, цепочка прерывается, и вызывающий абонент получает 401. withSupabaseClient добавляет ctx.supabase — клиент, ограниченный этим вызывающим абонентом. Затем выполняется ваш обработчик, и в нем остается только то, что относится непосредственно к вашей задаче.
Каждый элемент выполняет одну задачу и не «знает» об обработчике, в котором находится. Он добавляет один типизированный ключ в общий ctx, и ваш обработчик видит всё, что было добавлено до него, с правильными типами. Элемент также может вернуть Response и остановить цепочку, либо прочитать и изменить ответ на выходе. Именно поэтому вы можете перенести элемент в другой обработчик, не переписывая его.
@supabase/middleware — это движок, стоящий за конвейером (pipeline) выше, и withSupabase в @supabase/server работает на его основе. @supabase/server также поставляет приведенное ниже промежуточное ПО в виде отдельных элементов. Два из них, withSupabaseClient и withSupabaseAdminClient, — это те же компоненты, из которых построено withSupabase. Движок добавляет withCors и withFeatureFlag.
Большинство из этого — не новые возможности. Например, ctx.supabase документирован как ограниченный RLS еще с мая. Новизна заключается в самом движке. Каждый компонент — это элемент в одном и том же списке, поэтому вы можете добавить один, удалить другой или написать свой собственный, и этот список будет следовать за вашим обработчиком везде.
Вы можете выбрать среду выполнения или фреймворк, которые подходят для задачи, и изменить их позже, не переписывая логику обработки запросов. В этом разделе показано, как это работает в других средах выполнения и внутри фреймворка.
Экспорт конвейера как обработчика fetch#
Переход на другую среду выполнения не означает переписывание логики. Это важно, когда ваш код не работает в одном месте. Допустим, часть ваших функций выполняется в Supabase Edge Functions, а другие — в Vercel Functions или Cloudflare Workers, и правила аутентификации и CORS должны совпадать в обоих случаях. Вы пишете эти правила один раз, и каждая среда выполнения их исполняет.
Это работает, потому что конвейер в примере выше — это стандартный обработчик fetch: функция, которая принимает Request и возвращает Response. Supabase Edge Functions, Vercel Functions, Cloudflare Workers, Deno, Bun и Node 22 или новее могут запустить такой обработчик, а движок основан на обычном Web Fetch.
Использование движка не привязывает вас к Supabase. Он не требует базы данных, проекта Supabase или учетной записи и распространяется по лицензии MIT. Поэтому, если вы решите отказаться от Supabase, движок и всё, что вы с ним написали, останется с вами.
Обратите внимание, что не всё промежуточное ПО работает везде. withPostgresClient и withPostgresAdminClient не работают в Cloudflare Workers, так как pg требует TCP-сокет. Ограничение исходит от pg. Сам движок работает везде, где есть fetch. В Workers используйте withSupabaseClient и ctx.supabase.
Внутри фреймворка с мостом#
Переход на фреймворк также не требует переписывания логики. Например, допустим, ваши Edge Functions — это обычные обработчики fetch, ваш API работает на Hono, и одни и те же правила аутентификации и CORS должны применяться к обоим.
У каждого фреймворка свой формат промежуточного ПО и свой объект контекста, поэтому ПО, написанное для одного, не работает в другом. Без моста переход на фреймворк означает переписывание логики как промежуточного ПО этого фреймворка. Мост устраняет этот шаг. Это небольшой файл, который вы копируете в свой проект, а не пакет, поэтому единственное, что нужно установить — это @supabase/middleware. Он запускает уже написанное вами промежуточное ПО внутри слота фреймворка, и вы можете редактировать этот файл.
Мосты для Hono, H3, Elysia, NestJS и TanStack Start уже доступны, каждый из них проверен на соответствие типам с минимальным примером приложения. Вот версия для Hono. Скопируйте hono/supabase-middleware.ts в свой проект, затем передайте в toHono тот же массив элементов, который вы бы передали в pipeline в Edge Functions. Элементы остаются такими, как вы их написали, а мост — это единственный новый компонент.
_16
import { withSupabaseClient } from '@supabase/server/middleware/client'
_16
import { withRequiredClaims } from '@supabase/server/middleware/required-claims'
_16
import { Hono } from 'hono'
_16
_16
import { toHono } from './lib/supabase-middleware.js'
_16
_16
const app = new Hono()
_16
.use('*', toHono([withRequiredClaims(), withSupabaseClient()]))
_16
.get('/todos', async (c) => {
_16
const { data, error } = await c.var.supabase.from('todos').select()
_16
if (error) return c.json({ error: error.message }, 500)
_16
return c.json(data)
_16
_16
.get('/me', (c) => c.json({ id: c.var.jwtClaims.sub }))
_16
_16
export default { fetch: app.fetch }
У Hono есть собственное промежуточное ПО, которое остается в Hono. Промежуточное ПО, написанное по стандарту fetch, работает там тоже, как и в любом другом фреймворке с мостом.
Мы создали промежуточное ПО Supabase с помощью defineMiddleware — того же публичного API, которое доступно и вам, без использования скрытых внутренних API.
Предоставьте своему приложению собственный MCP-сервер#
Предположим, ваши пользователи хотят использовать ваше приложение через собственный агент. Это означает, что для вашего приложения нужен MCP-сервер, а MCP-серверу требуется авторизация: поток защищенного ресурса OAuth. Он включает маршрут метаданных, предварительную проверку (preflight), заголовок запроса (challenge header) в соответствующих ответах и URL-адреса, соответствующие тем, что вызвал клиент. Каждая часть должна совпадать. Если хотя бы один элемент неверен, ошибки не возникает, но клиент не может подключиться.
С помощью Supabase Middleware весь этот поток превращается в одну запись. withOAuthProtectedResource обрабатывает обнаружение OAuth для MCP-клиентов, а withSupabase({ auth: 'user' }) проверяет токен пользователя и предоставляет вашему обработчику клиент, ограниченный правами этого пользователя.
_14
import { pipeline } from 'npm:@supabase/middleware@1'
_14
import { withOAuthProtectedResource, withSupabase } from 'npm:@supabase/server@1'
_14
_14
export default {
_14
fetch: pipeline(
_14
// 1. Обнаружение OAuth для MCP-клиентов, 2. проверка токена пользователя и ограничение клиента его правами
_14
[withOAuthProtectedResource(), withSupabase({ auth: 'user' })],
_14
async (req, { supabase }) => {
_14
// RLS ограничивает этот запрос текущим авторизованным пользователем
_14
const { data } = await supabase.from('tasks').select('id, title')
_14
return Response.json(data)
_14
_14
_14
withOAuthProtectedResource отвечает на запрос метаданных до проверки авторизации, а при возврате добавляет заголовок OAuth challenge к ответу 401. Поскольку клиент ограничен правами авторизованного пользователя, каждый вызов инструмента выполняется от его имени, поэтому ваши инструменты могут использовать обычные запросы. Мы проверили это на работающем MCP-сервере в Edge Functions, а руководство «Bring your own MCP» описывает создание полноценного сервера с использованием тех же двух записей.
Ваши политики RLS применяются к агенту так же, как и к сессии браузера. Вам не нужно копировать их в каждый инструмент, и при изменении политики доступ агента меняется автоматически.
Написание собственного middleware#
Supabase не предоставляет всё, что может потребоваться вашим обработчикам для каждого запроса. Ваш собственный поток авторизации, способ вызова другого сервиса и телеметрия — это также логика для каждого запроса, и без middleware вам пришлось бы копировать её из обработчика в обработчик. defineMiddleware превращает каждую такую задачу в запись, которую вы пишете один раз и используете где угодно в конвейере (pipeline). Это работает так же, как и наши встроенные решения, поскольку используется тот же API.
Вы передаете ключ и функцию выполнения (run). Функция run принимает вашу конфигурацию и возвращает функцию, которая обрабатывает каждый запрос, а эта функция возвращает объект с вашим ключом. Вот пример добавления идентификатора запроса (request ID):
_10
import { defineMiddleware } from '@supabase/middleware'
_10
_10
const withRequestId = defineMiddleware<'requestId', void, Record<never, never>, string>({
_10
key: 'requestId',
_10
run: () => async (req) => ({
_10
requestId: req.headers.get('x-request-id') ?? crypto.randomUUID(),
_10
_10
Аргументы типа — это ключ, конфигурация, зависимости от предыдущего middleware и то, что добавляется в контекст (ctx). Типы позволяют выявлять ошибки до выпуска кода, а ошибки в логике обработки запросов легко допустить и трудно обнаружить. Дублирование ключа приведет к ошибке компиляции, как и размещение middleware перед тем, от которого оно зависит. Эти проверки выполняются на уровне типов, поэтому требуется TypeScript 5.4 или новее. В обычном JavaScript поведение во время выполнения будет таким же, но без этих проверок.
Пример пользовательского middleware для извлечения ID организации из JWT вызывающей стороны:
_23
import { defineMiddleware, pipeline } from '@supabase/middleware'
_23
import type { JWTClaims } from '@supabase/server'
_23
import { withSupabaseClient } from '@supabase/server/middleware/client'
_23
import { withRequiredClaims } from '@supabase/server/middleware/required-claims'
_23
_23
// Требует ctx.jwtClaims из предыдущей записи, добавляет ctx.orgId
_23
const withOrg = defineMiddleware<'orgId', void, { jwtClaims: JWTClaims }, string>({
_23
key: 'orgId',
_23
run: () => async (_req, ctx) => {
_23
const orgId = ctx.jwtClaims.app_metadata?.org_id
_23
if (typeof orgId !== 'string') {
_23
return Response.json({ error: 'No organization' }, { status: 403 })
_23
_23
return { orgId }
_23
_23
_23
_23
export default {
_23
fetch: pipeline([withRequiredClaims(), withOrg(), withSupabaseClient()], async (_req, ctx) => {
_23
const { data } = await ctx.supabase.from('projects').select().eq('org_id', ctx.orgId)
_23
return Response.json(data)
_23
_23
Ваша логика не привязана к одному проекту. Вместо копирования файла в следующий проект, вы публикуете свое middleware как пакет и устанавливаете его. Логика в пакете имеет версию, поэтому при изменении потока авторизации или добавлении телеметрии вы выпускаете релиз, а не редактируете каждый репозиторий.
Например, @supabase-labs/middleware-openfeature — это независимый от поставщика пакет, который зависит только от движка и @openfeature/core. Мы написали его вне репозитория движка, следуя руководству по разработке, используя тот же defineMiddleware, что и остальные.
Установите пакет, а затем попробуйте пример с MCP-сервером.
_10
npm install @supabase/middleware
- Создайте MCP-сервер для вашего приложения с помощью руководства «Bring your own MCP»
- Напишите собственное middleware
- Используйте его в фреймворке или откажитесь от адаптеров фреймворков с помощью руководства по фреймворкам
- Ознакомьтесь со справочником API и репозиторием на GitHub



/)






