Valet

Auth

Authentication tokens, token refresh, and accessing auth in handlers.

// App.tsx
import { ValetProvider } from './_generated/valet/react'

function App() {
    return (
        <ValetProvider
            url={process.env.VALET_PROJECT_URL!}
            authToken={async () => {
                const response = await fetch('/api/auth/token')
                const { token } = await response.json()
                return token
            }}
        >
            <TodoApp />
        </ValetProvider>
    )
}

Pass an authToken prop to ValetProvider to authenticate the connection. The token is a JWT that the server validates. Sync filters and mutation handlers access the authenticated user through ctx.auth.

Passing auth tokens

Static token

Pass a string for a token you already have:

// App.tsx
import { ValetProvider } from './_generated/valet/react'

function App({ token }: { token: string }) {
    return (
        <ValetProvider url={process.env.VALET_PROJECT_URL!} authToken={token}>
            <TodoApp />
        </ValetProvider>
    )
}

Async token function

Pass an async function to fetch or refresh the token on demand. Valet calls this function when the connection is established and when the token needs refreshing:

// App.tsx
import { ValetProvider } from './_generated/valet/react'

function App() {
    return (
        <ValetProvider
            url={process.env.VALET_PROJECT_URL!}
            authToken={async () => {
                const response = await fetch('/api/auth/token')
                const { token } = await response.json()
                return token
            }}
        >
            <TodoApp />
        </ValetProvider>
    )
}

Token refresh

Set authRefreshBuffer to control how far in advance of token expiry Valet requests a new token. The value is in milliseconds:

// App.tsx
import { ValetProvider } from './_generated/valet/react'

function App() {
    return (
        <ValetProvider
            url={process.env.VALET_PROJECT_URL!}
            authToken={async () => {
                const response = await fetch('/api/auth/token')
                const { token } = await response.json()
                return token
            }}
            authRefreshBuffer={60_000}
        >
            <TodoApp />
        </ValetProvider>
    )
}

With authRefreshBuffer={60_000}, Valet calls the authToken function 60 seconds before the current token expires.

useValetAuth hook

The useValetAuth() hook provides access to the current auth state and methods to update it:

// UserMenu.tsx
import { useValetAuth } from './_generated/valet/react'

function UserMenu() {
    const { authState, isAuthenticated, userId, setToken, clearAuth } =
        useValetAuth()

    if (!isAuthenticated) {
        return <button onClick={() => login()}>Sign in</button>
    }

    return (
        <div>
            <span>User: {userId}</span>
            <button onClick={() => clearAuth()}>Sign out</button>
        </div>
    )
}

async function login() {
    // Your login flow
}
PropertyTypeDescription
authState'pending' | 'authenticated' | 'unauthenticated' | 'error'Current auth state
isAuthenticatedbooleanWhether the user is authenticated
userIdstring | undefinedThe authenticated user's ID from the JWT
setToken(token: string) => voidSet a new auth token
clearAuth() => voidClear the current auth token and disconnect

Auth tables

Valet provides a set of built-in tables for managing users, accounts, sessions, and verification codes. Spread authTables into your schema to enable them:

// valet/schema.ts
import { defineSchema, defineTable, v, authTables } from 'valet-dev/server'

export default defineSchema({
    ...authTables,
    todos: defineTable({
        title: v.string(),
        userId: v.string(),
    }).sync({
        filter: (q, ctx) => q.eq('userId', ctx.auth.userId),
        mode: 'full',
    }),
})

This adds four server-only tables (all with sync: { mode: 'none' }):

TablePurpose
_auth_usersUser profiles (email, name, email_verified)
_auth_accountsOAuth provider connections and passwords
_auth_sessionsActive sessions and expiry tracking
_auth_verification_codesEmail verification and password reset codes

These tables are never synced to clients. They are only accessible in server-side queries and mutations via ctx.db.

If your app uses an external auth provider (Firebase, Clerk, etc.) and only needs JWT validation via ctx.auth, you can omit authTables from your schema.

Accessing auth in handlers

The handler's context object (ctx) includes ctx.auth with the authenticated user's information:

// valet/todos.ts
import { defineMutation, v } from './_generated/valet/api'

export const create = defineMutation({
    args: { title: v.string() },
    handler: async (ctx, args) => {
        if (!ctx.auth) {
            throw new Error('Not authenticated')
        }

        return ctx.db.insert('todos', {
            title: args.title,
            completed: false,
            userId: ctx.auth.userId,
        })
    },
})
PropertyTypeDescription
ctx.auth.userIdstringThe user's ID from the JWT sub claim
ctx.auth.claimsRecord<string, any>All custom claims from the JWT

If the user is not authenticated, ctx.auth is undefined.

JWT format

Valet expects a standard JWT with the following claims:

ClaimRequiredDescription
subYesUser ID, available as ctx.auth.userId
expYesExpiration timestamp (Unix seconds)
iatNoIssued-at timestamp

Additional claims are accessible through ctx.auth.claims. You can include any custom data your app needs (team IDs, roles, permissions).

Common patterns

Firebase Auth

// App.tsx
import { ValetProvider } from './_generated/valet/react'
import { getAuth } from 'firebase/auth'

function App() {
    return (
        <ValetProvider
            url={process.env.VALET_PROJECT_URL!}
            authToken={async () => {
                const auth = getAuth()
                const user = auth.currentUser
                if (!user) return null
                return user.getIdToken()
            }}
            authRefreshBuffer={300_000}
        >
            <TodoApp />
        </ValetProvider>
    )
}

Clerk

// App.tsx
import { ValetProvider } from './_generated/valet/react'
import { useAuth } from '@clerk/clerk-react'

function ValetWrapper({ children }: { children: React.ReactNode }) {
    const { getToken } = useAuth()

    return (
        <ValetProvider
            url={process.env.VALET_PROJECT_URL!}
            authToken={async () => {
                const token = await getToken({ template: 'valet' })
                return token
            }}
        >
            {children}
        </ValetProvider>
    )
}

Create a JWT template named valet in the Clerk dashboard. Include the sub claim (Clerk sets this automatically) and any custom claims your sync filters need.

Custom JWT

// App.tsx
import { ValetProvider } from './_generated/valet/react'

function App() {
    return (
        <ValetProvider
            url={process.env.VALET_PROJECT_URL!}
            authToken={async () => {
                const response = await fetch('/api/auth/valet-token', {
                    credentials: 'include',
                })
                if (!response.ok) return null
                const { token } = await response.json()
                return token
            }}
        >
            <TodoApp />
        </ValetProvider>
    )
}

Your /api/auth/valet-token endpoint should issue a JWT with at least sub and exp claims. Sign it with a secret shared with your Valet project (set via bunx valet-dev env set JWT_SECRET=your-secret).

On this page