auth-setup
Set up Convex authentication with proper user management, identity mapping, and access control patterns. Use when implementing auth flows.
What this skill does
# 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
```typescript
// 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
```typescript
// 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
```typescript
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
```typescript
// 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
```typescript
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
```typescript
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
```typescript
// 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)
```typescript
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)
```typescript
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)
```typescript
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
```bash
npm install @workos-inc/authkit-react
```
```typescript
// 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
```bash
npm install @workos-inc/authkit-nextjs
```
```typescript
// app/layout.tsx
import { AuthKitProvider } from "@workos-inc/authkit-nextjs";
import { ConvexClientProvider } from Related in General
modeling-omnistudio-epc-catalog
IncludedSalesforce Industries CME EPC product-modeling skill for Product2-based catalog creation. Use when creating EPC products, configuring product attributes, building offer bundles with Product Child Items, or reviewing EPC DataPack JSON metadata for product catalog changes. TRIGGER when: user creates or updates Product2 EPC records, AttributeAssignment payloads, AttributeMetadata/AttributeDefaultValues, Offer bundles, or ProductChildItem relationships. DO NOT TRIGGER when: designing OmniScripts/FlexCards/Integration Procedures (use building-omnistudio-omniscript, building-omnistudio-flexcard, or building-omnistudio-integration-procedure), implementing Apex business logic (use generating-apex), or troubleshooting deployment pipelines (use deploying-metadata).
relationship-science-coach
IncludedUse this skill for direct, practical adult relationship coaching: couples conflict, repair, trust, marriage, dating, flirting, attachment patterns, emotional connection, sex, desire differences, eroticism, kink negotiation, affection, love languages, breakups, and long-term passion. Draw on Gottman, EFT and Hold Me Tight, attachment science, modern sex research, Perel, Nagoski, Kerner, Schnarch, Love and Stosny, and flexible love-language tools. Be concrete and low-hedge. Redirect only for imminent danger, abuse, coercive control, minors, non-consent, self-harm, stalking, or medical/legal/psychiatric decisions.
building-sf-integrations
IncludedSalesforce integration architecture and runtime plumbing with 120-point scoring. Use this skill to set up Named Credentials, External Credentials, External Services, REST/SOAP callout patterns, Platform Events, and Change Data Capture. TRIGGER when: user sets up Named Credentials, External Services, REST/SOAP callouts, Platform Events, CDC, or touches .namedCredential-meta.xml files. DO NOT TRIGGER when: Connected App/OAuth config (use configuring-connected-apps), Apex-only logic (use generating-apex), or data import/export (use handling-sf-data).
venue-templates
IncludedAccess comprehensive LaTeX templates, formatting requirements, and submission guidelines for major scientific publication venues (Nature, Science, PLOS, IEEE, ACM), academic conferences (NeurIPS, ICML, CVPR, CHI), research posters, and grant proposals (NSF, NIH, DOE, DARPA). This skill should be used when preparing manuscripts for journal submission, conference papers, research posters, or grant proposals and need venue-specific formatting requirements and templates.
let-fate-decide
IncludedDraws the 12 Houses of the Zodiac Tarot spread to inject entropy into planning when prompts are vague, ambiguous, or casually delegated. Interprets the spread to guide next steps. Use when the user says 'let fate decide', 'YOLO', 'whatever', 'idk', or other nonchalant phrases, makes Yu-Gi-Oh references, or when you are about to arbitrarily pick between multiple reasonable approaches. Prefer over ask-questions-if-underspecified when the user's tone is casual or playful rather than precision-seeking.
net-ops
IncludedCross-platform network troubleshooting (Windows, macOS, Linux) via local or remote shell. Use for: DNS broken, can't resolve hostnames, nslookup/dig works but apps fail, NRPT, WFP, scutil, /etc/resolver, systemd-resolved, /etc/resolv.conf, NetworkManager, VPN DNS leak residue (ProtonVPN/Mullvad/WireGuard/AnyConnect), AV/firewall blocking DNS or DoH, Tailscale DNS interaction, intermittent connectivity, remote diagnostics over SSH.