Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 7 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -266,12 +266,13 @@ Adapters wrap `withSupabase` for a specific framework's middleware contract. The

> **Adapters are a community-driven initiative.** They're developed, maintained, and evolved by contributors — including responding to upstream framework changes. See [`src/adapters/README.md`](src/adapters/README.md) for the contribution requirements (tests, types, docs, build wiring) if you'd like to add or help maintain one.

| Framework | Import | Framework version | Docs |
| --------- | ---------------------------------- | ---------------------- | -------------------------------------------------- |
| Hono | `@supabase/server/adapters/hono` | `^4.0.0` | [docs/adapters/hono.md](docs/adapters/hono.md) |
| H3 / Nuxt | `@supabase/server/adapters/h3` | `^2.0.0` | [docs/adapters/h3.md](docs/adapters/h3.md) |
| Elysia | `@supabase/server/adapters/elysia` | `^1.4.0` | [docs/adapters/elysia.md](docs/adapters/elysia.md) |
| NestJS | `@supabase/server/adapters/nestjs` | `^10.0.0 \|\| ^11.0.0` | [docs/adapters/nestjs.md](docs/adapters/nestjs.md) |
| Framework | Import | Framework version | Docs |
| -------------- | ------------------------------------------ | -------------------------------------- | ------------------------------------------------------------------ |
| Hono | `@supabase/server/adapters/hono` | `^4.0.0` | [docs/adapters/hono.md](docs/adapters/hono.md) |
| H3 / Nuxt | `@supabase/server/adapters/h3` | `^2.0.0` | [docs/adapters/h3.md](docs/adapters/h3.md) |
| Elysia | `@supabase/server/adapters/elysia` | `^1.4.0` | [docs/adapters/elysia.md](docs/adapters/elysia.md) |
| NestJS | `@supabase/server/adapters/nestjs` | `^10.0.0 \|\| ^11.0.0` | [docs/adapters/nestjs.md](docs/adapters/nestjs.md) |
| TanStack Start | `@supabase/server/adapters/tanstack-start` | `@tanstack/start-client-core ^1.170.0` | [docs/adapters/tanstack-start.md](docs/adapters/tanstack-start.md) |

See the per-adapter docs above for setup, per-route auth, CORS, error handling, and other patterns.

Expand Down
149 changes: 149 additions & 0 deletions docs/adapters/tanstack-start.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
# TanStack Start Adapter

The adapter exposes `withSupabase` as a TanStack Start **request middleware**. Because request middleware runs on every server request, the same middleware works for both server functions and server routes; in either, the `SupabaseContext` is available (and typed) as `context.supabaseContext` in the handler.

It is framework-agnostic: it imports from `@tanstack/start-client-core`, which every `@tanstack/{react,solid,vue}-start` package re-exports. So the same adapter works for React, Solid, and Vue Start.

## Setup

You don't install the core package directly; it comes in with your framework's Start package, which you already have:

```bash
pnpm add @tanstack/react-start # or @tanstack/solid-start, @tanstack/vue-start
```

## Server function

```ts
import { createServerFn } from '@tanstack/react-start'
import { withSupabase } from '@supabase/server/adapters/tanstack-start'

export const getTodos = createServerFn()
.middleware([withSupabase({ auth: 'user' })])
.handler(async ({ context }) => {
const { data } = await context.supabaseContext.supabase
.from('todos')
.select()
return data
})
```

The context is available as `context.supabaseContext` and contains the same `SupabaseContext` fields as the main `withSupabase` wrapper: `supabase`, `supabaseAdmin`, `userClaims`, `jwtClaims`, and `authMode`.

## Server route

The same middleware attaches to a server route's `server.middleware`:

```ts
import { createFileRoute } from '@tanstack/react-router'
import { withSupabase } from '@supabase/server/adapters/tanstack-start'

export const Route = createFileRoute('/api/todos')({
server: {
middleware: [withSupabase({ auth: 'user' })],
handlers: {
GET: async ({ context }) => {
const { data } = await context.supabaseContext.supabase
.from('todos')
.select()
return Response.json(data)
},
},
},
})
```

## Per-route auth

Because middleware is attached per server function or per route, each one can require a different auth mode: just give it its own `withSupabase(...)`.

```ts
// User-authenticated
export const listTodos = createServerFn()
.middleware([withSupabase({ auth: 'user' })])
.handler(async ({ context }) => {
const { data } = await context.supabaseContext.supabase
.from('todos')
.select()
return data
})

// Secret-key-protected (e.g. a trusted server-to-server call)
export const syncAuditLog = createServerFn({ method: 'POST' })
.middleware([withSupabase({ auth: 'secret' })])
.handler(async ({ context }) => {
const { data } = await context.supabaseContext.supabaseAdmin
.from('audit_log')
.insert({ action: 'sync' })
return data
})
```

