Skip to content

Latest commit

 

History

History
194 lines (143 loc) · 4.71 KB

File metadata and controls

194 lines (143 loc) · 4.71 KB

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' } },
  }),
)