Authenticated server route
Call Convex from Nitro with the incoming user's identity and a safe HTTP contract.
Result
A same-origin API route requires the Better Auth session, calls a protected Convex query, and maps failures without exposing credentials.
import { api } from '#convex/api'
import { serverConvex, toConvexH3Error } from '#convex/server'
export default defineEventHandler(async (event) => {
setHeader(event, 'cache-control', 'private, no-store')
try {
return await serverConvex(event, { auth: 'required' }).query(api.accounts.exportData)
} catch (error) {
throw toConvexH3Error(error)
}
})toConvexH3Error answers 401 for a missing or invalid session, 502 for a transport failure, and the application's 4xx status for a ConvexError. The response data is the serialized ConvexCallError without functionName, so the browser never learns which Convex function the route called. In the browser, normalizeConvexError() turns the $fetch error back into a ConvexCallError:
import { isConvexCallError, normalizeConvexError } from '@lupinum/better-convex-nuxt/errors'
try {
exportFile.value = await $fetch('/api/account/export')
} catch (raw) {
const error = normalizeConvexError(raw)
if (isConvexCallError(error, 'UNAUTHENTICATED')) await navigateTo('/auth/signin')
else exportError.value = 'Export unavailable'
}Security boundary
serverConvexexchanges only the request's relevant Better Auth cookies.auth: 'required'refuses anonymous fallback.accounts.exportDatarechecks identity and product access.- The response is private and not cached.
- Public errors do not contain cookies, tokens, or upstream bodies.
- Use
requireConvexUser(event)instead when the route needs only the signed-in user, not Convex data.
Verify
- No-cookie request returns 401 with code
UNAUTHENTICATED. - Valid session returns only that user's export.
- Revoked/invalid session does not fall back to anonymous.
- A transport failure returns the safe 502 contract.