AGENT SKILL

auth-setup

Set up Convex authentication with proper user management, identity mapping, and access control patterns. Use when implementing auth flows.

License 许可
AGPL-3.0
所属仓库
baisharg/ASTN
最近更新
2026-11-04
Stars
★ 2
Forks
2

01 前言 Front Matter

name
auth-setup
description
Set up Convex authentication with proper user management, identity mapping, and access control patterns. Use when implementing auth flows.

02 资源文件 6

03 SKILL.md 正文

Convex Authentication Setup

Implement secure authentication in Convex with user management and access control.

When to Use

  • Setting up authentication for the first time
  • Implementing user management (users table, identity mapping)
  • Creating authentication helper functions
  • Setting up OAuth providers (WorkOS, Auth0, etc.)

Architecture Overview

Convex authentication has two main parts:

  1. Client Authentication: Use a provider (WorkOS, Auth0, custom JWT)
  2. Backend Identity: Map auth provider identity to your users table

Schema Setup

// convex/schema.ts
import { defineSchema, defineTable } from 'convex/server'
import { v } from 'convex/values'

export default defineSchema({
  users: defineTable({
    // From auth provider identity
    tokenIdentifier: v.string(), // Unique per auth provider

    // User profile data
    name: v.string(),
    email: v.string(),
    pictureUrl: v.optional(v.string()),

    // Your app-specific fields
    role: v.union(v.literal('user'), v.literal('admin')),

    createdAt: v.number(),
    updatedAt: v.optional(v.number()),
  })
    .index('by_token', ['tokenIdentifier'])
    .index('by_email', ['email']),
})

Core Helper Functions

Get Current User

// convex/lib/auth.ts
import { QueryCtx, MutationCtx } from './_generated/server'
import { Doc } from './_generated/dataModel'

export async function getCurrentUser(
  ctx: QueryCtx | MutationCtx,
): Promise<Doc<'users'>> {
  const identity = await ctx.auth.getUserIdentity()
  if (!identity) {
    throw new Error('Not authenticated')
  }

  const user = await ctx.db
    .query('users')
    .withIndex('by_token', (q) =>
      q.eq('tokenIdentifier', identity.tokenIdentifier),
    )
    .unique()

  if (!user) {
    throw new Error('User not found')
  }

  return user
}

export async function getCurrentUserOrNull(
  ctx: QueryCtx | MutationCtx,
): Promise<Doc<'users'> | null> {
  const identity = await ctx.auth.getUserIdentity()
  if (!identity) {
    return null
  }

  return await ctx.db
    .query('users')
    .withIndex('by_token', (q) =>
      q.eq('tokenIdentifier', identity.tokenIdentifier),
    )
    .unique()
}

Require Admin

export async function requireAdmin(
  ctx: QueryCtx | MutationCtx,
): Promise<Doc<'users'>> {
  const user = await getCurrentUser(ctx)

  if (user.role !== 'admin') {
    throw new Error('Admin access required')
  }

  return user
}

User Creation/Upsert

On First Sign-In

// convex/users.ts
import { mutation } from './_generated/server'
import { v } from 'convex/values'

export const storeUser = mutation({
  args: {},
  handler: async (ctx) => {
    const identity = await ctx.auth.getUserIdentity()
    if (!identity) {
      throw new Error('Not authenticated')
    }

    // Check if user exists
    const existingUser = await ctx.db
      .query('users')
      .withIndex('by_token', (q) =>
        q.eq('tokenIdentifier', identity.tokenIdentifier),
      )
      .unique()

    if (existingUser) {
      // Update last seen or other fields
      await ctx.db.patch(existingUser._id, {
        updatedAt: Date.now(),
      })
      return existingUser._id
    }

    // Create new user
    const userId = await ctx.db.insert('users', {
      tokenIdentifier: identity.tokenIdentifier,
      name: identity.name ?? 'Anonymous',
      email: identity.email ?? '',
      pictureUrl: identity.pictureUrl,
      role: 'user',
      createdAt: Date.now(),
    })

    return userId
  },
})

Access Control Patterns

Owner-Only Access

import { mutation } from './_generated/server'
import { v } from 'convex/values'
import { getCurrentUser } from './lib/auth'

export const updateProfile = mutation({
  args: {
    name: v.string(),
  },
  handler: async (ctx, args) => {
    const user = await getCurrentUser(ctx)

    await ctx.db.patch(user._id, {
      name: args.name,
      updatedAt: Date.now(),
    })
  },
})

Resource Ownership

export const deleteTask = mutation({
  args: { taskId: v.id('tasks') },
  handler: async (ctx, args) => {
    const user = await getCurrentUser(ctx)

    const task = await ctx.db.get(args.taskId)
    if (!task) {
      throw new Error('Task not found')
    }

    // Check ownership
    if (task.userId !== user._id) {
      throw new Error('You can only delete your own tasks')
    }

    await ctx.db.delete(args.taskId)
  },
})

Team-Based Access

// Schema includes membership table
export default defineSchema({
  teams: defineTable({
    name: v.string(),
    ownerId: v.id('users'),
  }),

  teamMembers: defineTable({
    teamId: v.id('teams'),
    userId: v.id('users'),
    role: v.union(v.literal('owner'), v.literal('member')),
  })
    .index('by_team', ['teamId'])
    .index('by_user', ['userId'])
    .index('by_team_and_user', ['teamId', 'userId']),
})

// Helper to check team access
async function requireTeamAccess(
  ctx: MutationCtx,
  teamId: Id<'teams'>,
): Promise<{ user: Doc<'users'>; membership: Doc<'teamMembers'> }> {
  const user = await getCurrentUser(ctx)

  const membership = await ctx.db
    .query('teamMembers')
    .withIndex('by_team_and_user', (q) =>
      q.eq('teamId', teamId).eq('userId', user._id),
    )
    .unique()

  if (!membership) {
    throw new Error("You don't have access to this team")
  }

  return { user, membership }
}

// Use in functions
export const createProject = mutation({
  args: {
    teamId: v.id('teams'),
    name: v.string(),
  },
  handler: async (ctx, args) => {
    await requireTeamAccess(ctx, args.teamId)

    return await ctx.db.insert('projects', {
      teamId: args.teamId,
      name: args.name,
    })
  },
})

Public vs Private Queries

Public Query (No Auth Required)

export const listPublicPosts = query({
  args: {},
  handler: async (ctx) => {
    // No auth check - anyone can read
    return await ctx.db
      .query('posts')
      .withIndex('by_published', (q) => q.eq('published', true))
      .collect()
  },
})

Private Query (Auth Required)

export const getMyPosts = query({
  args: {},
  handler: async (ctx) => {
    const user = await getCurrentUser(ctx)

    return await ctx.db
      .query('posts')
      .withIndex('by_user', (q) => q.eq('userId', user._id))
      .collect()
  },
})

Hybrid Query (Optional Auth)

export const getPosts = query({
  args: {},
  handler: async (ctx) => {
    const user = await getCurrentUserOrNull(ctx)

    if (user) {
      // Show all posts including drafts for this user
      return await ctx.db
        .query('posts')
        .withIndex('by_user', (q) => q.eq('userId', user._id))
        .collect()
    } else {
      // Show only public posts for anonymous users
      return await ctx.db
        .query('posts')
        .withIndex('by_published', (q) => q.eq('published', true))
        .collect()
    }
  },
})

Client Setup with WorkOS

WorkOS AuthKit provides a complete authentication solution with minimal setup.

React/Vite Setup

npm install @workos-inc/authkit-react
// src/main.tsx
import { AuthKitProvider, useAuth } from "@workos-inc/authkit-react";
import { ConvexReactClient } from "convex/react";
import { ConvexProvider } from "convex/react";

const convex = new ConvexReactClient(import.meta.env.VITE_CONVEX_URL);

// Configure Convex to use WorkOS auth
convex.setAuth(useAuth);

function App() {
  return (
    <AuthKitProvider clientId={import.meta.env.VITE_WORKOS_CLIENT_ID}>
      <ConvexProvider client={convex}>
        <YourApp />
      </ConvexProvider>
    </AuthKitProvider>
  );
}

Next.js Setup

npm install @workos-inc/authkit-nextjs
// app/layout.tsx
import { AuthKitProvider } from "@workos-inc/authkit-nextjs";
import { ConvexClientProvider } from "./ConvexClientProvider";

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html>
      <body>
        <AuthKitProvider>
          <ConvexClientProvider>
            {children}
          </ConvexClientProvider>
        </AuthKitProvider>
      </body>
    </html>
  );
}
// app/ConvexClientProvider.tsx
"use client";

import { ConvexReactClient } from "convex/react";
import { ConvexProvider } from "convex/react";
import { useAuth } from "@workos-inc/authkit-nextjs";

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

export function ConvexClientProvider({ children }: { children: React.ReactNode }) {
  const { getToken } = useAuth();

  convex.setAuth(async () => {
    return await getToken();
  });

  return <ConvexProvider client={convex}>{children}</ConvexProvider>;
}

Environment Variables

# .env.local (React/Vite)
VITE_CONVEX_URL=https://your-deployment.convex.cloud
VITE_WORKOS_CLIENT_ID=your_workos_client_id

# .env.local (Next.js)
NEXT_PUBLIC_CONVEX_URL=https://your-deployment.convex.cloud
NEXT_PUBLIC_WORKOS_CLIENT_ID=your_workos_client_id
WORKOS_API_KEY=your_workos_api_key
WORKOS_COOKIE_PASSWORD=generate_a_random_32_character_string

Call storeUser on Sign-In

// In your app after user signs in
import { useMutation } from 'convex/react'
import { api } from '../convex/_generated/api'
import { useEffect } from 'react'
import { useAuth } from '@workos-inc/authkit-react'

function YourApp() {
  const { user } = useAuth()
  const storeUser = useMutation(api.users.storeUser)

  useEffect(() => {
    if (user) {
      storeUser()
    }
  }, [user, storeUser])

  // ... rest of your app
}

Alternative Auth Providers

If you need to use a different provider, see the Convex auth documentation for:

  • Custom JWT
  • Auth0
  • Other OAuth providers

Checklist

  • [ ] Users table with tokenIdentifier index
  • [ ] getCurrentUser helper function
  • [ ] storeUser mutation for first sign-in
  • [ ] Authentication check in all protected functions
  • [ ] Authorization check for resource access
  • [ ] Clear error messages ("Not authenticated", "Unauthorized")
  • [ ] Client auth provider configured (WorkOS, Auth0, etc.)

04 同仓库更多 Skills