Logging

Better Auth Integration

Automatically identify users on every request. Every wide event includes who made the request — userId, user profile, and session metadata — with zero manual work.

evlog/better-auth turns anonymous wide events into identified ones. Every request automatically includes who made it — no manual log.set({ user }) needed.

Prompt
Add Better Auth user identification to my app with evlog.

- Import createAuthMiddleware from 'evlog/better-auth'
- Call createAuthMiddleware(auth) to get an identify function
- Call identify(log, headers, path) in your middleware/hook to auto-identify users on every request
- Safe by default — only extracts whitelisted fields, never logs passwords or tokens
- Supports include/exclude route patterns, lifecycle hooks, and Better Auth plugin fields
- Works with all frameworks: Nuxt, Next.js, Express, Hono, Fastify, NestJS, Elysia, standalone

Docs: https://www.evlog.dev/logging/better-auth
Adapters: https://www.evlog.dev/adapters

Quick Start

One middleware, all requests identified:

import { createAuthMiddleware } from 'evlog/better-auth'

const identify = createAuthMiddleware(auth, {
  exclude: ['/api/auth/**'],
})

export default defineEventHandler(async (event) => {
  if (!event.context.log) return
  await identify(event.context.log, event.headers, event.path)
})

Your wide event now includes the user:

{
  "level": "info",
  "method": "POST",
  "path": "/api/checkout",
  "status": 200,
  "duration": "120ms",
  "requestId": "a5669202-7765-4f59-b6f0-b9f40ce71599",
  "cart": { "items": 3, "total": 9999 }
}

How It Works

The integration resolves the Better Auth session from request cookies, extracts safe user and session fields, and sets them on the logger. Auth routes are skipped by default.

RequestMiddlewareBetter AuthLoggerDrain

  1. Incoming request hits your middleware
  2. Middleware checks include/exclude patterns — skips if route doesn't match
  3. getSession(headers) resolves the session via Better Auth (timing is captured)
  4. identifyUser(log, session) sets userId, user, and session on the logger
  5. onIdentify or onAnonymous hook fires based on session result
  6. At request end, the wide event is emitted with full user context to your drain
ExportDescription
identifyUser(log, session)Core helper — extracts safe fields from a session and sets them on the logger. Returns true if identified
createAuthMiddleware(auth)Returns an async (log, headers, path?) => Promise<boolean> function with route filtering, timing, and hooks
createAuthIdentifier(auth)Nitro request hook factory for standalone Nitro apps
maskEmail(email)Mask an email: hugo@example.comh***@example.com

identifyUser

The core building block. Takes a RequestLogger and a Better Auth session, extracts safe fields, and calls log.set(). Returns true if the user was identified, false otherwise:

server/api/checkout.post.ts
import { identifyUser } from 'evlog/better-auth'

const session = await auth.api.getSession({ headers: event.headers })
if (session) {
  const identified = identifyUser(log, session)
  if (identified) {
    log.set({ subscription: 'premium' })
  }
}

Safe by default — only extracts whitelisted fields. Passwords, tokens, and secrets are never included.

Options

OptionTypeDefaultDescription
maskEmailbooleanfalseMask emails as h***@example.com
sessionbooleantrueInclude session metadata (session.id, session.expiresAt, session.ipAddress, session.userAgent)
fieldsstring[]['id', 'name', 'email', 'image', 'emailVerified', 'createdAt']User fields to extract
extend(session) => Record<string, unknown>undefinedAdd custom fields from Better Auth plugins (organizations, roles, etc.)
server/api/checkout.post.ts
identifyUser(log, session, {
  maskEmail: true,
  fields: ['id', 'name'],
  session: false,
})

extend

Use extend to capture fields added by Better Auth plugins (organizations, 2FA, roles, etc.):

server/middleware/auth-identify.ts
import { createAuthMiddleware } from 'evlog/better-auth'

const identify = createAuthMiddleware(auth, {
  extend: (session) => ({
    organization: session.user.activeOrganization,
    role: session.user.role,
  }),
})

Wide event with plugin fields:

Wide Event
{
  "userId": "QBX9tPjJQExWawAbNll75",
  "user": { "id": "QBX9tPjJQExWawAbNll75", "name": "Hugo Richard" },
  "organization": { "id": "org_42", "name": "Acme" },
  "role": "admin"
}

createAuthMiddleware

Framework-agnostic factory. Call it once at startup, then use the returned function in your middleware. The third argument path enables built-in route filtering:

server/middleware/auth-identify.ts
import { createAuthMiddleware } from 'evlog/better-auth'

const identify = createAuthMiddleware(auth, {
  exclude: ['/api/auth/**', '/api/public/**'],
  include: ['/api/**'],
  maskEmail: true,
})

