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
}| Property | Type | Description |
|---|---|---|
authState | 'pending' | 'authenticated' | 'unauthenticated' | 'error' | Current auth state |
isAuthenticated | boolean | Whether the user is authenticated |
userId | string | undefined | The authenticated user's ID from the JWT |
setToken | (token: string) => void | Set a new auth token |
clearAuth | () => void | Clear 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' }):
| Table | Purpose |
|---|---|
_auth_users | User profiles (email, name, email_verified) |
_auth_accounts | OAuth provider connections and passwords |
_auth_sessions | Active sessions and expiry tracking |
_auth_verification_codes | Email 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,
})
},
})| Property | Type | Description |
|---|---|---|
ctx.auth.userId | string | The user's ID from the JWT sub claim |
ctx.auth.claims | Record<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:
| Claim | Required | Description |
|---|---|---|
sub | Yes | User ID, available as ctx.auth.userId |
exp | Yes | Expiration timestamp (Unix seconds) |
iat | No | Issued-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).