Playbooks

Auth with Better Auth

Keep a PostgREST token in step with your Better Auth session.

Better Auth owns the sign-in endpoints, so your code doesn't run at the moment of sign-in. Instead, you derive the PostgREST token from the Better Auth session. A small server utility does it, a middleware keeps it fresh on every request, and one route lets the browser sync right after signing in or out.

This guide was checked against Better Auth's published types, not run end to end. Better Auth's own setup for Nuxt is in its docs.

1. Before you start

You need a Better Auth server instance (auth) that your server code can import, for example from server/utils/auth.ts, and the JWT secret for the module:

.env
# Must equal PostgREST's jwt-secret (PGRST_JWT_SECRET)
NUXT_POSTGREST_JWT_SECRET=replace-with-a-secret-of-at-least-32-characters

2. Turn the session into a token

server/utils/postgrest-token.ts
import type { H3Event } from 'h3'

/** Make the PostgREST token cookie match the current Better Auth session. */
export async function syncPostgrestToken(event: H3Event) {
  const session = await auth.api.getSession({ headers: event.headers })

  if (!session) {
    clearPostgrestToken(event)
    return
  }

  setPostgrestToken(
    event,
    await mintJwt(
      { role: 'authenticated', sub: session.user.id },
      { expiresAt: session.session.expiresAt },
    ),
  )
}

getSession returns { session, user } or null. On Nuxt 5 the event is a web-standard request, so pass event.req.headers instead of event.headers. The token expires when the session does, and Better Auth extends sessions as the user stays active, so the token follows that extension because it's re-minted on each request.

3. Run it on every request

server/middleware/postgrest-token.ts
export default defineEventHandler(async (event) => {
  // Better Auth's own endpoints don't need a token
  if (event.path.startsWith('/api/auth')) return

  await syncPostgrestToken(event)
})

Every page and API request now refreshes the cookie from the session. It clears when the session ends, and a returning user whose session is still alive gets a valid token on their first request back, before the page renders.

4. Sync right after signing in or out

Better Auth completes sign-in from the browser, and a client-side navigation doesn't reach your server, so the cookie wouldn't change until the next server request. Give the browser a route to call:

server/api/postgrest-token.post.ts
export default defineEventHandler(async (event) => {
  await syncPostgrestToken(event)
  return { ok: true }
})
await authClient.signIn.email({ email, password })
await $fetch('/api/postgrest-token', { method: 'POST' })
await refreshNuxtData() // re-run queries, now as the user

Do the same after authClient.signOut(): the route finds no session and clears the cookie.

5. Query as the user

usePostgrest() and usePostgrestUser(event) now run as the signed-in user, and your RLS policies can read the sub claim:

const { data } = await usePostgrest().from('todos').select('*')

Things to know

  • Revocation. If you revoke a session in Better Auth, the token already in the browser still works until its exp. The middleware removes it on that user's next request.
  • Cost. getSession runs on every request. If you render pages for signed-in users anyway, Better Auth is already doing this lookup, but check that it hits your session cache.
Copyright © 2026