ValetAlpha

Auth

Set up authentication with built-in email/password, GitHub, Google, or an external provider.

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

function App() {
    return (
        <ValetProvider url={process.env.VALET_PROJECT_URL!} auth>
            <AuthGate />
        </ValetProvider>
    )
}

function AuthGate() {
    const { isAuthenticated, isLoading } = useAuth()

    if (isLoading) return <div>Loading...</div>
    if (!isAuthenticated) return <LoginForm />

    return <TodoApp />
}

Valet supports three approaches to authentication. Pick the one that fits your app:

ApproachWhen to useWhat you need
OAuth onlyYour app signs in with GitHub, Google, or bothClient IDs + secrets from the provider
Email and passwordYour app has its own sign-up form with email/passwordNothing beyond Valet
External providerYou already use Firebase, Clerk, or another auth serviceYour existing auth setup

You can also combine OAuth and email/password — they share the same setup and user table, and accounts with matching emails are linked automatically.

Common setup

Every built-in auth approach (OAuth and email/password) shares these steps. If you're using an external provider, skip to External auth providers.

1. Add auth tables to your schema

Run auth init to add the built-in auth tables to your project:

# terminal
bunx valet-dev auth init

To include OAuth providers from the start, pass them with --provider:

# terminal
bunx valet-dev auth init --provider github,google

This adds authTables to your schema.ts and creates an auth.ts file with a defineAuth configuration. Without --provider, auth.ts defaults to email/password only (empty providers list). If you prefer to do it manually, add the authTables import and spread it into your schema:

// 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 (never synced to clients):

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

2. Set the auth secret

Set VALET_AUTH_SECRET as a server-side environment variable. This secret signs and verifies JWT tokens:

# terminal
bunx valet-dev env set VALET_AUTH_SECRET=$(openssl rand -base64 32)

Without VALET_AUTH_SECRET, the server rejects all auth tokens by default. During development, VALET_AUTH_SECRET is still required for built-in auth to work — the auth init next steps will remind you to set it.

3. Wrap your app in ValetProvider with auth

Pass the auth prop to ValetProvider to enable built-in auth. This handles sign-in state, token management, and WebSocket authentication in a single component:

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

function App() {
    return (
        <ValetProvider url={process.env.VALET_PROJECT_URL!} auth>
            <AuthGate />
        </ValetProvider>
    )
}

function AuthGate() {
    const { isAuthenticated, isLoading } = useAuth()

    if (isLoading) return <div>Loading...</div>
    if (!isAuthenticated) return <LoginForm />

    return <MyApp />
}

The auth prop automatically wires up token management — the WebSocket connection waits for auth to resolve before connecting. You can also pass auth={{ storage: customStorage }} for custom token storage.

Advanced: manual provider nesting

If you need more control over the auth and sync lifecycle (e.g. different URLs for auth and sync), you can nest ValetAuthProvider and ValetProvider manually:

import { ValetAuthProvider, useAuth } from 'valet-dev/react'
import { ValetProvider } from './_generated/valet/react'

function App() {
    return (
        <ValetAuthProvider url="http://localhost:3000">
            <AuthGate />
        </ValetAuthProvider>
    )
}

function AuthGate() {
    const { isAuthenticated, isLoading, authClient } = useAuth()

    if (isLoading) return <div>Loading...</div>
    if (!isAuthenticated) return <LoginForm />

    return (
        <ValetProvider
            url="ws://localhost:3000/ws"
            authToken={authClient.getTokenProvider()}
        >
            <MyApp />
        </ValetProvider>
    )
}

authClient.getTokenProvider() returns a function that provides the current JWT to ValetProvider for WebSocket authentication.

After completing these steps, continue with one or both of the sign-in methods below.


OAuth (GitHub, Google)

Use OAuth if you want users to sign in with their existing GitHub or Google account. No password form needed — just a button.

How OAuth works

When a user clicks "Continue with GitHub", the flow is:

  1. Client → Valet server: The client generates a state token for CSRF protection and redirects to your Valet server's /auth/oauth/github/authorize endpoint.
  2. Valet server → GitHub: The server redirects to GitHub's authorization page.
  3. GitHub → Valet server: After the user authorizes, GitHub redirects back to your Valet server's callback endpoint (configured in your GitHub OAuth app settings).
  4. Valet server → Client: The server exchanges the code for tokens, creates or links the user account, and redirects to VALET_SITE_URL with credentials and the state token in the URL fragment.
  5. Client verifies: The client checks that the state token matches, then stores the credentials.

You need to tell the server where your client app lives so it can redirect back after the flow completes:

# terminal — set this to your client app's URL
bunx valet-dev env set VALET_SITE_URL=http://localhost:8081

For production, set it to your deployed frontend URL (e.g., https://myapp.com).

GitHub

  1. Create a GitHub OAuth App at github.com/settings/developers.

  2. Set the Authorization callback URL to your Valet server's callback endpoint:

    https://your-project.fly.valet.host/auth/oauth/github/callback

    This is your Valet server URL (the same one in VALET_PROJECT_URL), not your client app URL.

  3. Set the client credentials on the server:

    # terminal
    bunx valet-dev env set GITHUB_CLIENT_ID=your_client_id
    bunx valet-dev env set GITHUB_CLIENT_SECRET=your_client_secret

    valet-dev env set pushes secrets directly to the server's encrypted env store — you don't need to add them to a local .env file. The server reads from the env store first, then falls back to process environment variables.

  4. Add a sign-in button:

    import { useAuth } from 'valet-dev/react'
    
    function LoginForm() {
        const { signInWithOAuth } = useAuth()
    
        return (
            <button onClick={() => signInWithOAuth('github')}>
                Continue with GitHub
            </button>
        )
    }

The user is redirected to GitHub, authorizes your app, and is redirected back signed in. ValetAuthProvider handles the callback automatically.

Google

  1. Create OAuth credentials in the Google Cloud Console.

  2. Add your Valet server's callback URL to Authorized redirect URIs:

    https://your-project.fly.valet.host/auth/oauth/google/callback
  3. Set the credentials:

    # terminal
    bunx valet-dev env set GOOGLE_CLIENT_ID=your_client_id
    bunx valet-dev env set GOOGLE_CLIENT_SECRET=your_client_secret
  4. Add a sign-in button:

    <button onClick={() => signInWithOAuth('google')}>
        Continue with Google
    </button>

OAuth-only example

If your app only uses OAuth (no email/password), the login form is just a button or two:

import { useAuth } from 'valet-dev/react'

function LoginForm() {
    const { signInWithOAuth } = useAuth()

    return (
        <div>
            <h2>Sign in to continue</h2>
            <button onClick={() => signInWithOAuth('github')}>
                Continue with GitHub
            </button>
            <button onClick={() => signInWithOAuth('google')}>
                Continue with Google
            </button>
        </div>
    )
}

Email and password

Use email/password if you want users to create accounts with a traditional sign-up form. No external service or API keys needed.

There is no additional server setup beyond the common setup — email/password auth works as soon as authTables and VALET_AUTH_SECRET are configured.

Sign-up and sign-in

Use signUp to create an account and signIn to authenticate:

import { useState } from 'react'
import { useAuth } from 'valet-dev/react'

function LoginForm() {
    const { signUp, signIn } = useAuth()
    const [email, setEmail] = useState('')
    const [password, setPassword] = useState('')
    const [error, setError] = useState<string | null>(null)

    const handleSignIn = async (e: React.FormEvent) => {
        e.preventDefault()
        try {
            await signIn({ email, password })
        } catch (err: any) {
            setError(err.message)
        }
    }

    const handleSignUp = async () => {
        try {
            await signUp({ email, password, name: 'Alice' })
        } catch (err: any) {
            setError(err.message)
        }
    }

    return (
        <form onSubmit={handleSignIn}>
            <input type="email" value={email} onChange={(e) => setEmail(e.target.value)} />
            <input type="password" value={password} onChange={(e) => setPassword(e.target.value)} />
            {error && <p>{error}</p>}
            <button type="submit">Sign In</button>
            <button type="button" onClick={handleSignUp}>Sign Up</button>
        </form>
    )
}

Passwords must be at least 8 characters. They are hashed with Argon2id on the server.


Combining methods

You can offer both OAuth and email/password on the same login form. They share the same user table — if a user signs up with email/password and later signs in with an OAuth provider that has the same email, Valet automatically links the accounts. The user can then sign in with either method.

import { useState } from 'react'
import { useAuth } from 'valet-dev/react'

function LoginForm() {
    const { signIn, signUp, signInWithOAuth } = useAuth()
    const [email, setEmail] = useState('')
    const [password, setPassword] = useState('')

    return (
        <div>
            <button onClick={() => signInWithOAuth('github')}>
                Continue with GitHub
            </button>

            <hr />

            <form onSubmit={async (e) => {
                e.preventDefault()
                await signIn({ email, password })
            }}>
                <input type="email" value={email} onChange={(e) => setEmail(e.target.value)} />
                <input type="password" value={password} onChange={(e) => setPassword(e.target.value)} />
                <button type="submit">Sign In</button>
                <button type="button" onClick={() => signUp({ email, password })}>
                    Sign Up
                </button>
            </form>
        </div>
    )
}

External auth providers

If you already use Clerk, Firebase, Auth0, or another auth service, you can validate their tokens directly. The Valet server fetches the provider's public keys via JWKS and verifies RS256/ES256 signatures natively — no proxy backend needed.

# terminal — scaffolds auth.ts with a customJwt template
bunx valet-dev auth init --external

Setup

  1. Configure customJwt() in your valet/auth.ts with your provider's issuer and JWKS URL:

    // valet/auth.ts
    import { defineAuth, customJwt } from 'valet-dev/server'
    
    export default defineAuth({
        providers: [
            customJwt({
                issuer: 'https://your-provider.example.com',
                jwksUrl: 'https://your-provider.example.com/.well-known/jwks.json',
            })
        ]
    })

    The issuer must match the iss claim in your provider's tokens. The jwksUrl is where the server fetches public keys for signature verification. These are standard OIDC values — every provider publishes them.

  2. Pass the provider's token to ValetProvider:

    import { ValetProvider } from './_generated/valet/react'
    
    function App() {
        return (
            <ValetProvider
                url={process.env.VALET_PROJECT_URL!}
                authToken={async () => {
                    // Get the token from your auth provider
                    return await getTokenFromYourProvider()
                }}
            >
                <TodoApp />
            </ValetProvider>
        )
    }
  3. Deploy. The server fetches JWKS keys on deploy and validates tokens against them.

    # terminal
    bunx valet-dev deploy

customJwt options

customJwt({
    issuer: 'https://...',          // Required — expected iss claim
    jwksUrl: 'https://.../.well-known/jwks.json', // Required — JWKS endpoint
    audience: 'my-app-id',          // Optional — expected aud claim
    algorithm: 'RS256',             // Optional — RS256 (default) or ES256
})

Clerk

// valet/auth.ts
import { defineAuth, customJwt } from 'valet-dev/server'

export default defineAuth({
    providers: [
        customJwt({
            issuer: 'https://your-app.clerk.accounts.dev',
            jwksUrl: 'https://your-app.clerk.accounts.dev/.well-known/jwks.json',
        })
    ]
})
// 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={() => getToken()}
        >
            {children}
        </ValetProvider>
    )
}

Firebase Auth

// valet/auth.ts
import { defineAuth, customJwt } from 'valet-dev/server'

export default defineAuth({
    providers: [
        customJwt({
            issuer: 'https://securetoken.google.com/YOUR_PROJECT_ID',
            jwksUrl: 'https://www.googleapis.com/service_accounts/v1/jwk/securetoken@system.gserviceaccount.com',
            audience: 'YOUR_PROJECT_ID',
        })
    ]
})
// 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 user = getAuth().currentUser
                if (!user) return null
                return await user.getIdToken()
            }}
            authRefreshBuffer={300_000}
        >
            <TodoApp />
        </ValetProvider>
    )
}

Auth0

// valet/auth.ts
import { defineAuth, customJwt } from 'valet-dev/server'

export default defineAuth({
    providers: [
        customJwt({
            issuer: 'https://your-tenant.auth0.com/',
            jwksUrl: 'https://your-tenant.auth0.com/.well-known/jwks.json',
            audience: 'your-api-identifier',
        })
    ]
})

Mixing built-in and external auth

You can use customJwt() alongside built-in OAuth providers. The server tries JWKS validation first (matching by issuer), then falls back to HMAC for built-in auth tokens:

// valet/auth.ts
import { defineAuth, github, customJwt } from 'valet-dev/server'

export default defineAuth({
    providers: [
        github(),
        customJwt({
            issuer: 'https://your-app.clerk.accounts.dev',
            jwksUrl: 'https://your-app.clerk.accounts.dev/.well-known/jwks.json',
        })
    ]
})

Token refresh

Valet can automatically refresh tokens before they expire. Set authRefreshBuffer to control how far in advance (in milliseconds) the authToken function is called again:

<ValetProvider
    url={process.env.VALET_PROJECT_URL!}
    authToken={async () => {
        return await getTokenFromYourProvider()
    }}
    authRefreshBuffer={60_000}
>

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


Accessing auth in handlers

The handler context object (ctx) includes ctx.auth with the authenticated user's information. This works the same way regardless of which auth approach you use. For a deeper dive into userId types, when ctx.auth is available, and guarding mutations, see User-Scoped Data.

// 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.

Sync filters

Use ctx.auth in sync filters to control which rows sync to each user:

// valet/schema.ts
todos: defineTable({
    title: v.string(),
    userId: v.string(),
}).sync({
    filter: (q, ctx) => q.eq('userId', ctx.auth.userId),
    mode: 'full',
}),

Each user's device only receives documents that match their filter.


Reference

useAuth hook

The useAuth() hook is available inside ValetAuthProvider. It provides the current auth state and methods for sign-in and sign-out:

import { useAuth } from 'valet-dev/react'

function Profile() {
    const { user, isAuthenticated, isLoading, signOut } = useAuth()

    if (isLoading) return <div>Loading...</div>
    if (!isAuthenticated) return <div>Not signed in</div>

    return (
        <div>
            <p>{user?.email}</p>
            <button onClick={signOut}>Sign Out</button>
        </div>
    )
}
PropertyTypeDescription
userAuthUser | nullThe current user (id, email, name, emailVerified)
state'pending' | 'authenticated' | 'unauthenticated'Current auth state
isAuthenticatedbooleanWhether the user is signed in
isLoadingbooleanWhether auth state is being restored from storage
signUp(params: { email, password, name? }) => Promise<void>Create an account with email and password
signIn(params: { email, password }) => Promise<void>Sign in with email and password
signInWithOAuth(provider: string, redirectUri?) => voidStart an OAuth flow (redirects to provider)
signOut() => Promise<void>Sign out and clear the session
authClientValetAuthClientThe underlying client instance

useValetAuth hook

The useValetAuth() hook (distinct from useAuth()) provides the WebSocket connection's auth state. Use it inside ValetProvider to check if the sync connection is authenticated:

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

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

    if (!isAuthenticated) {
        return <div>Not connected</div>
    }

    return <div>Syncing as {userId}</div>
}
PropertyTypeDescription
authStateAuthStateThe full auth state object
isAuthenticatedbooleanWhether the WebSocket connection is authenticated
isPendingbooleanWhether the auth token is being validated
isErrorbooleanWhether authentication failed
userIdstring | undefinedThe authenticated user's ID
errorSyncError | undefinedThe authentication error, if any
setToken(token: TokenProvider) => voidSet a new auth token and re-authenticate
clearAuth() => voidClear the auth token and disconnect

JWT format

Valet expects a standard JWT with these claims:

ClaimRequiredDescription
subYesUser ID, available as ctx.auth.userId
expYesExpiration timestamp (Unix seconds)
iatNoIssued-at timestamp
sidNoSession ID (set automatically by built-in auth)

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

Environment variables

VariableRequiredDescription
VALET_AUTH_SECRETYes (built-in auth)HMAC secret for signing and verifying JWTs. Not needed when using only customJwt().
VALET_SITE_URLFor OAuthYour client app's URL. The server redirects here after OAuth completes (e.g., http://localhost:8081 for dev, https://myapp.com for prod).
VALET_DEVNoSet to 1 or true to enable development mode (accepts any token). Equivalent to --dev flag.
GITHUB_CLIENT_IDFor GitHub OAuthGitHub OAuth app client ID
GITHUB_CLIENT_SECRETFor GitHub OAuthGitHub OAuth app client secret
GOOGLE_CLIENT_IDFor Google OAuthGoogle OAuth client ID
GOOGLE_CLIENT_SECRETFor Google OAuthGoogle OAuth client secret

Set these with the valet-dev env command:

# terminal
bunx valet-dev env set VALET_AUTH_SECRET=$(openssl rand -base64 32)
bunx valet-dev env set GITHUB_CLIENT_ID=your_id
bunx valet-dev env set GITHUB_CLIENT_SECRET=your_secret

On this page