Supabase Middleware: компонуемое и переиспользуемое middleware для Web Fetch-обработчиков

Источник: Supabase•

Supabase Middleware: компонуемое и переиспользуемое middleware для Web Fetch-обработчиков

Типобезопасное, компонуемое middleware для Web Fetch-обработчиков. Напишите один раз, используйте в любой среде выполнения fetch или внутри фреймворка.

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

О чём эта статья