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
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
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
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:
- owner (highest) - Team owner, can delete team and manage billing
- admin - Can manage members and invite users
- member - Can create and edit content
- 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