Skip to main content

Overview

Lawn uses Clerk for authentication, integrated with Convex for backend authorization. The authentication system provides:
  • User identity management
  • Team-based access control
  • Role-based permissions
  • Secure session handling

Authentication Flow

Clerk handles user authentication on the frontend, and Convex validates the user’s identity on each API call:
import { useUser } from '@clerk/nextjs';
import { useQuery } from 'convex/react';
import { api } from '../convex/_generated/api';

function Dashboard() {
  const { user, isLoaded } = useUser();
  const teams = useQuery(api.teams.list);
  
  if (!isLoaded) return <div>Loading...</div>;
  if (!user) return <div>Please sign in</div>;
  
  return <div>Welcome {user.firstName}!</div>;
}

Authorization Helpers

The backend provides several helper functions for authorization (defined in convex/auth.ts):

getUser

Retrieves the authenticated user or returns null:
const user = await getUser(ctx);
if (!user) {
  return [];
}

requireUser

Requires authentication, throws error if not authenticated:
const user = await requireUser(ctx);
// user is guaranteed to exist here

requireTeamAccess

Verifies team membership and optional role requirement:
const { user, membership } = await requireTeamAccess(ctx, teamId, 'admin');
// Throws if user is not a team member or doesn't have admin+ role
teamId
Id<'teams'>
required
The team to check access for
requiredRole
'owner' | 'admin' | 'member' | 'viewer'
Minimum required role (optional)

requireProjectAccess

Verifies access to a project (through team membership):
const { user, membership, project } = await requireProjectAccess(ctx, projectId, 'member');
// Throws if user doesn't have access to the project's team
projectId
Id<'projects'>
required
The project to check access for
requiredRole
'owner' | 'admin' | 'member' | 'viewer'
Minimum required role (optional)

requireVideoAccess

Verifies access to a video (through project and team membership):
const { user, membership, project, video } = await requireVideoAccess(ctx, videoId, 'viewer');
// Throws if user doesn't have access to the video's project
videoId
Id<'videos'>
required
The video to check access for
requiredRole
'owner' | 'admin' | 'member' | 'viewer'
Minimum required role (optional)

Role Hierarchy

Roles are hierarchical, with higher roles having all permissions of lower roles:
  1. owner (highest) - Team owner, can delete team and manage billing
  2. admin - Can manage members and invite users
  3. member - Can create and edit content
  4. viewer (lowest) - Read-only access
const ROLE_HIERARCHY = {
  owner: 4,
  admin: 3,
  member: 2,
  viewer: 1,
};
When you specify requireTeamAccess(ctx, teamId, 'member'), users with member, admin, or owner roles will have access.

Identity Helpers

Utility functions extract user information from Clerk identity:

identityName

Extracts user’s display name:
const name = identityName(user);
// Returns: name, or "firstName lastName", or email, or "Unknown"

identityEmail

Extracts user’s email:
const email = identityEmail(user);
// Returns: email address or empty string

identityAvatarUrl

Extracts user’s avatar URL:
const avatarUrl = identityAvatarUrl(user);
// Returns: pictureUrl or undefined

Usage Examples

Creating a Protected Query

import { query } from './_generated/server';
import { v } from 'convex/values';
import { requireProjectAccess } from './auth';

export const get = query({
  args: { projectId: v.id('projects') },
  handler: async (ctx, args) => {
    // Verify user has access to this project
    const { project, membership } = await requireProjectAccess(ctx, args.projectId);
    
    return {
      ...project,
      role: membership.role,
    };
  },
});

Creating a Protected Mutation

import { mutation } from './_generated/server';
import { v } from 'convex/values';
import { requireVideoAccess } from './auth';

export const update = mutation({
  args: {
    videoId: v.id('videos'),
    title: v.string(),
  },
  handler: async (ctx, args) => {
    // Require at least 'member' role
    await requireVideoAccess(ctx, args.videoId, 'member');
    
    await ctx.db.patch(args.videoId, {
      title: args.title,
    });
  },
});

Checking Role Permissions

import { useQuery } from 'convex/react';
import { api } from '../convex/_generated/api';

function ProjectSettings({ projectId }) {
  const project = useQuery(api.projects.get, { projectId });
  
  const canEdit = project?.role && ['member', 'admin', 'owner'].includes(project.role);
  const canDelete = project?.role && ['admin', 'owner'].includes(project.role);
  
  return (
    <div>
      {canEdit && <button>Edit</button>}
      {canDelete && <button>Delete</button>}
    </div>
  );
}

Error Handling

Authentication errors are thrown as standard JavaScript errors:
import { useMutation } from 'convex/react';
import { api } from '../convex/_generated/api';

function DeleteButton({ projectId }) {
  const deleteProject = useMutation(api.projects.remove);
  
  const handleDelete = async () => {
    try {
      await deleteProject({ projectId });
    } catch (error) {
      if (error.message.includes('Requires admin role')) {
        alert('You need admin permissions');
      } else {
        alert('Delete failed: ' + error.message);
      }
    }
  };
  
  return <button onClick={handleDelete}>Delete</button>;
}

Common Error Messages

  • "Not authenticated" - User is not signed in
  • "Not a team member" - User is not a member of the team
  • "Requires {role} role or higher" - User doesn’t have sufficient permissions
  • "Project not found" - Project doesn’t exist or user doesn’t have access
  • "Video not found" - Video doesn’t exist or user doesn’t have access

Build docs developers (and LLMs) love