diff --git a/skills/convex/SKILL.md b/skills/convex/SKILL.md
new file mode 100644
index 00000000..64c8c938
--- /dev/null
+++ b/skills/convex/SKILL.md
@@ -0,0 +1,797 @@
+---
+name: convex
+description: "Convex reactive backend expert: schema design, TypeScript functions, real-time subscriptions, auth, file storage, scheduling, and deployment."
+risk: safe
+source: "https://docs.convex.dev"
+---
+
+# Convex
+
+You are an expert in Convex — the open-source, reactive backend platform where queries are TypeScript code. You have deep knowledge of schema design, function authoring (queries, mutations, actions), real-time data subscriptions, authentication, file storage, scheduling, and deployment workflows across React, Next.js, Angular, Vue, Svelte, React Native, and server-side environments.
+
+## When to Use
+
+- Use when building a new project with Convex as the backend
+- Use when adding Convex to an existing React, Next.js, Angular, Vue, Svelte, or React Native app
+- Use when designing schemas for a Convex document-relational database
+- Use when writing or debugging Convex functions (queries, mutations, actions)
+- Use when implementing real-time/reactive data patterns
+- Use when setting up authentication with Convex Auth or third-party providers (Clerk, Auth0, etc.)
+- Use when working with Convex file storage, scheduled functions, or cron jobs
+- Use when deploying or managing Convex projects
+
+## Core Concepts
+
+Convex is a **document-relational** database with a fully managed backend. Key differentiators:
+
+- **Reactive by default**: Queries automatically re-run and push updates to all connected clients when underlying data changes
+- **TypeScript-first**: All backend logic — queries, mutations, actions, schemas — is written in TypeScript
+- **ACID transactions**: Serializable isolation with optimistic concurrency control
+- **No infrastructure to manage**: Serverless, scales automatically, zero config
+- **End-to-end type safety**: Types flow from schema → backend functions → client hooks
+
+### Function Types
+
+| Type | Purpose | Can Read DB | Can Write DB | Can Call External APIs | Cached/Reactive |
+| :-------------- | :------------------------ | :------------- | :---------------- | :--------------------- | :-------------- |
+| **Query** | Read data | ✅ | ❌ | ❌ | ✅ |
+| **Mutation** | Write data | ✅ | ✅ | ❌ | ❌ |
+| **Action** | Side effects | via `runQuery` | via `runMutation` | ✅ | ❌ |
+| **HTTP Action** | Webhooks/custom endpoints | via `runQuery` | via `runMutation` | ✅ | ❌ |
+
+## Project Setup
+
+### New Project (Next.js)
+
+```bash
+npx create-next-app@latest my-app
+cd my-app && npm install convex
+npx convex dev
+```
+
+### Add to Existing Project
+
+```bash
+npm install convex
+npx convex dev
+```
+
+The `npx convex dev` command:
+
+1. Prompts you to log in (GitHub)
+2. Creates a project and deployment
+3. Generates `convex/` folder for backend functions
+4. Syncs functions to your dev deployment in real-time
+5. Creates `.env.local` with `CONVEX_DEPLOYMENT` and `NEXT_PUBLIC_CONVEX_URL`
+
+### Folder Structure
+
+```
+my-app/
+├── convex/
+│ ├── _generated/ ← Auto-generated (DO NOT EDIT)
+│ │ ├── api.d.ts
+│ │ ├── dataModel.d.ts
+│ │ └── server.d.ts
+│ ├── schema.ts ← Database schema definition
+│ ├── tasks.ts ← Query/mutation functions
+│ └── http.ts ← HTTP actions (optional)
+├── .env.local ← CONVEX_DEPLOYMENT, NEXT_PUBLIC_CONVEX_URL
+└── convex.json ← Project config (optional)
+```
+
+## Schema Design
+
+Define your schema in `convex/schema.ts` using the validator library:
+
+```typescript
+import { defineSchema, defineTable } from "convex/server";
+import { v } from "convex/values";
+
+export default defineSchema({
+ users: defineTable({
+ name: v.string(),
+ email: v.string(),
+ avatarUrl: v.optional(v.string()),
+ tokenIdentifier: v.string(),
+ })
+ .index("by_token", ["tokenIdentifier"])
+ .index("by_email", ["email"]),
+
+ messages: defineTable({
+ authorId: v.id("users"),
+ channelId: v.id("channels"),
+ body: v.string(),
+ attachmentId: v.optional(v.id("_storage")),
+ })
+ .index("by_channel", ["channelId"])
+ .searchIndex("search_body", { searchField: "body" }),
+
+ channels: defineTable({
+ name: v.string(),
+ description: v.optional(v.string()),
+ isPrivate: v.boolean(),
+ }),
+});
+```
+
+### Validator Types
+
+| Validator | TypeScript Type | Notes |
+| :-------------------------------- | :-------------------- | :--------------------------------------------- |
+| `v.string()` | `string` | |
+| `v.number()` | `number` | IEEE 754 float |
+| `v.bigint()` | `bigint` | |
+| `v.boolean()` | `boolean` | |
+| `v.null()` | `null` | |
+| `v.id("tableName")` | `Id<"tableName">` | Document reference |
+| `v.array(v.string())` | `string[]` | |
+| `v.object({...})` | `{...}` | Nested objects |
+| `v.optional(v.string())` | `string \| undefined` | |
+| `v.union(v.string(), v.number())` | `string \| number` | |
+| `v.literal("active")` | `"active"` | Literal types |
+| `v.bytes()` | `ArrayBuffer` | Binary data |
+| `v.float64()` | `number` | Explicit 64-bit float (used in vector indexes) |
+| `v.any()` | `any` | Escape hatch |
+
+### Indexes
+
+```typescript
+// Single-field index
+defineTable({ email: v.string() }).index("by_email", ["email"]);
+
+// Compound index (order matters for range queries)
+defineTable({
+ orgId: v.string(),
+ createdAt: v.number(),
+}).index("by_org_and_date", ["orgId", "createdAt"]);
+
+// Full-text search index
+defineTable({ body: v.string(), channelId: v.id("channels") }).searchIndex(
+ "search_body",
+ {
+ searchField: "body",
+ filterFields: ["channelId"],
+ },
+);
+
+// Vector search index (for AI/embeddings)
+defineTable({ embedding: v.array(v.float64()), text: v.string() }).vectorIndex(
+ "by_embedding",
+ {
+ vectorField: "embedding",
+ dimensions: 1536,
+ },
+);
+```
+
+## Writing Functions
+
+### Queries (Read Data)
+
+Queries are reactive — clients automatically get updates when data changes.
+
+````typescript
+import { query } from "./_generated/server";
+import { v } from "convex/values";
+
+// Simple query — list all tasks
+export const list = query({
+ args: {},
+ handler: async (ctx) => {
+ return await ctx.db.query("tasks").collect();
+ },
+});
+
+// Query with arguments and filtering
+export const getByChannel = query({
+ args: { channelId: v.id("channels") },
+ handler: async (ctx, args) => {
+ return await ctx.db
+ .query("messages")
+ .withIndex("by_channel", (q) => q.eq("channelId", args.channelId))
+ .order("desc")
+ .take(50);
+ },
+});
+
+// Query with auth check
+export const getMyProfile = query({
+ args: {},
+ handler: async (ctx) => {
+ 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();
+ },
+});
+
+### Paginated Queries
+
+Use cursor-based pagination for lists or infinite scroll UIs.
+
+```typescript
+import { query } from "./_generated/server";
+import { paginationOptsValidator } from "convex/server";
+
+export const listPaginated = query({
+ args: {
+ paginationOpts: paginationOptsValidator
+ },
+ handler: async (ctx, args) => {
+ return await ctx.db
+ .query("messages")
+ .order("desc")
+ .paginate(args.paginationOpts);
+ },
+});
+```
+
+### Mutations (Write Data)
+
+Mutations run as ACID transactions with serializable isolation.
+
+```typescript
+import { mutation } from "./_generated/server";
+import { v } from "convex/values";
+
+// Insert a document
+export const create = mutation({
+ args: { text: v.string(), isCompleted: v.boolean() },
+ handler: async (ctx, args) => {
+ const taskId = await ctx.db.insert("tasks", {
+ text: args.text,
+ isCompleted: args.isCompleted,
+ });
+ return taskId;
+ },
+});
+
+// Update a document
+export const update = mutation({
+ args: { id: v.id("tasks"), isCompleted: v.boolean() },
+ handler: async (ctx, args) => {
+ await ctx.db.patch(args.id, { isCompleted: args.isCompleted });
+ },
+});
+
+// Delete a document
+export const remove = mutation({
+ args: { id: v.id("tasks") },
+ handler: async (ctx, args) => {
+ await ctx.db.delete(args.id);
+ },
+});
+
+// Multi-document transaction (automatically atomic)
+export const transferCredits = mutation({
+ args: {
+ fromUserId: v.id("users"),
+ toUserId: v.id("users"),
+ amount: v.number(),
+ },
+ handler: async (ctx, args) => {
+ const fromUser = await ctx.db.get(args.fromUserId);
+ const toUser = await ctx.db.get(args.toUserId);
+ if (!fromUser || !toUser) throw new Error("User not found");
+ if (fromUser.credits < args.amount) throw new Error("Insufficient credits");
+
+ await ctx.db.patch(args.fromUserId, {
+ credits: fromUser.credits - args.amount,
+ });
+ await ctx.db.patch(args.toUserId, {
+ credits: toUser.credits + args.amount,
+ });
+ },
+});
+````
+
+### Actions (External APIs & Side Effects)
+
+Actions can call third-party services but cannot directly access the database — they must use `ctx.runQuery` and `ctx.runMutation`.
+
+```typescript
+import { action } from "./_generated/server";
+import { v } from "convex/values";
+import { api } from "./_generated/api";
+
+export const sendEmail = action({
+ args: { to: v.string(), subject: v.string(), body: v.string() },
+ handler: async (ctx, args) => {
+ // Call external API
+ const response = await fetch("https://api.sendgrid.com/v3/mail/send", {
+ method: "POST",
+ headers: {
+ Authorization: `Bearer ${process.env.SENDGRID_API_KEY}`,
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify({
+ personalizations: [{ to: [{ email: args.to }] }],
+ from: { email: "noreply@example.com" },
+ subject: args.subject,
+ content: [{ type: "text/plain", value: args.body }],
+ }),
+ });
+
+ if (!response.ok) throw new Error("Failed to send email");
+
+ // Write result back to database via mutation
+ await ctx.runMutation(api.emails.recordSent, {
+ to: args.to,
+ subject: args.subject,
+ sentAt: Date.now(),
+ });
+ },
+});
+
+// Generate AI embeddings
+export const generateEmbedding = action({
+ args: { text: v.string(), documentId: v.id("documents") },
+ handler: async (ctx, args) => {
+ const response = await fetch("https://api.openai.com/v1/embeddings", {
+ method: "POST",
+ headers: {
+ Authorization: `Bearer ${process.env.OPENAI_API_KEY}`,
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify({
+ model: "text-embedding-3-small",
+ input: args.text,
+ }),
+ });
+
+ const { data } = await response.json();
+ await ctx.runMutation(api.documents.saveEmbedding, {
+ documentId: args.documentId,
+ embedding: data[0].embedding,
+ });
+ },
+});
+```
+
+### HTTP Actions (Webhooks)
+
+```typescript
+import { httpRouter } from "convex/server";
+import { httpAction } from "./_generated/server";
+import { api } from "./_generated/api";
+
+const http = httpRouter();
+
+http.route({
+ path: "/webhooks/stripe",
+ method: "POST",
+ handler: httpAction(async (ctx, request) => {
+ const body = await request.text();
+ const signature = request.headers.get("stripe-signature");
+
+ // Verify webhook signature here...
+
+ const event = JSON.parse(body);
+ await ctx.runMutation(api.payments.handleWebhook, { event });
+
+ return new Response("OK", { status: 200 });
+ }),
+});
+
+export default http;
+```
+
+## Client-Side Integration
+
+### React / Next.js
+
+```typescript
+// app/ConvexClientProvider.tsx
+"use client";
+import { ConvexProvider, ConvexReactClient } from "convex/react";
+import { ReactNode } from "react";
+
+const convex = new ConvexReactClient(process.env.NEXT_PUBLIC_CONVEX_URL!);
+
+export function ConvexClientProvider({ children }: { children: ReactNode }) {
+ return
Loading...
; + + return ( +Loading...
} + + {status === "CanLoadMore" && ( + + )} +Loading...
+ } + + `, +}) +export class TaskListComponent { + convex = inject(ConvexService); +} +``` + +> **Note:** The community library `@robmanganelly/ngx-convex` provides a more Angular-native experience with React-like hooks adapted for Angular DI and Signals. + +## Scheduling & Cron Jobs + +### One-off Scheduled Functions + +```typescript +import { mutation } from "./_generated/server"; +import { api } from "./_generated/api"; + +export const sendReminder = mutation({ + args: { userId: v.id("users"), message: v.string(), delayMs: v.number() }, + handler: async (ctx, args) => { + await ctx.scheduler.runAfter(args.delayMs, api.notifications.send, { + userId: args.userId, + message: args.message, + }); + }, +}); +``` + +### Cron Jobs + +```typescript +// convex/crons.ts +import { cronJobs } from "convex/server"; +import { api } from "./_generated/api"; + +const crons = cronJobs(); + +crons.interval("clear old logs", { hours: 24 }, api.logs.clearOld); + +crons.cron( + "weekly digest", + "0 9 * * 1", // Every Monday at 9 AM + api.emails.sendWeeklyDigest, +); + +export default crons; +``` + +## File Storage + +```typescript +// Generate an upload URL (mutation) +export const generateUploadUrl = mutation({ + args: {}, + handler: async (ctx) => { + return await ctx.storage.generateUploadUrl(); + }, +}); + +// Save file reference after upload (mutation) +export const saveFile = mutation({ + args: { storageId: v.id("_storage"), name: v.string() }, + handler: async (ctx, args) => { + await ctx.db.insert("files", { + storageId: args.storageId, + name: args.name, + }); + }, +}); + +// Get a URL to serve a file (query) +export const getFileUrl = query({ + args: { storageId: v.id("_storage") }, + handler: async (ctx, args) => { + return await ctx.storage.getUrl(args.storageId); + }, +}); +``` + +## Environment Variables + +```bash +# Set environment variables for your deployment +npx convex env set OPENAI_API_KEY sk-... +npx convex env set SENDGRID_API_KEY SG... + +# List current env vars +npx convex env list + +# Remove an env var +npx convex env unset OPENAI_API_KEY +``` + +Access in actions (NOT in queries or mutations): + +```typescript +// Only available in actions +const apiKey = process.env.OPENAI_API_KEY; +``` + +## Deployment & CLI + +```bash +# Development (watches for changes, syncs to dev deployment) +npx convex dev + +# Deploy to production +npx convex deploy + +# Import data +npx convex import --table tasks data.jsonl + +# Export data +npx convex export --path ./backup + +# Open Convex dashboard +npx convex dashboard + +# Run a function from CLI +npx convex run tasks:list + +# View logs +npx convex logs +``` + +## Best Practices + +- ✅ Define schemas — adds type safety across your entire stack +- ✅ Use indexes for queries — avoids full table scans +- ✅ Use compound indexes with equality filters first, range filter last +- ✅ Rely on native determinism — `Date.now()` and `Math.random()` are 100% safe to use in queries and mutations because Convex freezes time at the start of every function execution! +- ✅ Use `v.id("tableName")` for document references instead of plain strings +- ✅ Use actions for external API calls (never call external APIs from queries or mutations) +- ✅ Use `ctx.runQuery` / `ctx.runMutation` from actions — never access `ctx.db` directly in actions +- ✅ Add argument validators to all functions — they enforce runtime type safety +- ✅ Return `null` when a document isn't found instead of throwing an error unless missing is exceptional +- ✅ Prefer `withIndex` over `.filter()` for query performance + +## Anti-Patterns to Avoid + +1. **❌ External API calls in queries/mutations**: Only actions can call external services. Queries and mutations run in the Convex transaction engine. +2. **❌ Doing slow CPU-bound work in mutations**: Mutations block database commits; offload heavy processing to actions. +3. **❌ Using `.collect()` on large tables without limits**: Fetches all documents into memory. Use `.take(N)` or `.paginate()`. +4. **❌ Skipping schema definition**: Without a schema you lose end-to-end type safety, the main Convex advantage. +5. **❌ Using `.filter()` instead of indexes**: `.filter()` does a full table scan. Define an index and use `.withIndex()`. +6. **❌ Storing large blobs in documents**: Use Convex file storage (`_storage`) for files; keep documents lean. +7. **❌ Circular `runQuery`/`runMutation` chains**: Actions calling mutations that schedule actions can create infinite loops. + +## Common Pitfalls + +- **Problem:** "Query returns `undefined` on first render" + **Solution:** This is expected — Convex queries are async. Check for `undefined` before rendering (this means loading, not empty). + +- **Problem:** "Mutation throws `Document not found`" + **Solution:** Documents may have been deleted between your read and write due to optimistic concurrency. Re-read inside the mutation. + +- **Problem:** "`process.env` is undefined in query/mutation" + **Solution:** Environment variables are only accessible in **actions** (not queries or mutations) because queries/mutations run in the deterministic transaction engine. + +- **Problem:** "Function handler is too slow" + **Solution:** Add indexes for your query patterns. Use `withIndex()` instead of `.filter()`. For complex operations, break into smaller mutations. + +- **Problem:** "Schema push fails with existing data" + **Solution:** Convex validates existing data against new schemas. Either migrate existing documents first, or use `v.optional()` for new fields. + +## Limitations + +- Queries and mutations cannot call external HTTP APIs (use actions instead) +- No raw SQL — you work with the Convex query builder API +- Environment variables only available in actions, not in queries or mutations +- Document size limit of 1MB +- Maximum function execution time limits apply +- No server-side rendering of Convex data without specific SSR patterns (use preloading) +- Schemas are enforced at write-time; changing schemas requires data migration for existing documents + +## Related Skills + +- `@firebase` — Alternative BaaS with Firestore (compare: Convex is TypeScript-first with ACID transactions) +- `@supabase-automation` — Alternative with PostgreSQL backend (compare: Convex is document-relational with built-in reactivity) +- `@prisma-expert` — ORM for traditional databases (Convex replaces both ORM and database) +- `@react-patterns` — Frontend patterns that pair well with Convex React hooks +- `@nextjs-app-router` — Next.js App Router integration patterns +- `@authentication-oauth` — Auth patterns (Convex supports Clerk, Auth0, Convex Auth) +- `@stripe` — Payment integration via Convex actions and HTTP webhooks + +## Resources + +- [Official Docs](https://docs.convex.dev) +- [Convex Stack (Blog)](https://stack.convex.dev) +- [GitHub](https://github.com/get-convex/convex-backend) +- [Discord Community](https://convex.dev/community) +- [Convex Chef (AI Starter)](https://chef.convex.dev)