Auth Security

Authentication

User authentication with SSR support, zero loading flash, and declarative components.

Choosing the Right Composable

ComposablePurposeUse when...
useConvexAuth()Reactive auth state + auth actionsDefault choice. Sign in, sign up, sign out, and read auth state.
useAuthClient() / custom clientRaw Better Auth clientPlugin-specific APIs (admin, organization, etc.).

useConvexAuth() is the canonical public composable for auth state and common auth actions in this module.

Reading Auth State (useConvexAuth)

<script setup lang="ts">
const { isAuthenticated, user, isPending } = useConvexAuth()
</script>

<template>
  <div v-if="isPending">Checking auth...</div>
  <div v-else-if="isAuthenticated">Welcome, {{ user?.name }}!</div>
  <div v-else>
    <NuxtLink to="/auth/signin">Sign In</NuxtLink>
  </div>
</template>

SSR Auth Flow (No Flash)

Authentication is pre-populated during SSR for instant authenticated rendering:

flowchart LR
  A["Browser Cookie"] --> B["Nuxt SSR plugin"]
  B --> C["Convex JWT exchange"]
  C --> D["HTML + hydrated auth state"]
  D --> E["Client hydration (no flash)"]
  E --> F["Convex WebSocket starts"]

Key benefit: Users see authenticated content immediately, no flash of unauthenticated state.

CSR-Only Mode

When running with ssr: false in your Nuxt config, the module automatically detects this and fetches auth state client-side:

1. Browser loads page (no SSR)
          |
          v
2. Client Plugin
   - Detects CSR-only mode
   - Fetches token from /api/auth/convex/token
   - Extracts user from JWT
   - Initializes ConvexClient with token
CSR mode requires one additional request to check auth status. This is unavoidable since HttpOnly cookies cannot be read by JavaScript.

URLs and Local Development (Progressive Setup)

Recommended default (most apps): only set CONVEX_URL.

.env.local
CONVEX_URL=https://your-dev-deployment.convex.cloud
SITE_URL=http://localhost:3000

The module automatically derives convex.siteUrl (*.convex.site) from CONVEX_URL.

  • convex.siteUrl = Convex HTTP Actions host (used by the Nuxt auth proxy and SSR token exchange)
  • SITE_URL = your app origin used by Better Auth inside convex/auth.ts (important for redirects/OAuth) ::

Custom Convex HTTP Actions Domain (Advanced)

If your Convex HTTP Actions are served from a custom domain, override convex.siteUrl explicitly:
nuxt.config.ts
export default defineNuxtConfig({
  modules: ['better-convex-nuxt'],
  convex: {
    url: process.env.CONVEX_URL,
    siteUrl: 'https://api.example.com',
  },
})

Cross-Origin / Preview Scenarios (Advanced)

