$npx -y skills add PolarCoding85/convex-agent-skillz --skill convex-skillExpert guidance for Convex backend development including queries, mutations, actions, schemas, authentication, scheduling, file storage, search, and Next.js integration. Use when working with Convex functions, database operations, convex/ directory code, or Next.js App Router wit
| 1 | # Convex Backend Development |
| 2 | |
| 3 | ## Core Architecture |
| 4 | |
| 5 | Convex is a reactive database where queries are TypeScript functions. The sync engine (queries + mutations + database) is the heart of Convex — center your app around it. |
| 6 | |
| 7 | ### Function Types |
| 8 | |
| 9 | | Type | DB Access | Deterministic | Cached/Reactive | Use For | |
| 10 | | ------------ | ------------- | ------------- | --------------- | -------------------------- | |
| 11 | | `query` | Read only | Yes | Yes | All reads, subscriptions | |
| 12 | | `mutation` | Read/Write | Yes | No | All writes (transactions) | |
| 13 | | `action` | Via ctx.run\* | No | No | External APIs, LLMs, email | |
| 14 | | `httpAction` | Via ctx.run\* | No | No | Webhooks, custom HTTP | |
| 15 | |
| 16 | **Key rule**: Queries and mutations cannot make network requests. Actions cannot directly access the database. |
| 17 | |
| 18 | ## Project Structure (Best Practice) |
| 19 | |
| 20 | ``` |
| 21 | convex/ |
| 22 | ├── _generated/ # Auto-generated types (commit this) |
| 23 | ├── schema.ts # Database schema |
| 24 | ├── model/ # Helper functions (most logic lives here) |
| 25 | │ ├── users.ts |
| 26 | │ └── messages.ts |
| 27 | ├── users.ts # Thin wrappers exposing public API |
| 28 | ├── messages.ts |
| 29 | ├── crons.ts # Cron job definitions |
| 30 | └── http.ts # HTTP action routes |
| 31 | ``` |
| 32 | |
| 33 | ## Essential Patterns |
| 34 | |
| 35 | ### 1. Function Structure |
| 36 | |
| 37 | ```typescript |
| 38 | // convex/messages.ts |
| 39 | import { query, mutation, internalMutation } from './_generated/server'; |
| 40 | import { internal } from './_generated/api'; |
| 41 | import { v } from 'convex/values'; |
| 42 | |
| 43 | // PUBLIC query with validators (always validate public functions) |
| 44 | export const list = query({ |
| 45 | args: { channelId: v.id('channels') }, |
| 46 | handler: async (ctx, { channelId }) => { |
| 47 | return await ctx.db |
| 48 | .query('messages') |
| 49 | .withIndex('by_channel', (q) => q.eq('channelId', channelId)) |
| 50 | .order('desc') |
| 51 | .take(50); |
| 52 | } |
| 53 | }); |
| 54 | |
| 55 | // PUBLIC mutation with validators and auth check |
| 56 | export const send = mutation({ |
| 57 | args: { channelId: v.id('channels'), body: v.string() }, |
| 58 | handler: async (ctx, { channelId, body }) => { |
| 59 | const user = await ctx.auth.getUserIdentity(); |
| 60 | if (!user) throw new Error('Unauthorized'); |
| 61 | |
| 62 | await ctx.db.insert('messages', { |
| 63 | channelId, |
| 64 | body, |
| 65 | authorId: user.subject |
| 66 | }); |
| 67 | } |
| 68 | }); |
| 69 | |
| 70 | // INTERNAL mutation (for scheduling, crons, actions) |
| 71 | export const deleteOld = internalMutation({ |
| 72 | args: { before: v.number() }, |
| 73 | handler: async (ctx, { before }) => { |
| 74 | const old = await ctx.db |
| 75 | .query('messages') |
| 76 | .withIndex('by_createdAt', (q) => q.lt('_creationTime', before)) |
| 77 | .take(100); |
| 78 | for (const msg of old) { |
| 79 | await ctx.db.delete(msg._id); |
| 80 | } |
| 81 | } |
| 82 | }); |
| 83 | ``` |
| 84 | |
| 85 | ### 2. Helper Functions Pattern |
| 86 | |
| 87 | Most logic should live in helper functions, NOT in query/mutation handlers: |
| 88 | |
| 89 | ```typescript |
| 90 | // convex/model/users.ts |
| 91 | import { QueryCtx, MutationCtx } from '../_generated/server'; |
| 92 | import { Doc } from '../_generated/dataModel'; |
| 93 | |
| 94 | export async function getCurrentUser( |
| 95 | ctx: QueryCtx |
| 96 | ): Promise<Doc<'users'> | null> { |
| 97 | const identity = await ctx.auth.getUserIdentity(); |
| 98 | if (!identity) return null; |
| 99 | |
| 100 | return await ctx.db |
| 101 | .query('users') |
| 102 | .withIndex('by_tokenIdentifier', (q) => |
| 103 | q.eq('tokenIdentifier', identity.tokenIdentifier) |
| 104 | ) |
| 105 | .unique(); |
| 106 | } |
| 107 | |
| 108 | export async function requireUser(ctx: QueryCtx): Promise<Doc<'users'>> { |
| 109 | const user = await getCurrentUser(ctx); |
| 110 | if (!user) throw new Error('Unauthorized'); |
| 111 | return user; |
| 112 | } |
| 113 | ``` |
| 114 | |
| 115 | ### 3. Actions with Scheduling |
| 116 | |
| 117 | ```typescript |
| 118 | // convex/ai.ts |
| 119 | import { action, internalMutation } from './_generated/server'; |
| 120 | import { internal } from './_generated/api'; |
| 121 | import { v } from 'convex/values'; |
| 122 | |
| 123 | export const summarize = action({ |
| 124 | args: { documentId: v.id('documents') }, |
| 125 | handler: async (ctx, { documentId }) => { |
| 126 | // Read data via internal query |
| 127 | const doc = await ctx.runQuery(internal.documents.get, { documentId }); |
| 128 | |
| 129 | // Call external API |
| 130 | const response = await fetch('https://api.o |