## Skip behavior

If a previous middleware already resolved `context.supabaseContext`, subsequent `withSupabase` calls skip auth and preserve the established context. The first middleware to run wins, matching the Hono and H3 adapters.

Request middleware runs outer-to-inner: a globally registered `withSupabase` (via `createStart`'s `requestMiddleware`) runs before a per-function or per-route one. So if you need different auth modes for different routes, attach `withSupabase` per route rather than globally; reserve global registration for when every request shares one auth mode.

## Error handling

When auth fails, the middleware throws the package's `AuthError`, which carries an HTTP `status` (`401` for invalid credentials, `500` for server-side auth failures) and a machine-readable `code`. The handler only runs on successful auth.

The most robust place to handle it is server-side (a route's `beforeLoad` or loader), where the thrown value is reliably an `AuthError` instance:

```ts
import { createFileRoute, redirect } from '@tanstack/react-router'
import { AuthError } from '@supabase/server'
import { getTodos } from './todos.server'

export const Route = createFileRoute('/todos')({
loader: async () => {
try {
return { todos: await getTodos() }
} catch (error) {
if (error instanceof AuthError && error.status === 401) {
throw redirect({ to: '/login' })
}
throw error
}
},
})
```

> **Client-invoked server functions:** TanStack Start serializes thrown errors across the RPC boundary. When a server function is called from a client component (e.g. via `useServerFn`), the `AuthError` message survives but the prototype and custom fields (`instanceof AuthError`, `.status`, `.code`) may not be reconstructed on the client. Catch the error in a server-side `beforeLoad`/loader, where fidelity is guaranteed, and map it to a `redirect()` or your own response shape there.

## Environment overrides

Pass `env` to override auto-detected environment variables, same as the main wrapper:

```ts
createServerFn()
.middleware([
withSupabase({ auth: 'user', env: { url: 'http://localhost:54321' } }),
])
.handler(async ({ context }) => context.supabaseContext.userClaims)
```

## Supabase client options

Forward options to the underlying `createClient()` calls:

```ts
createServerFn()
.middleware([
withSupabase({
auth: 'user',
supabaseOptions: { db: { schema: 'api' } },
}),
])
.handler(async ({ context }) => {
const { data } = await context.supabaseContext.supabase
.from('todos')
.select()
return data
})
```

## CORS

The adapter does not handle CORS: the `cors` option is excluded from its config type. Set any required CORS headers in your server route handlers (server functions are same-origin RPC calls and don't need it).
3 changes: 2 additions & 1 deletion jsr.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@
"./adapters/hono": "./src/adapters/hono/index.ts",
"./adapters/h3": "./src/adapters/h3/index.ts",
"./adapters/elysia": "./src/adapters/elysia/index.ts",
"./adapters/nestjs": "./src/adapters/nestjs/index.ts"
"./adapters/nestjs": "./src/adapters/nestjs/index.ts",
"./adapters/tanstack-start": "./src/adapters/tanstack-start/index.ts"
},
"publish": {
"include": ["src/**/*.ts", "README.md", "LICENSE"],
Expand Down
16 changes: 13 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,11 @@
"import": "./dist/adapters/nestjs/index.mjs",
"require": "./dist/adapters/nestjs/index.cjs"
},
"./adapters/tanstack-start": {
"types": "./dist/adapters/tanstack-start/index.d.mts",
"import": "./dist/adapters/tanstack-start/index.mjs",
"require": "./dist/adapters/tanstack-start/index.cjs"
},
"./package.json": "./package.json"
},
"main": "./dist/index.cjs",
Expand Down Expand Up @@ -82,14 +87,18 @@
"peerDependencies": {
"@nestjs/common": "^10.0.0 || ^11.0.0",
"@supabase/supabase-js": "^2.0.0",
"@tanstack/start-client-core": "^1.170.0",
"elysia": "^1.4.0",
"h3": "^2.0.0",
"hono": "^4.0.0",
"elysia": "^1.4.0"
"hono": "^4.0.0"
},
"peerDependenciesMeta": {
"@nestjs/common": {
"optional": true
},
"@tanstack/start-client-core": {
"optional": true
},
"h3": {
"optional": true
},
Expand All @@ -110,9 +119,10 @@
"@nestjs/testing": "^11.1.19",
"@supabase/supabase-js": "^2.105.4",
"@swc/core": "^1.15.33",
"@tanstack/start-client-core": "^1.170.4",
"@types/supertest": "^7.2.0",
"eslint": "^10.0.2",
"elysia": "^1.4.0",
"eslint": "^10.0.2",
"h3": "2.0.1-rc.20",
"hono": "^4.12.5",
"prettier": "3.8.1",
Expand Down
80 changes: 69 additions & 11 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading