or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

docs

advanced.mdauth.mddatabase.mdindex.mdnextjs.mdreact.mdschema.mdserver-functions.mdvalues-validators.md
tile.json

auth.mddocs/

Authentication

Server-Side Auth

Access User Identity

import { query, mutation } from './_generated/server';

// Check authentication
export const getCurrentUser = query({
  handler: async (ctx) => {
    const identity = await ctx.auth.getUserIdentity();
    if (!identity) return null;

    return {
      id: identity.tokenIdentifier,
      subject: identity.subject,
      issuer: identity.issuer,
      name: identity.name,
      email: identity.email,
      emailVerified: identity.emailVerified,
    };
  },
});

// Require authentication
export const createPost = mutation({
  args: { title: v.string(), body: v.string() },
  handler: async (ctx, args) => {
    const identity = await ctx.auth.getUserIdentity();
    if (!identity) throw new Error('Not authenticated');

    return await ctx.db.insert('posts', {
      title: args.title,
      body: args.body,
      authorId: identity.tokenIdentifier,
      authorName: identity.name ?? 'Anonymous',
    });
  },
});

// Authorization by role
export const deleteUser = mutation({
  args: { userId: v.id('users') },
  handler: async (ctx, args) => {
    const identity = await ctx.auth.getUserIdentity();
    if (!identity) throw new Error('Not authenticated');

    const role = identity.role as string | undefined;
    if (role !== 'admin') throw new Error('Not authorized');

    await ctx.db.delete(args.userId);
  },
});

User Identity Type

interface UserIdentity {
  tokenIdentifier: string;  // {issuer}|{subject}
  subject: string;          // Unique user ID within issuer
  issuer: string;           // Auth provider URL
  name?: string;
  email?: string;
  emailVerified?: boolean;
  phoneNumber?: string;
  phoneNumberVerified?: boolean;
  pictureUrl?: string;
  givenName?: string;
  familyName?: string;
  nickname?: string;
  updatedAt?: number;
  [key: string]: unknown;   // Custom claims
}

Common Patterns

// Store user on first login
export const syncUser = mutation({
  handler: async (ctx) => {
    const identity = await ctx.auth.getUserIdentity();
    if (!identity) throw new Error('Not authenticated');

    const existing = await ctx.db
      .query('users')
      .withIndex('by_token_id', q => q.eq('tokenIdentifier', identity.tokenIdentifier))
      .unique();

    if (existing) {
      await ctx.db.patch(existing._id, {
        name: identity.name,
        email: identity.email,
        lastSeen: Date.now(),
      });
      return existing._id;
    } else {
      return await ctx.db.insert('users', {
        tokenIdentifier: identity.tokenIdentifier,
        name: identity.name ?? 'Anonymous',
        email: identity.email,
        createdAt: Date.now(),
      });
    }
  },
});

// Get user-specific data
export const getMyPosts = query({
  handler: async (ctx) => {
    const identity = await ctx.auth.getUserIdentity();
    if (!identity) return [];

    return await ctx.db
      .query('posts')
      .withIndex('by_author', q => q.eq('authorId', identity.tokenIdentifier))
      .collect();
  },
});

// Optional authentication
export const listPosts = query({
  handler: async (ctx) => {
    const identity = await ctx.auth.getUserIdentity();

    if (identity) {
      // Show all posts for authenticated users
      return await ctx.db.query('posts').collect();
    } else {
      // Show only public posts
      return await ctx.db.query('posts')
        .filter(q => q.eq(q.field('isPublic'), true))
        .collect();
    }
  },
});

React Integration

With Clerk

// Setup
import { ConvexProviderWithClerk } from 'convex/react-clerk';
import { ConvexReactClient } from 'convex/react';
import { ClerkProvider, useAuth } from '@clerk/clerk-react';

const convex = new ConvexReactClient(process.env.REACT_APP_CONVEX_URL!);

function App() {
  return (
    <ClerkProvider publishableKey={process.env.REACT_APP_CLERK_KEY!}>
      <ConvexProviderWithClerk client={convex} useAuth={useAuth}>
        <YourApp />
      </ConvexProviderWithClerk>
    </ClerkProvider>
  );
}

// Use Clerk hooks
import { useUser } from '@clerk/clerk-react';
import { SignInButton, SignOutButton } from '@clerk/clerk-react';

function Profile() {
  const { isSignedIn, user } = useUser();

  if (!isSignedIn) return <SignInButton />;

  return (
    <div>
      <h1>Welcome, {user.firstName}</h1>
      <SignOutButton />
    </div>
  );
}

With Auth0

// Setup
import { ConvexProviderWithAuth0 } from 'convex/react-auth0';
import { ConvexReactClient } from 'convex/react';
import { Auth0Provider } from '@auth0/auth0-react';

const convex = new ConvexReactClient(process.env.REACT_APP_CONVEX_URL!);

function App() {
  return (
    <Auth0Provider
      domain={process.env.REACT_APP_AUTH0_DOMAIN!}
      clientId={process.env.REACT_APP_AUTH0_CLIENT_ID!}
      authorizationParams={{
        redirect_uri: window.location.origin,
      }}
    >
      <ConvexProviderWithAuth0 client={convex}>
        <YourApp />
      </ConvexProviderWithAuth0>
    </Auth0Provider>
  );
}

// Use Auth0 hooks
import { useAuth0 } from '@auth0/auth0-react';

function Profile() {
  const { isAuthenticated, user, loginWithRedirect, logout } = useAuth0();

  if (!isAuthenticated) {
    return <button onClick={() => loginWithRedirect()}>Log In</button>;
  }

  return (
    <div>
      <h1>Welcome, {user?.name}</h1>
      <button onClick={() => logout()}>Log Out</button>
    </div>
  );
}

Conditional Rendering

import { Authenticated, Unauthenticated, AuthLoading } from 'convex/react';

function App() {
  return (
    <div>
      <AuthLoading>
        <div>Loading authentication...</div>
      </AuthLoading>

      <Authenticated>
        <Dashboard />
        <LogoutButton />
      </Authenticated>

      <Unauthenticated>
        <LandingPage />
        <LoginButton />
      </Unauthenticated>
    </div>
  );
}

// Navigation
function Navigation() {
  return (
    <nav>
      <a href="/">Home</a>
      <Authenticated>
        <a href="/dashboard">Dashboard</a>
        <a href="/profile">Profile</a>
      </Authenticated>
      <Unauthenticated>
        <a href="/about">About</a>
      </Unauthenticated>
    </nav>
  );
}

Custom Auth Provider

import { ConvexProviderWithAuth, ConvexReactClient } from 'convex/react';

const convex = new ConvexReactClient(process.env.REACT_APP_CONVEX_URL!);

function useMyAuth() {
  const [isLoading, setIsLoading] = useState(true);
  const [isAuthenticated, setIsAuthenticated] = useState(false);

  const fetchAccessToken = async ({ forceRefreshToken }) => {
    // Your logic to get JWT token
    return await myAuthService.getToken(forceRefreshToken);
  };

  useEffect(() => {
    // Initialize auth state
    myAuthService.onAuthChange((authenticated) => {
      setIsAuthenticated(authenticated);
      setIsLoading(false);
    });
  }, []);

  return { isLoading, isAuthenticated, fetchAccessToken };
}

function App() {
  return (
    <ConvexProviderWithAuth client={convex} useAuth={useMyAuth}>
      <YourApp />
    </ConvexProviderWithAuth>
  );
}

Next.js Integration

Server Components with Clerk

import { auth } from '@clerk/nextjs';
import { fetchQuery, preloadQuery } from 'convex/nextjs';
import { api } from '@/convex/_generated/api';

// Fetch with auth
export default async function PrivatePage() {
  const { getToken } = auth();
  const token = await getToken({ template: 'convex' });

  const userData = await fetchQuery(
    api.users.getCurrent,
    {},
    { token }
  );

  return <div>Welcome, {userData.name}</div>;
}

// Preload with auth
export default async function Dashboard() {
  const { getToken } = auth();
  const token = await getToken({ template: 'convex' });

  const preloadedData = await preloadQuery(
    api.dashboard.getData,
    {},
    { token }
  );

  return <DashboardClient preloaded={preloadedData} />;
}

// Server Action with auth
async function updateProfile(formData: FormData) {
  'use server';

  const { getToken } = auth();
  const token = await getToken({ template: 'convex' });

  const bio = formData.get('bio') as string;

  await fetchMutation(
    api.users.updateProfile,
    { bio },
    { token }
  );

  revalidatePath('/profile');
}

Auth Configuration

// convex/auth.config.ts

// Clerk
export default {
  providers: [
    {
      domain: 'https://your-domain.clerk.accounts.dev',
      applicationID: 'YOUR_CLERK_APP_ID',
    },
  ],
};

// Auth0
export default {
  providers: [
    {
      domain: 'https://your-domain.auth0.com',
      applicationID: 'YOUR_AUTH0_CLIENT_ID',
    },
  ],
};

// Custom JWT
export default {
  providers: [
    {
      type: 'customJwt',
      issuer: 'https://your-auth-server.com',
      jwks: 'https://your-auth-server.com/.well-known/jwks.json',
      algorithm: 'RS256',
    },
  ],
};

// Multiple providers
export default {
  providers: [
    {
      domain: 'https://your-domain.auth0.com',
      applicationID: 'YOUR_AUTH0_CLIENT_ID',
    },
    {
      domain: 'https://accounts.google.com',
      applicationID: 'YOUR_GOOGLE_CLIENT_ID',
    },
  ],
};

Manual Token Management

import { ConvexReactClient } from 'convex/react';

const convex = new ConvexReactClient(process.env.REACT_APP_CONVEX_URL!);

// Set auth token
convex.setAuth(async () => {
  const token = await myAuthService.getToken();
  return token;
});

// Clear auth
convex.clearAuth();

// With Firebase Auth
import { getAuth, onAuthStateChanged } from 'firebase/auth';

const auth = getAuth();

onAuthStateChanged(auth, async (user) => {
  if (user) {
    convex.setAuth(async () => await user.getIdToken());
  } else {
    convex.clearAuth();
  }
});