* fix(jsr): resolve slow-type errors in elysia and h3 adapters * chore(lint): disable no-empty-object-type for elysia plugin return type
4.7 KiB
H3 / Nuxt Adapter
Setup
Install H3 as a peer dependency:
pnpm add h3
The adapter exports its own withSupabase that returns H3 middleware instead of a fetch handler. Works with standalone H3 servers and Nuxt server routes (which run on H3 under the hood).
Typing event.context.supabaseContext
The middleware stores the context on event.context.supabaseContext. Add this declaration once in your project (e.g. in types/h3.d.ts) for typed access:
import type { SupabaseContext } from '@supabase/server'
declare module 'h3' {
interface H3EventContext {
supabaseContext: SupabaseContext
}
}
Basic app with auth
import { H3 } from 'h3'
import { withSupabase } from '@supabase/server/adapters/h3'
const app = new H3()
// Apply auth to all routes
app.use(withSupabase({ auth: 'user' }))
app.get('/todos', async (event) => {
const { supabase } = event.context.supabaseContext
const { data } = await supabase.from('todos').select()
return data
})
app.get('/profile', async (event) => {
const { supabase, userClaims } = event.context.supabaseContext
const { data } = await supabase
.from('profiles')
.select()
.eq('id', userClaims!.id)
return data
})
export default { fetch: app.fetch }
The context is stored in event.context.supabaseContext and contains the same SupabaseContext fields as the main withSupabase wrapper: supabase, supabaseAdmin, userClaims, jwtClaims, and authMode.
Per-route auth
Apply different auth modes to different routes by attaching the middleware to specific handlers:
import { H3 } from 'h3'
import { withSupabase } from '@supabase/server/adapters/h3'
const app = new H3()
// Public route — no auth
app.get('/health', () => ({ status: 'ok' }))
// User-authenticated route
app.get('/todos', withSupabase({ auth: 'user' }), async (event) => {
const { supabase } = event.context.supabaseContext
const { data } = await supabase.from('todos').select()
return data
})
// Secret-key-protected admin route
app.post('/admin/sync', withSupabase({ auth: 'secret' }), async (event) => {
const { supabaseAdmin } = event.context.supabaseContext
const { data } = await supabaseAdmin
.from('audit_log')
.insert({ action: 'sync' })
return data
})
// Dual auth — users or services
app.get(
'/reports',
withSupabase({ auth: ['user', 'secret'] }),
async (event) => {
const { authMode } = event.context.supabaseContext
return { authMode }
},
)
export default { fetch: app.fetch }
Nuxt: file-based routes
Use defineHandler to attach auth to a single Nuxt server route:
// server/api/games.get.ts
import { defineHandler } from 'h3'
import { withSupabase } from '@supabase/server/adapters/h3'
export default defineHandler({
middleware: [withSupabase({ auth: 'user' })],
handler: async (event) => {
const { supabase } = event.context.supabaseContext
return supabase.from('favorite_games').select()
},
})
Nuxt: app-wide auth
Register as a server middleware to apply auth to every route:
// server/middleware/supabase.ts
import { withSupabase } from '@supabase/server/adapters/h3'
export default withSupabase({ auth: 'user' })
Skip behavior
If a previous middleware already set event.context.supabaseContext, subsequent withSupabase calls skip auth. This enables a pattern where route-level middleware overrides the app-wide default:
const app = new H3()
// App-wide: require user auth
app.use(withSupabase({ auth: 'user' }))
// This route needs secret auth instead.
// The route-level middleware runs first, sets the context,
// and the app-wide middleware skips.
app.post('/webhook', withSupabase({ auth: 'secret' }), async (event) => {
const { supabaseAdmin } = event.context.supabaseContext
// ...
})
CORS
The H3 adapter does not handle CORS — the cors option is excluded from its config type. Use H3's built-in CORS utilities:
import { H3, handleCors } from 'h3'
import { withSupabase } from '@supabase/server/adapters/h3'
const app = new H3()
app.use((event) => handleCors(event, { origin: '*' }))
app.use(withSupabase({ auth: 'user' }))
app.get('/todos', async (event) => {
const { supabase } = event.context.supabaseContext
const { data } = await supabase.from('todos').select()
return data
})
export default { fetch: app.fetch }
Environment overrides
Pass env to override auto-detected environment variables, same as the main wrapper:
app.use(
withSupabase({
auth: 'user',
env: { url: 'http://localhost:54321' },
}),
)
Supabase client options
Forward options to the underlying createClient() calls:
app.use(
withSupabase({
auth: 'user',
supabaseOptions: { db: { schema: 'api' } },
}),
)