Auth with Clerk
Clerk signs users in from the browser and puts the session on every server request through its Nuxt module. Your server reads who the user is, mints a PostgREST token for them, and stores it. A middleware does that on every request, and a small watcher syncs the browser when the user signs in or out.
@clerk/nuxt's published types, not run end to end. Set up Clerk itself with its docs.1. Before you start
You need @clerk/nuxt set up, which registers its server middleware for you, and the JWT secret for the module:
# 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
Clerk's middleware makes event.context.auth() available on the server. It returns the signed-in user's id, or null when nobody is signed in:
import type { H3Event } from 'h3'
/** Make the PostgREST token cookie match the current Clerk session. */
export async function syncPostgrestToken(event: H3Event) {
const { userId } = event.context.auth()
if (!userId) {
clearPostgrestToken(event)
return
}
setPostgrestToken(
event,
await mintJwt({ role: 'authenticated', sub: userId }, { expiresIn: '1h' }),
)
}
Clerk's session tokens are short-lived (around a minute) and Clerk refreshes them in the browser, so there's no long expiry to match. Choose how long a PostgREST token should live instead, and see Expiry for the trade-off.
3. Run it on every request
export default defineEventHandler(async (event) => {
await syncPostgrestToken(event)
})
The cookie is minted for signed-in users, renewed on each request, and cleared once Clerk reports no user.
event.context.auth is set by Clerk's own middleware, so it has to run before this one. If you see event.context.auth is not a function, check the order the two middlewares run in.4. Sync the browser on sign-in and sign-out
Clerk signs users in from the browser, and a client-side navigation doesn't reach your server. Give the browser a route to call:
export default defineEventHandler(async (event) => {
await syncPostgrestToken(event)
return { ok: true }
})
Then call it whenever the signed-in state changes, for example in app.vue:
<script setup lang="ts">
const { isSignedIn } = useAuth()
watch(isSignedIn, async () => {
await $fetch('/api/postgrest-token', { method: 'POST' })
await refreshNuxtData() // re-run queries as the new user
})
</script>
5. Query as the user
const { data } = await usePostgrest().from('todos').select('*')
Your RLS policies can read the sub claim, which is the Clerk user id.
Things to know
- Revocation. If a user is signed out or banned, a token already in the browser keeps working until its
exp. The middleware removes it on that user's next request. Use a shorterexpiresInif that window matters.