Other frameworks
Run withBetterSupabase in any framework through a bridge, or write an adapter with handle().
better-supabase ships a bridge for each framework below. A bridge runs an
@supabase/middleware entry array (with
withBetterSupabase first) in the framework's
middleware slot and returns the framework's response back through the
entries.
| Framework | Import | Docs |
|---|---|---|
| TanStack Start | toTanStackStart from better-supabase/tanstack-start | TanStack Start |
| SvelteKit | toSvelteKit from better-supabase/sveltekit | SvelteKit |
| React Router | toReactRouter from better-supabase/react-router | React Router |
| H3 2, Nitro 3 | toH3 from better-supabase/h3 | H3 |
| Nitro 2, Nuxt | toH3V1 from better-supabase/h3/v1 | Nuxt |
| Elysia | toElysia from better-supabase/elysia | Elysia |
| Node, Express | toNodeHandler, toExpress from better-supabase/node | Node |
| Fastify, Koa | toFastify, toKoa from better-supabase/node | Node |
| NestJS | toNestMiddleware from better-supabase/nestjs | NestJS |
| Astro | createAstro from better-supabase/astro | Astro |
| SolidStart | createSolidStart from better-supabase/solid-start | SolidStart |
The bridges are typed structurally: none of them imports its framework, so installing better-supabase adds no framework dependency.
The @supabase/server adapters (adapters/hono, /h3, /elysia and
/nestjs) are deprecated upstream and removed on 2026-12-01. Move to the
bridge for your framework; NestJS apps move to better-supabase/nestjs.
Write a bridge
A framework with a middleware that gets a Request and a next() returning
a Response needs about ten lines. Fold the pipeline once, hand the
framework's next to the terminal through the context, and return the
response:
import { type AnyEntry, pipeline, seedContext } from "@supabase/middleware";
const NEXT = Symbol("acme.next");
export function toAcme(entries: readonly AnyEntry[]) {
const run = pipeline(entries, (_request, ctx) => {
const { c, next } = (
ctx as { [NEXT]: { c: AcmeContext; next: () => Promise<Response> } }
)[NEXT];
Object.assign(c.locals, ctx);
return next();
});
return (c: AcmeContext, next: () => Promise<Response>) =>
run(c.request, { ...seedContext(c.env), [NEXT]: { c, next } });
}Write an adapter
The built-in adapters share their request handling through
better-supabase/server, so an adapter for another framework is a thin layer:
| Export | What it does |
|---|---|
handle(server, request, run, options) | resolves the caller, applies the guard (401 or 403 Problem Details), answers run's value as JSON or Problem Details, turns other errors into a 500, adds the context's cookies and hands pending event sends to waitUntil |
extendServer(server, extra) | the adapter's server: every BetterServer method plus the adapter's own, without loading the environment early |
flushEvents(server, waitUntil) | hands event sink sends still running to waitUntil, for adapters that build their own responses |
unexpectedResponse(cause, { instance, expose }) | the 500 for a thrown error, with its message only when expose is set |
resolveToken(server, token) | verifies a bare access token (a WebSocket's, a queue message's) without cookies |
ctx.cookies() | the cookies ctx.apply(response) would add, for frameworks that set cookies through their own API |
An adapter for a framework whose handlers take a Request and return a
Response:
import type { AnyFunctions, AnyModels, BetterSupabase } from "better-supabase";
import {
type BetterServer,
createServer,
extendServer,
handle,
type MiddlewareOptions,
type ServerContext,
type ServerOptions,
} from "better-supabase/server";
import type { AcmeContext } from "acme";
export interface BetterAcme<
M extends AnyModels,
F extends AnyFunctions,
E,
> extends BetterServer<M, F, E> {
route(
fn: (c: AcmeContext, ctx: ServerContext<M, F, E>) => unknown,
options?: MiddlewareOptions,
): (c: AcmeContext) => Promise<Response>;
}
export function createAcme<M extends AnyModels, D, F extends AnyFunctions, E>(
betterSupabase: BetterSupabase<M, D, F, E>,
options: ServerOptions & { readonly exposeErrors?: boolean } = {},
): BetterAcme<M, F, E> {
const bs = createServer(betterSupabase, options);
return extendServer<BetterAcme<M, F, E>>(bs, {
route(fn, routeOptions = {}) {
return (c) =>
handle(bs, c.request, (ctx) => fn(c, ctx), {
...routeOptions,
...(options.exposeErrors === undefined
? {}
: { expose: options.exposeErrors }),
waitUntil: (promise) => c.waitUntil(promise),
});
},
});
}Then prove it behaves like the built-in adapters with
testAdapter:
import { testAdapter } from "better-supabase/testing";
import { it } from "vitest";
it("conforms", () =>
testAdapter("acme", {
betterSupabase,
serve: (server, run, { allow, waitUntil }) => {
const route = createAcme(betterSupabase, server).route(
(_c, ctx) => run(ctx),
{ allow },
);
return (request) => route({ request, waitUntil });
},
}));Without an adapter, createServer(betterSupabase).context(request) gives the
caller's db, auth and supabase in any framework, and
defineResource(betterSupabase, table) serves the REST resource routes from
any router through resource.handle(request, ctx.db, id).
Last updated on