The function signature is (log, headers, path?) => Promise<boolean>. It resolves the session, calls identifyUser, captures timing, fires lifecycle hooks, and silently catches errors so session resolution never breaks a request.

Options

Inherits all identifyUser options, plus:

OptionTypeDefaultDescription
excludestring[]['/api/auth/**']Route patterns to skip (glob)
includestring[]undefinedIf set, only matching routes are resolved
onIdentify(log, session) => voidundefinedCalled after successful identification
onAnonymous(log) => voidundefinedCalled when no session is found

Lifecycle Hooks

Use onIdentify to react to user identification — for example, force-keep logs for premium users via tail sampling:

server/middleware/auth-identify.ts
const identify = createAuthMiddleware(auth, {
  onIdentify: (log, session) => {
    if (session.user.plan === 'enterprise') {
      log.set({ _forceKeep: true })
    }
  },
  onAnonymous: (log) => {
    log.set({ anonymous: true })
  },
})

createAuthIdentifier (Standalone Nitro)

A factory that creates a Nitro request hook. Designed for standalone Nitro apps where the evlog Nitro module handles hook ordering.

For Nuxt, use createAuthMiddleware in a server middleware instead — Nitro plugin hook ordering can cause the logger to not be available yet in the request hook.
server/plugins/evlog-auth.ts
import { createAuthIdentifier } from 'evlog/better-auth'
import { auth } from './lib/auth'

export default defineNitroPlugin((nitroApp) => {
  nitroApp.hooks.hook('request', createAuthIdentifier(auth, {
    exclude: ['/api/auth/**', '/api/public/**'],
  }))
})

Performance

getSession() costs a database query on every request. The auth.resolvedIn field in your wide event tells you exactly how long each resolution takes. For high-traffic apps:

  1. Enable session caching in Better Auth to avoid hitting the database on every request
  2. Use exclude to skip public routes that don't need user context
  3. Use include to limit resolution to specific route patterns
Wide Event — slow session resolution
{
  "auth": { "resolvedIn": 245, "identified": true },
  "duration": "312ms"
}

When auth.resolvedIn is high relative to duration, enable session caching in Better Auth.

Client Identity Sync

On the client side, watch the Better Auth session and call setIdentity() to include user context in client-side logs:

composables/useAuthIdentity.ts
import { authClient } from '~/lib/auth-client'

export function useAuthIdentity() {
  const session = authClient.useSession()

  watch(() => session.value?.data?.user, (user) => {
    if (user) {
      setIdentity({ userId: user.id, userName: user.name })
    } else {
      clearIdentity()
    }
  }, { immediate: true })
}

Call it once in your root layout:

app.vue
<script setup>
useAuthIdentity()
</script>

Client-side logs now include the user identity:

Client Log
{
  "level": "info",
  "tag": "checkout",
  "message": "User clicked checkout",
  "userId": "QBX9tPjJQExWawAbNll75",
  "userName": "Hugo Richard"
}

Wide Event Fields

FieldSourceDescription
userIdsession.user.idTop-level user ID (used by PostHog adapter as distinct_id)
user.idsession.user.idUser ID
user.namesession.user.nameDisplay name
user.emailsession.user.emailEmail (maskable with maskEmail: true)
user.imagesession.user.imageAvatar URL
user.emailVerifiedsession.user.emailVerifiedEmail verification status
user.createdAtsession.user.createdAtAccount creation date (ISO string)
session.idsession.session.idSession ID
session.expiresAtsession.session.expiresAtSession expiry (ISO string)
session.ipAddresssession.session.ipAddressClient IP from the session
session.userAgentsession.session.userAgentUser agent string from the session
session.createdAtsession.session.createdAtSession creation date (ISO string)
auth.resolvedInMeasuredSession resolution time in ms
auth.identifiedComputedWhether the request was identified

Works With the AI SDK

When combined with evlog/ai, your wide events include both user identity and AI metrics in a single event:

Wide Event — AI + User
{
  "method": "POST",
  "path": "/api/chat",
  "status": 200,
  "duration": "4.5s",
  "userId": "QBX9tPjJQExWawAbNll75",
  "user": {
    "id": "QBX9tPjJQExWawAbNll75",
    "name": "Hugo Richard",
    "email": "hugo@example.com"
  },
  "auth": { "resolvedIn": 8, "identified": true },
  "ai": {
    "calls": 1,
    "model": "claude-sonnet-4.6",
    "provider": "anthropic",
    "inputTokens": 3312,
    "outputTokens": 814,
    "totalTokens": 4126,
    "msToFirstChunk": 234,
    "msToFinish": 4500,
    "tokensPerSecond": 180
  }
}

This is the power of wide events — one event per request, all context in one place: who made the request, what they did, how the AI responded, and how it performed.