# Elysia Adapter ## Setup Install Elysia as a peer dependency: ```bash pnpm add elysia ``` The adapter exports its own `withSupabase` that returns an Elysia plugin instead of a fetch handler. ## Basic app with auth ```ts import { Elysia } from 'elysia' import { withSupabase } from '@supabase/server/adapters/elysia' const app = new Elysia() .use(withSupabase({ auth: 'user' })) .get('/todos', async ({ supabaseContext }) => { const { data } = await supabaseContext.supabase.from('todos').select() return data }) app.listen(3000) ``` The context is available as `supabaseContext` in your route handlers 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 using the plugin on scoped route groups: ```ts import { Elysia } from 'elysia' import { withSupabase } from '@supabase/server/adapters/elysia' const app = new Elysia() // Public route — no auth .get('/health', () => ({ status: 'ok' })) // User-authenticated routes .group('/api', (app) => app .use(withSupabase({ auth: 'user' })) .get('/todos', async ({ supabaseContext }) => { const { data } = await supabaseContext.supabase.from('todos').select() return data }), ) // Secret-key-protected admin routes .group('/admin', (app) => app .use(withSupabase({ auth: 'secret' })) .post('/sync', async ({ supabaseContext }) => { const { data } = await supabaseContext.supabaseAdmin .from('audit_log') .insert({ action: 'sync' }) return data }), ) app.listen(3000) ``` ## Skip behavior If a previous plugin already resolved `supabaseContext`, subsequent `withSupabase` calls skip auth. This allows chaining plugins without redundant work. **Important:** The plugin calls `.as('scoped')` so its `resolve` hook propagates one level up to the parent app — routes registered after `.use(withSupabase(...))` will see `supabaseContext`. The skip-if-set pattern cannot make a route stricter than an already-resolved context. For routes that need different auth than the rest of the app, use scoped `.group()` with `.use(withSupabase(...))` without an app-wide plugin (see the "Per-route auth" section above). ## CORS The Elysia adapter does not handle CORS — the `cors` option is excluded from its config type. Use Elysia's CORS plugin: ```ts import { Elysia } from 'elysia' import { cors } from '@elysiajs/cors' import { withSupabase } from '@supabase/server/adapters/elysia' const app = new Elysia() .use(cors()) .use(withSupabase({ auth: 'user' })) .get('/todos', async ({ supabaseContext }) => { const { data } = await supabaseContext.supabase.from('todos').select() return data }) app.listen(3000) ``` ## Error handling When auth fails, the plugin throws a `SupabaseError`. The HTTP status is on `.status` directly, and the original `AuthError` is available as the typed `.cause`. Discriminate in `onError` via `code === 'SupabaseError'`: ```ts import { Elysia } from 'elysia' import { withSupabase } from '@supabase/server/adapters/elysia' const app = new Elysia() .use(withSupabase({ auth: 'user' })) .onError(({ code, error, status }) => { if (code !== 'SupabaseError') return return status(error.status as 401, { error: error.message, code: error.cause.code, }) }) .get('/todos', async ({ supabaseContext }) => { const { data } = await supabaseContext.supabase.from('todos').select() return data }) app.listen(3000) ``` Without a custom `onError`, Elysia uses the `status` property on the thrown `SupabaseError` to set the response status automatically (401 for auth failures, 500 for internal errors). ## Environment overrides Pass `env` to override auto-detected environment variables, same as the main wrapper: ```ts app.use(withSupabase({ auth: 'user', env: { url: 'http://localhost:54321' } })) ``` ## Supabase client options Forward options to the underlying `createClient()` calls: ```ts app.use( withSupabase({ auth: 'user', supabaseOptions: { db: { schema: 'api' } }, }), ) ```