For localhost development, keep auth requests same-origin through the Nuxt proxy (/api/auth/*) and point the module at your dev Convex HTTP Actions URL.
nuxt.config.ts
export default defineNuxtConfig({
  modules: ['better-convex-nuxt'],
  convex: {
    url: process.env.CONVEX_URL,
    siteUrl: process.env.CONVEX_SITE_URL,
    trustedOrigins: ['https://preview-*.vercel.app'],
  },
})
.env.local
CONVEX_URL=https://your-dev-deployment.convex.cloud
CONVEX_SITE_URL=https://your-dev-deployment.convex.site
SITE_URL=http://localhost:3000
This avoids browser CORS preflight failures caused by cross-origin auth requests to production domains.
better-convex-nuxtv0.2.11+ follows only canonical host redirects server-side (same path/query, different host) and forwards OAuth redirects to the browser.
When using the Nuxt auth proxy (/api/auth/*), set convex.siteUrl in nuxt.config to your Convex HTTP Actions host (*.convex.site), but set Better Auth baseURL in convex/auth.ts to your app URL (SITE_URL). Redirect handling improved in v0.2.11+, but social OAuth callbacks still depend on the correct baseURL.
Common mistake: setting Better Auth baseURL to convex.siteUrl / CONVEX_SITE_URL.
For Nuxt proxy setups, Better Auth baseURL should be your app origin (SITE_URL), while convex.siteUrl should point to Convex HTTP Actions.

Auth Operations

useConvexAuth() (Primary API)

Use useConvexAuth() for both reactive auth state and standard auth actions.
<script setup lang="ts">
const { signIn, signOut, isAuthenticated, user } = useConvexAuth()

async function signInWithEmail(email: string, password: string) {
  const { data, error } = await signIn.email({
    email,
    password,
  })

  if (error) {
    console.error('Sign in failed:', error.message)
  } else {
    navigateTo('/dashboard')
  }
}

async function handleSignOut() {
  await signOut()
  navigateTo('/')
}
</script>
  • signIn / signUp are client-only (safe to reference during SSR; they warn if called on the server)
  • client is available on useConvexAuth() as an advanced escape hatch for plugin-specific Better Auth APIs ::
What should I use?
  • Show user name/avatar: useConvexAuth().user
  • Sign in / sign up: useConvexAuth().signIn, useConvexAuth().signUp
  • Sign out: useConvexAuth().signOut
  • Better Auth plugin methods (admin, organization, ...): useConvexAuth().client or a custom typed client ::

Using Additional Better Auth Plugins

useConvexAuth().client returns the module-managed Better Auth client used by better-convex-nuxt. It is ideal for standard sign-in/sign-up flows, but it is not plugin-typed for arbitrary Better Auth plugins (for example admin, organization, etc.).If you need plugin-specific client methods like authClient.admin.listUsers(), create your own client instance on the frontend and include:
  • convexClient() to preserve Convex token sync
  • the Better Auth client plugin (for example adminClient())

Server: install the Better Auth plugin in convex/auth.ts

convex/auth.ts
import { convex } from '@convex-dev/better-auth/plugins'
import { admin } from 'better-auth/plugins'

plugins: [convex({ authConfig }), admin()]

Client: create a custom auth client for plugin methods

app/composables/useExtendedAuthClient.ts
import { convexClient } from '@convex-dev/better-auth/client/plugins'
import { createAuthClient } from 'better-auth/vue'
import { adminClient } from 'better-auth/client/plugins'

export function useExtendedAuthClient() {
  const authBaseURL = `${window.location.origin}/api/auth`

  return createAuthClient({
    // If you use the Nuxt auth proxy, keep this same-origin.
    // Better Auth client expects an absolute URL.
    baseURL: authBaseURL,
    plugins: [convexClient(), adminClient()],
    fetchOptions: { credentials: 'include' },
  })
}

Example: use plugin APIs + keep reactive logout

<script setup lang="ts">
const authClient = useExtendedAuthClient()
const { signOut } = useConvexAuth()

async function loadUsers() {
  const result = await authClient.admin.listUsers({
    query: { limit: 10 },
  })
  console.log(result)
}

async function handleSignOut() {
  // Prefer Convex-aware logout so local auth state updates immediately.
  await signOut()
  navigateTo('/')
}
</script>
Common mistake: calling authClient.signOut() directly in app code and expecting useConvexAuth().isAuthenticated to update immediately.
Prefer useConvexAuth().signOut() so Better Auth logout and Convex-local auth state stay in sync in the same tab.

Email/Password Sign In

<script setup lang="ts">
const { signIn } = useConvexAuth()

const email = ref('')
const password = ref('')
const error = ref<string | null>(null)
const loading = ref(false)

async function handleSignIn() {
  error.value = null
  loading.value = true

  try {
    const { error: authError } = await signIn.email({
      email: email.value,
      password: password.value,
    })

    if (authError) {
      error.value = authError.message
    } else {
      navigateTo('/dashboard')
    }
  } finally {
    loading.value = false
  }
}
</script>

<template>
  <form @submit.prevent="handleSignIn">
    <input v-model="email" type="email" placeholder="Email" />
    <input v-model="password" type="password" placeholder="Password" />
    <button :disabled="loading">
      {{ loading ? 'Signing in...' : 'Sign In' }}
    </button>
    <p v-if="error" class="error">{{ error }}</p>
  </form>
</template>

OAuth Sign In

<script setup lang="ts">
const { signIn } = useConvexAuth()

async function signInWithGoogle() {
  await signIn.social({
    provider: 'google',
    callbackURL: '/dashboard',
  })
}

async function signInWithGitHub() {
  await signIn.social({
    provider: 'github',
    callbackURL: '/dashboard',
  })
}
</script>

<template>
  <div class="oauth-buttons">
    <button @click="signInWithGoogle">Continue with Google</button>
    <button @click="signInWithGitHub">Continue with GitHub</button>
  </div>
</template>

Sign Up

<script setup lang="ts">
const { signUp } = useConvexAuth()

const name = ref('')
const email = ref('')
const password = ref('')
const error = ref<string | null>(null)

async function handleSignUp() {
  const { error: authError } = await signUp.email({
    name: name.value,
    email: email.value,
    password: password.value,
  })

  if (authError) {
    error.value = authError.message
  } else {
    navigateTo('/dashboard')
  }
}
</script>

<template>
  <form @submit.prevent="handleSignUp">
    <input v-model="name" placeholder="Name" />
    <input v-model="email" type="email" placeholder="Email" />
    <input v-model="password" type="password" placeholder="Password" />
    <button>Create Account</button>
    <p v-if="error" class="error">{{ error }}</p>
  </form>
</template>

Sign Out

<script setup lang="ts">
const { isAuthenticated, user, signOut } = useConvexAuth()

async function handleSignOut() {
  await signOut()
  navigateTo('/')
}
</script>

<template>
  <div v-if="isAuthenticated" class="user-menu">
    <span>{{ user?.name }}</span>
    <button @click="handleSignOut">Sign Out</button>
  </div>
</template>

Auth Components

Declarative components for rendering content based on authentication state.
ComponentShows content when...
<ConvexAuthLoading>Auth state is being determined
<ConvexAuthenticated>User is authenticated
<ConvexUnauthenticated>User is NOT authenticated
<ConvexAuthError>Auth error occurred (401/403 or token decode failure)

Basic Usage

<template>
  <ConvexAuthLoading>
    <div class="loading">Checking authentication...</div>
  </ConvexAuthLoading>

  <ConvexAuthenticated>
    <Dashboard />
  </ConvexAuthenticated>

  <ConvexUnauthenticated>
    <LoginPrompt />
  </ConvexUnauthenticated>
</template>

ConvexAuthLoading

Renders content while auth state is being determined.
<template>
  <ConvexAuthLoading>
    <div class="auth-loading">
      <Spinner />
      <p>Loading...</p>
    </div>
  </ConvexAuthLoading>
</template>

ConvexAuthenticated

Renders content when user is authenticated.
<template>
  <ConvexAuthenticated>
    <nav class="user-nav">
      <NuxtLink to="/dashboard">Dashboard</NuxtLink>
      <NuxtLink to="/settings">Settings</NuxtLink>
      <UserMenu />
    </nav>
  </ConvexAuthenticated>
</template>

ConvexUnauthenticated

Renders content when user is NOT authenticated.
<template>
  <ConvexUnauthenticated>
    <div class="guest-nav">
      <NuxtLink to="/auth/signin">Sign In</NuxtLink>
      <NuxtLink to="/auth/signup">Sign Up</NuxtLink>
    </div>
  </ConvexUnauthenticated>
</template>

ConvexAuthError

Renders content when authentication has failed (401/403 from token endpoint, or token decode error). Provides retry and error slot props.
<template>
  <ConvexAuthError v-slot="{ retry, error }">
    <div class="auth-error">
      <p>{{ error || 'Authentication failed. Please try again.' }}</p>
      <button @click="retry">Retry</button>
    </div>
  </ConvexAuthError>
</template>

Patterns

Header with Auth States

<template>
  <header class="app-header">
    <NuxtLink to="/" class="logo">MyApp</NuxtLink>

    <nav>
      <ConvexAuthLoading>
        <div class="skeleton-avatar" />
      </ConvexAuthLoading>

      <ConvexAuthenticated>
        <NuxtLink to="/dashboard">Dashboard</NuxtLink>
        <UserDropdown />
      </ConvexAuthenticated>

      <ConvexUnauthenticated>
        <NuxtLink to="/auth/signin">Sign In</NuxtLink>
        <NuxtLink to="/auth/signup" class="btn-primary"> Get Started </NuxtLink>
      </ConvexUnauthenticated>
    </nav>
  </header>
</template>

Protected Page Layout

layouts/dashboard.vue
<template>
  <div class="dashboard-layout">
    <ConvexAuthLoading>
      <div class="loading-screen">
        <Spinner size="lg" />
        <p>Loading your dashboard...</p>
      </div>
    </ConvexAuthLoading>

    <ConvexAuthenticated>
      <DashboardSidebar />
      <main class="dashboard-content">
        <slot />
      </main>
    </ConvexAuthenticated>

    <ConvexUnauthenticated>
      <div class="auth-required">
        <h1>Authentication Required</h1>
        <p>Please sign in to access this page.</p>
        <NuxtLink to="/auth/signin" class="btn">Sign In</NuxtLink>
      </div>
    </ConvexUnauthenticated>
  </div>
</template>

Protected Page with Redirect

<script setup lang="ts">
const { isAuthenticated, isPending } = useConvexAuth()

// Redirect if not authenticated
watch(
  () => ({
    isAuthenticated: isAuthenticated.value,
    isPending: isPending.value,
  }),
  ({ isAuthenticated, isPending }) => {
    if (!isPending && !isAuthenticated) {
      navigateTo('/auth/signin')
    }
  },
  { immediate: true },
)
</script>

<template>
  <div v-if="isPending">Loading...</div>
  <div v-else-if="isAuthenticated">
    <Dashboard />
  </div>
</template>

Route Middleware (Auth and Permissions)

Use useConvexAuth() for simple "signed in / signed out" guards in route middleware.
app/middleware/auth.ts
export default defineNuxtRouteMiddleware(() => {
  const { isAuthenticated, isPending } = useConvexAuth()

  if (isPending.value) return
  if (!isAuthenticated.value) {
    return navigateTo('/auth/signin')
  }
})
If you need to query Convex in route middleware (for role/permission checks), use one-shot APIs:
  • client navigation: useConvexCall
  • SSR navigation: serverConvexQuery
app/middleware/admin-only.ts
import { api } from '~~/convex/_generated/api'

export default defineNuxtRouteMiddleware(async () => {
  const context = import.meta.server
    ? await serverConvexQuery(api.auth.getPermissionContext, {})
    : await useConvexCall({ timeoutMs: 5000 }).query(api.auth.getPermissionContext, {})

  if (!context || context.role !== 'admin') {
    return navigateTo('/')
  }
})
useConvexQuery and useConvexPaginatedQuery are setup-scope composables and should not be used in route middleware/plugins.

Conditional Query Based on Auth

<script setup lang="ts">
const { isAuthenticated } = useConvexAuth()

// Only fetch when authenticated
const { data: profile } = await useConvexQuery(
  api.users.getProfile,
  computed(() => (isAuthenticated.value ? {} : 'skip')),
)
</script>

Conditional Feature

<template>
  <div class="comments-section">
    <h2>Comments</h2>

    <!-- Anyone can read comments -->
    <CommentList :postId="postId" />

    <!-- Only authenticated users can post -->
    <ConvexAuthenticated>
      <CommentForm :postId="postId" />
    </ConvexAuthenticated>

    <ConvexUnauthenticated>
      <p class="signin-prompt">
        <NuxtLink to="/auth/signin">Sign in</NuxtLink>
        to leave a comment.
      </p>
    </ConvexUnauthenticated>
  </div>
</template>

With SSR and ClientOnly

For proper SSR handling, wrap auth components in <ClientOnly>:
<template>
  <ClientOnly>
    <ConvexAuthLoading>
      <SkeletonHeader />
    </ConvexAuthLoading>

    <ConvexAuthenticated>
      <AuthenticatedHeader />
    </ConvexAuthenticated>

    <ConvexUnauthenticated>
      <GuestHeader />
    </ConvexUnauthenticated>

    <template #fallback>
      <SkeletonHeader />
    </template>
  </ClientOnly>
</template>

User Welcome Message

<script setup lang="ts">
const { user } = useConvexAuth()
</script>

<template>
  <ConvexAuthenticated>
    <div class="welcome">
      <img v-if="user?.image" :src="user.image" :alt="user.name" class="avatar" />
      <div>
        <p class="greeting">Welcome back,</p>
        <p class="name">{{ user?.name }}</p>
      </div>
    </div>
  </ConvexAuthenticated>
</template>

Composable vs Components

Auth components are syntax sugar over useConvexAuth():
<!-- Using components -->
<template>
  <ConvexAuthenticated>
    <Dashboard />
  </ConvexAuthenticated>
</template>

<!-- Equivalent with composable -->
<script setup>
const { isAuthenticated, isPending } = useConvexAuth()
</script>
<template>
  <Dashboard v-if="!isPending && isAuthenticated" />
</template>
When to use components:
  • Cleaner template syntax
  • Multiple auth states in one template
  • Layouts and common patterns
When to use composable:
  • Need access to user or token
  • Complex conditional logic
  • Programmatic auth checks

Complete Auth Page Example

pages/auth/signin.vue
<script setup lang="ts">
const { signIn, isAuthenticated, refreshAuth } = useConvexAuth()

// Redirect if already authenticated
watch(
  isAuthenticated,
  (value) => {
    if (value) navigateTo('/dashboard')
  },
  { immediate: true },
)

const email = ref('')
const password = ref('')
const error = ref<string | null>(null)
const loading = ref(false)

async function handleEmailSignIn() {
  error.value = null
  loading.value = true

  try {
    const { error: authError } = await signIn.email({
      email: email.value,
      password: password.value,
    })
    if (authError) {
      error.value = authError.message
    } else {
      await refreshAuth()
    }
  } finally {
    loading.value = false
  }
}

async function handleGoogleSignIn() {
  await signIn.social({
    provider: 'google',
    callbackURL: '/dashboard',
  })
}
</script>

<template>
  <div class="auth-page">
    <h1>Sign In</h1>

    <form @submit.prevent="handleEmailSignIn">
      <input v-model="email" type="email" placeholder="Email" required />
      <input v-model="password" type="password" placeholder="Password" required />
      <button type="submit" :disabled="loading">
        {{ loading ? 'Signing in...' : 'Sign In' }}
      </button>
    </form>

    <p v-if="error" class="error">{{ error }}</p>

    <div class="divider">or</div>

    <button @click="handleGoogleSignIn" class="oauth-btn">Continue with Google</button>

    <p class="signup-link">
      Don't have an account?
      <NuxtLink to="/auth/signup">Sign Up</NuxtLink>
    </p>
  </div>
</template>

API Reference

useConvexAuth Returns

PropertyTypeDescription
tokenReadonly<Ref<string | null>>JWT token for Convex auth
userReadonly<Ref<ConvexUser | null>>Authenticated user data
isAuthenticatedComputedRef<boolean>True when user is logged in
isPendingReadonly<Ref<boolean>>True during auth operations
authErrorReadonly<Ref<string | null>>Error message if auth failed (e.g., 401/403)
signOut() => Promise<void>Signs out from both Better Auth and Convex
refreshAuth() => Promise<void>Force refresh Convex auth state (fetches fresh token)

Additional useConvexAuth Properties

PropertyTypeDescription
clientAuthClient | nullRaw Better Auth client instance (null during SSR)
signInAuthClient['signIn']Better Auth sign-in methods (client-only)
signUpAuthClient['signUp']Better Auth sign-up methods (client-only)

ConvexUser Type

interface ConvexUser {
  id: string
  name: string
  email: string
  emailVerified?: boolean
  image?: string
  createdAt?: string
  updatedAt?: string
}

Which User Fields Layer Should I Use?

There are three different places where "extra user fields" can exist in a Better Auth + Convex app:
LayerExampleUsed byHow to add fields
Better Auth core schemauser.additionalFields.roleauthClient.useSession(), plugin endpoints, Better Auth DB recordsBetter Auth additionalFields + inferAdditionalFields(...)
Convex JWT claimsuseConvexAuth().user.roleNuxt UI/auth state convenience fieldsconvex({ jwt.definePayload }) + ConvexUser augmentation
Your app's Convex tablesusers.role, users.organizationIdAuthoritative permissions/business dataConvex schema + triggers/queries/mutations
Use the right layer for the job:
  • Need typed fields in Better Auth session/plugin responses: use Better Auth additionalFields.
  • Need fields on useConvexAuth().user: add JWT claims and extend ConvexUser.
  • Need authoritative roles/org membership: store/query them in Convex tables.

Better Auth Additional Fields for Sessions and Plugins

For Better Auth fields like user.additionalFields / session.additionalFields, use Better Auth's core schema config and client type inference.See also: Better Auth: Extending Core Schema
Common mistake: expecting plugin APIs on useConvexAuth().client to be strongly typed automatically.
For plugin methods (admin, organization, etc.) and typed additionalFields, create a custom client with plugin client adapters.

Server: define additional fields and export an auth type alias

convex/auth.ts
export const createAuth = (ctx: GenericCtx<DataModel>) => {
  return betterAuth({
    // ...your existing config
    database: authComponent.adapter(ctx),
    user: {
      additionalFields: {
        organizationId: { type: 'string', required: false },
        marketingOptIn: { type: 'boolean', required: false },
      },
    },
  })
}

// Type-only bridge for frontend inference (safe to import with `import type`)
export type AppAuth = ReturnType<typeof createAuth>

Client: infer additional fields on a custom auth client

app/composables/useExtendedAuthClient.ts
import { convexClient } from '@convex-dev/better-auth/client/plugins'
import { createAuthClient } from 'better-auth/vue'
import { inferAdditionalFields, adminClient } from 'better-auth/client/plugins'
import type { AppAuth } from '../../convex/auth'

export function useExtendedAuthClient() {
  const authBaseURL = `${window.location.origin}/api/auth`

  return createAuthClient({
    // Better Auth client expects an absolute URL.
    baseURL: authBaseURL,
    plugins: [convexClient(), inferAdditionalFields<AppAuth>(), adminClient()],
    fetchOptions: { credentials: 'include' },
  })
}
<script setup lang="ts">
const authClient = useExtendedAuthClient()
const session = authClient.useSession()

// Typed from Better Auth additionalFields (not from Convex JWT claims)
const orgId = computed(() => session.value.data?.user.organizationId)
const marketingOptIn = computed(() => session.value.data?.user.marketingOptIn)
</script>
Use import type { AppAuth } only. Do not runtime-import your server auth instance into client code.

Extending ConvexUser (Custom JWT Claims)

useConvexAuth().user is typed as ConvexUser. You can extend it with TypeScript module augmentation so app-specific fields like role, authId, or organizationId are recognized by your editor and type checker.
app/types/convex-user.d.ts
declare module 'better-convex-nuxt/dist/runtime/utils/types' {
  interface ConvexUser {
    role?: 'owner' | 'admin' | 'member' | 'viewer'
    authId?: string
    organizationId?: string
  }
}

export {}

Runtime values require JWT claims

Type augmentation only changes TypeScript. For user.role (or any custom field) to exist at runtime, that field must be included in the Convex JWT payload.
convex/auth.ts
import { convex } from '@convex-dev/better-auth/plugins'

plugins: [
  convex({
    authConfig,
    jwt: {
      definePayload: ({ user }) => ({
        // Keep standard fields used by useConvexAuth()
        name: user.name,
        email: user.email,
        emailVerified: user.emailVerified,
        image: user.image ?? undefined,

        // Custom claims
        authId: user.id,
        role: 'member', // example only
      }),
    },
  }),
]

Using extended fields in components

<script setup lang="ts">
const { user, refreshAuth } = useConvexAuth()

const role = computed(() => user.value?.role)

async function refreshClaims() {
  await refreshAuth()
}
</script>

<template>
  <p>JWT role claim: {{ role || '(no claim)' }}</p>
  <button @click="refreshClaims">Refresh Auth Claims</button>
</template>

Best Practices for Custom Claims

  • Treat useConvexAuth().user as identity + convenience claims (good for UI display and lightweight client checks).
  • Treat a Convex query (for example usePermissions() / api.auth.getPermissionContext) as the authoritative source for roles and org membership.
  • Keep JWT payload generation cheap and deterministic. Avoid expensive work in token minting.
  • Avoid calling Convex functions from jwt.definePayload() to build claims. Prefer data already available on the auth user/session, or fetch authoritative role/org data separately via Convex queries.
  • If claims can change during a session (e.g. role changes), call useConvexAuth().refreshAuth() after the change to fetch a fresh token.
  • Never rely on frontend/JWT claims alone for backend authorization. Always enforce permissions inside Convex functions.

Notes

  • Components use useConvexAuth() internally
  • Auth state is pre-populated during SSR (no flash)
  • Components render nothing (not even a wrapper element) when condition isn't met
  • All four components can coexist in the same template

Skipping Auth Checks

For marketing pages that never need authentication, you can skip auth checks entirely to avoid unnecessary requests.

Config-Based Skip

Skip auth for entire route patterns in nuxt.config.ts:
nuxt.config.ts
export default defineNuxtConfig({
  convex: {
    url: process.env.CONVEX_URL,
    skipAuthRoutes: [
      '/', // Home page
      '/pricing', // Pricing page
      '/about', // About page
      '/docs/**', // All docs pages
      '/blog/**', // All blog pages
    ],
  },
})
Supported patterns:
  • Exact match: /about matches only /about
  • Single wildcard: /blog/* matches /blog/post but not /blog/post/comments
  • Double wildcard: /docs/** matches /docs, /docs/guide, /docs/guide/auth

Page-Level Skip

Skip auth for individual pages using definePageMeta:
pages/landing.vue
<script setup lang="ts">
definePageMeta({
  skipConvexAuth: true,
})
</script>

<template>
  <div>
    <h1>Welcome to Our App</h1>
    <!-- Auth checks skipped for this page -->
  </div>
</template>

When to Skip Auth

Skip authDon't skip auth
Marketing/landing pagesDashboard
Public blog postsUser settings
DocumentationProtected content
Pricing pageAdmin panels
Skipping auth saves one API request per page load, reducing e.g. Vercel function invocations and Convex database reads.