[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"skill:01M715SCNFBQFT1GJCBMV2MTJV":3,"skill-same:01M715SCNFBQFT1GJCBMV2MTJV":70},{"skillNo":4,"name":5,"nameZh":6,"description":7,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":16,"categories":17,"keywords":23,"publishedAt":43,"linkOnly":44,"markdownContent":45,"markdownBody":46,"frontMatter":47,"refName":50,"commitSha":51,"skillDirectory":52,"forksCount":14,"repositoryHtmlUrl":53,"repositoryHomepageUrl":6,"ownerHtmlUrl":54,"ownerBlogUrl":6,"ownerDescription":6,"updatedAt":55,"install":56,"resources":69},"01M715SCNFBQFT1GJCBMV2MTJV","convex",null,"Convex reactive backend expert: schema design, TypeScript functions, real-time subscriptions, auth, file storage, scheduling, and deployment.","sapsapshen\u002Fai-memory-router","sapsapshen","https:\u002F\u002Favatars.githubusercontent.com\u002Fu\u002F20347959?v=4","01M6XDQEF1DP50DKF37H81J3J5","shenmian","01M6XDQEJPE41Y3DT2763X8ZJN",0,"MIT","https:\u002F\u002Fgithub.com\u002Fsapsapshen\u002Fai-memory-router\u002Fblob\u002Fmain\u002Fskills-store\u002Fcursor\u002Fconvex\u002FSKILL.md",[18],{"categoryNo":19,"categoryName":20,"taxonomyType":21,"taxonomyTypeDesc":22},"01JBSEEDCAT000000000000015","视觉设计","SCENARIO","使用场景",[5,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42],"2026","design","reactive","time","expert","subscriptions","functions","scheduling","schema design","schema","backend","real-time","file","dev","deployment","auth","storage","typescript","safe","2026-11-10 10:12:30",false,"---\nname: convex\ndescription: \"Convex reactive backend expert: schema design, TypeScript functions, real-time subscriptions, auth, file storage, scheduling, and deployment.\"\nrisk: safe\nsource: \"https:\u002F\u002Fdocs.convex.dev\"\ndate_added: \"2026-02-27\"\n---\n\n# Convex\n\nYou 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.\n\n## When to Use\n- Use when building a new project with Convex as the backend\n- Use when adding Convex to an existing React, Next.js, Angular, Vue, Svelte, or React Native app\n- Use when designing schemas for a Convex document-relational database\n- Use when writing or debugging Convex functions (queries, mutations, actions)\n- Use when implementing real-time\u002Freactive data patterns\n- Use when setting up authentication with Convex Auth or third-party providers (Clerk, Auth0, etc.)\n- Use when working with Convex file storage, scheduled functions, or cron jobs\n- Use when deploying or managing Convex projects\n\n## Core Concepts\n\nConvex is a **document-relational** database with a fully managed backend. Key differentiators:\n\n- **Reactive by default**: Queries automatically re-run and push updates to all connected clients when underlying data changes\n- **TypeScript-first**: All backend logic — queries, mutations, actions, schemas — is written in TypeScript\n- **ACID transactions**: Serializable isolation with optimistic concurrency control\n- **No infrastructure to manage**: Serverless, scales automatically, zero config\n- **End-to-end type safety**: Types flow from schema → backend functions → client hooks\n\n### Function Types\n\n| Type            | Purpose                   | Can Read DB    | Can Write DB      | Can Call External APIs | Cached\u002FReactive |\n| :-------------- | :------------------------ | :------------- | :---------------- | :--------------------- | :-------------- |\n| **Query**       | Read data                 | ✅             | ❌                | ❌                     | ✅              |\n| **Mutation**    | Write data                | ✅             | ✅                | ❌                     | ❌              |\n| **Action**      | Side effects              | via `runQuery` | via `runMutation` | ✅                     | ❌              |\n| **HTTP Action** | Webhooks\u002Fcustom endpoints | via `runQuery` | via `runMutation` | ✅                     | ❌              |\n\n## Project Setup\n\n### New Project (Next.js)\n\n```bash\nnpx create-next-app@latest my-app\ncd my-app && npm install convex\nnpx convex dev\n```\n\n### Add to Existing Project\n\n```bash\nnpm install convex\nnpx convex dev\n```\n\nThe `npx convex dev` command:\n\n1. Prompts you to log in (GitHub)\n2. Creates a project and deployment\n3. Generates `convex\u002F` folder for backend functions\n4. Syncs functions to your dev deployment in real-time\n5. Creates `.env.local` with `CONVEX_DEPLOYMENT` and `NEXT_PUBLIC_CONVEX_URL`\n\n### Folder Structure\n\n```\nmy-app\u002F\n├── convex\u002F\n│   ├── _generated\u002F        ← Auto-generated (DO NOT EDIT)\n│   │   ├── api.d.ts\n│   │   ├── dataModel.d.ts\n│   │   └── server.d.ts\n│   ├── schema.ts          ← Database schema definition\n│   ├── tasks.ts           ← Query\u002Fmutation functions\n│   └── http.ts            ← HTTP actions (optional)\n├── .env.local             ← CONVEX_DEPLOYMENT, NEXT_PUBLIC_CONVEX_URL\n└── convex.json            ← Project config (optional)\n```\n\n## Schema Design\n\nDefine your schema in `convex\u002Fschema.ts` using the validator library:\n\n```typescript\nimport { defineSchema, defineTable } from \"convex\u002Fserver\";\nimport { v } from \"convex\u002Fvalues\";\n\nexport default defineSchema({\n  users: defineTable({\n    name: v.string(),\n    email: v.string(),\n    avatarUrl: v.optional(v.string()),\n    tokenIdentifier: v.string(),\n  })\n    .index(\"by_token\", [\"tokenIdentifier\"])\n    .index(\"by_email\", [\"email\"]),\n\n  messages: defineTable({\n    authorId: v.id(\"users\"),\n    channelId: v.id(\"channels\"),\n    body: v.string(),\n    attachmentId: v.optional(v.id(\"_storage\")),\n  })\n    .index(\"by_channel\", [\"channelId\"])\n    .searchIndex(\"search_body\", { searchField: \"body\" }),\n\n  channels: defineTable({\n    name: v.string(),\n    description: v.optional(v.string()),\n    isPrivate: v.boolean(),\n  }),\n});\n```\n\n### Validator Types\n\n| Validator                         | TypeScript Type       | Notes                                          |\n| :-------------------------------- | :-------------------- | :--------------------------------------------- |\n| `v.string()`                      | `string`              |                                                |\n| `v.number()`                      | `number`              | IEEE 754 float                                 |\n| `v.bigint()`                      | `bigint`              |                                                |\n| `v.boolean()`                     | `boolean`             |                                                |\n| `v.null()`                        | `null`                |                                                |\n| `v.id(\"tableName\")`               | `Id\u003C\"tableName\">`     | Document reference                             |\n| `v.array(v.string())`             | `string[]`            |                                                |\n| `v.object({...})`                 | `{...}`               | Nested objects                                 |\n| `v.optional(v.string())`          | `string \\| undefined` |                                                |\n| `v.union(v.string(), v.number())` | `string \\| number`    |                                                |\n| `v.literal(\"active\")`             | `\"active\"`            | Literal types                                  |\n| `v.bytes()`                       | `ArrayBuffer`         | Binary data                                    |\n| `v.float64()`                     | `number`              | Explicit 64-bit float (used in vector indexes) |\n| `v.any()`                         | `any`                 | Escape hatch                                   |\n\n### Indexes\n\n```typescript\n\u002F\u002F Single-field index\ndefineTable({ email: v.string() }).index(\"by_email\", [\"email\"]);\n\n\u002F\u002F Compound index (order matters for range queries)\ndefineTable({\n  orgId: v.string(),\n  createdAt: v.number(),\n}).index(\"by_org_and_date\", [\"orgId\", \"createdAt\"]);\n\n\u002F\u002F Full-text search index\ndefineTable({ body: v.string(), channelId: v.id(\"channels\") }).searchIndex(\n  \"search_body\",\n  {\n    searchField: \"body\",\n    filterFields: [\"channelId\"],\n  },\n);\n\n\u002F\u002F Vector search index (for AI\u002Fembeddings)\ndefineTable({ embedding: v.array(v.float64()), text: v.string() }).vectorIndex(\n  \"by_embedding\",\n  {\n    vectorField: \"embedding\",\n    dimensions: 1536,\n  },\n);\n```\n\n## Writing Functions\n\n### Queries (Read Data)\n\nQueries are reactive — clients automatically get updates when data changes.\n\n````typescript\nimport { query } from \".\u002F_generated\u002Fserver\";\nimport { v } from \"convex\u002Fvalues\";\n\n\u002F\u002F Simple query — list all tasks\nexport const list = query({\n  args: {},\n  handler: async (ctx) => {\n    return await ctx.db.query(\"tasks\").collect();\n  },\n});\n\n\u002F\u002F Query with arguments and filtering\nexport const getByChannel = query({\n  args: { channelId: v.id(\"channels\") },\n  handler: async (ctx, args) => {\n    return await ctx.db\n      .query(\"messages\")\n      .withIndex(\"by_channel\", (q) => q.eq(\"channelId\", args.channelId))\n      .order(\"desc\")\n      .take(50);\n  },\n});\n\n\u002F\u002F Query with auth check\nexport const getMyProfile = query({\n  args: {},\n  handler: async (ctx) => {\n    const identity = await ctx.auth.getUserIdentity();\n    if (!identity) return null;\n\n    return await ctx.db\n      .query(\"users\")\n      .withIndex(\"by_token\", (q) =>\n        q.eq(\"tokenIdentifier\", identity.tokenIdentifier),\n      )\n      .unique();\n  },\n});\n\n### Paginated Queries\n\nUse cursor-based pagination for lists or infinite scroll UIs.\n\n```typescript\nimport { query } from \".\u002F_generated\u002Fserver\";\nimport { paginationOptsValidator } from \"convex\u002Fserver\";\n\nexport const listPaginated = query({\n  args: {\n    paginationOpts: paginationOptsValidator\n  },\n  handler: async (ctx, args) => {\n    return await ctx.db\n      .query(\"messages\")\n      .order(\"desc\")\n      .paginate(args.paginationOpts);\n  },\n});\n```\n\n### Mutations (Write Data)\n\nMutations run as ACID transactions with serializable isolation.\n\n```typescript\nimport { mutation } from \".\u002F_generated\u002Fserver\";\nimport { v } from \"convex\u002Fvalues\";\n\n\u002F\u002F Insert a document\nexport const create = mutation({\n  args: { text: v.string(), isCompleted: v.boolean() },\n  handler: async (ctx, args) => {\n    const taskId = await ctx.db.insert(\"tasks\", {\n      text: args.text,\n      isCompleted: args.isCompleted,\n    });\n    return taskId;\n  },\n});\n\n\u002F\u002F Update a document\nexport const update = mutation({\n  args: { id: v.id(\"tasks\"), isCompleted: v.boolean() },\n  handler: async (ctx, args) => {\n    await ctx.db.patch(args.id, { isCompleted: args.isCompleted });\n  },\n});\n\n\u002F\u002F Delete a document\nexport const remove = mutation({\n  args: { id: v.id(\"tasks\") },\n  handler: async (ctx, args) => {\n    await ctx.db.delete(args.id);\n  },\n});\n\n\u002F\u002F Multi-document transaction (automatically atomic)\nexport const transferCredits = mutation({\n  args: {\n    fromUserId: v.id(\"users\"),\n    toUserId: v.id(\"users\"),\n    amount: v.number(),\n  },\n  handler: async (ctx, args) => {\n    const fromUser = await ctx.db.get(args.fromUserId);\n    const toUser = await ctx.db.get(args.toUserId);\n    if (!fromUser || !toUser) throw new Error(\"User not found\");\n    if (fromUser.credits \u003C args.amount) throw new Error(\"Insufficient credits\");\n\n    await ctx.db.patch(args.fromUserId, {\n      credits: fromUser.credits - args.amount,\n    });\n    await ctx.db.patch(args.toUserId, {\n      credits: toUser.credits + args.amount,\n    });\n  },\n});\n````\n\n### Actions (External APIs & Side Effects)\n\nActions can call third-party services but cannot directly access the database — they must use `ctx.runQuery` and `ctx.runMutation`.\n\n```typescript\nimport { action } from \".\u002F_generated\u002Fserver\";\nimport { v } from \"convex\u002Fvalues\";\nimport { api } from \".\u002F_generated\u002Fapi\";\n\nexport const sendEmail = action({\n  args: { to: v.string(), subject: v.string(), body: v.string() },\n  handler: async (ctx, args) => {\n    \u002F\u002F Call external API\n    const response = await fetch(\"https:\u002F\u002Fapi.sendgrid.com\u002Fv3\u002Fmail\u002Fsend\", {\n      method: \"POST\",\n      headers: {\n        Authorization: `Bearer ${process.env.SENDGRID_API_KEY}`,\n        \"Content-Type\": \"application\u002Fjson\",\n      },\n      body: JSON.stringify({\n        personalizations: [{ to: [{ email: args.to }] }],\n        from: { email: \"noreply@example.com\" },\n        subject: args.subject,\n        content: [{ type: \"text\u002Fplain\", value: args.body }],\n      }),\n    });\n\n    if (!response.ok) throw new Error(\"Failed to send email\");\n\n    \u002F\u002F Write result back to database via mutation\n    await ctx.runMutation(api.emails.recordSent, {\n      to: args.to,\n      subject: args.subject,\n      sentAt: Date.now(),\n    });\n  },\n});\n\n\u002F\u002F Generate AI embeddings\nexport const generateEmbedding = action({\n  args: { text: v.string(), documentId: v.id(\"documents\") },\n  handler: async (ctx, args) => {\n    const response = await fetch(\"https:\u002F\u002Fapi.openai.com\u002Fv1\u002Fembeddings\", {\n      method: \"POST\",\n      headers: {\n        Authorization: `Bearer ${process.env.OPENAI_API_KEY}`,\n        \"Content-Type\": \"application\u002Fjson\",\n      },\n      body: JSON.stringify({\n        model: \"text-embedding-3-small\",\n        input: args.text,\n      }),\n    });\n\n    const { data } = await response.json();\n    await ctx.runMutation(api.documents.saveEmbedding, {\n      documentId: args.documentId,\n      embedding: data[0].embedding,\n    });\n  },\n});\n```\n\n### HTTP Actions (Webhooks)\n\n```typescript\nimport { httpRouter } from \"convex\u002Fserver\";\nimport { httpAction } from \".\u002F_generated\u002Fserver\";\nimport { api } from \".\u002F_generated\u002Fapi\";\n\nconst http = httpRouter();\n\nhttp.route({\n  path: \"\u002Fwebhooks\u002Fstripe\",\n  method: \"POST\",\n  handler: httpAction(async (ctx, request) => {\n    const body = await request.text();\n    const signature = request.headers.get(\"stripe-signature\");\n\n    \u002F\u002F Verify webhook signature here...\n\n    const event = JSON.parse(body);\n    await ctx.runMutation(api.payments.handleWebhook, { event });\n\n    return new Response(\"OK\", { status: 200 });\n  }),\n});\n\nexport default http;\n```\n\n## Client-Side Integration\n\n### React \u002F Next.js\n\n```typescript\n\u002F\u002F app\u002FConvexClientProvider.tsx\n\"use client\";\nimport { ConvexProvider, ConvexReactClient } from \"convex\u002Freact\";\nimport { ReactNode } from \"react\";\n\nconst convex = new ConvexReactClient(process.env.NEXT_PUBLIC_CONVEX_URL!);\n\nexport function ConvexClientProvider({ children }: { children: ReactNode }) {\n  return \u003CConvexProvider client={convex}>{children}\u003C\u002FConvexProvider>;\n}\n```\n\n```typescript\n\u002F\u002F app\u002Flayout.tsx — wrap children\nimport { ConvexClientProvider } from \".\u002FConvexClientProvider\";\n\nexport default function RootLayout({ children }: { children: React.ReactNode }) {\n  return (\n    \u003Chtml lang=\"en\">\n      \u003Cbody>\n        \u003CConvexClientProvider>{children}\u003C\u002FConvexClientProvider>\n      \u003C\u002Fbody>\n    \u003C\u002Fhtml>\n  );\n}\n```\n\n```typescript\n\u002F\u002F Component using Convex hooks\n\"use client\";\nimport { useQuery, useMutation } from \"convex\u002Freact\";\nimport { api } from \"@\u002Fconvex\u002F_generated\u002Fapi\";\n\nexport function TaskList() {\n  \u002F\u002F Reactive query — auto-updates when data changes\n  const tasks = useQuery(api.tasks.list);\n  const addTask = useMutation(api.tasks.create);\n  const toggleTask = useMutation(api.tasks.update);\n\n  if (tasks === undefined) return \u003Cp>Loading...\u003C\u002Fp>;\n\n  return (\n    \u003Cdiv>\n      {tasks.map((task) => (\n        \u003Cdiv key={task._id}>\n          \u003Cinput\n            type=\"checkbox\"\n            checked={task.isCompleted}\n            onChange={() =>\n              toggleTask({ id: task._id, isCompleted: !task.isCompleted })\n            }\n          \u002F>\n          {task.text}\n        \u003C\u002Fdiv>\n      ))}\n      \u003Cbutton onClick={() => addTask({ text: \"New task\", isCompleted: false })}>\n        Add Task\n      \u003C\u002Fbutton>\n    \u003C\u002Fdiv>\n  );\n}\n```\n\n```typescript\n\u002F\u002F Component using Paginated Queries\n\"use client\";\nimport { usePaginatedQuery } from \"convex\u002Freact\";\nimport { api } from \"@\u002Fconvex\u002F_generated\u002Fapi\";\n\nexport function MessageLog() {\n  const { results, status, loadMore } = usePaginatedQuery(\n    api.messages.listPaginated,\n    {}, \u002F\u002F args\n    { initialNumItems: 20 }\n  );\n\n  return (\n    \u003Cdiv>\n      {results.map((msg) => (\n        \u003Cdiv key={msg._id}>{msg.body}\u003C\u002Fdiv>\n      ))}\n\n      {status === \"LoadingFirstPage\" && \u003Cp>Loading...\u003C\u002Fp>}\n\n      {status === \"CanLoadMore\" && (\n        \u003Cbutton onClick={() => loadMore(20)}>Load More\u003C\u002Fbutton>\n      )}\n    \u003C\u002Fdiv>\n  );\n}\n```\n\n### With Auth (First-Party Convex Auth)\n\nConvex provides a robust, native authentication library (`@convex-dev\u002Fauth`) featuring Magic Links, Passwords, and 80+ OAuth providers without needing a third-party service.\n\n```typescript\n\u002F\u002F app\u002FConvexClientProvider.tsx\n\"use client\";\nimport { ConvexAuthProvider } from \"@convex-dev\u002Fauth\u002Freact\";\nimport { ConvexReactClient } from \"convex\u002Freact\";\nimport { ReactNode } from \"react\";\n\nconst convex = new ConvexReactClient(process.env.NEXT_PUBLIC_CONVEX_URL!);\n\nexport function ConvexClientProvider({ children }: { children: ReactNode }) {\n  return (\n    \u003CConvexAuthProvider client={convex}>\n      {children}\n    \u003C\u002FConvexAuthProvider>\n  );\n}\n```\n\n```typescript\n\u002F\u002F Client-side sign in\nimport { useAuthActions } from \"@convex-dev\u002Fauth\u002Freact\";\n\nexport function Login() {\n  const { signIn } = useAuthActions();\n  return \u003Cbutton onClick={() => signIn(\"github\")}>Sign in with GitHub\u003C\u002Fbutton>;\n}\n```\n\n### With Auth (Third-Party Clerk Example)\n\nIf you prefer a hosted third-party solution like Clerk:\n\n```typescript\n\u002F\u002F app\u002FConvexClientProvider.tsx\n\"use client\";\nimport { ConvexProviderWithClerk } from \"convex\u002Freact-clerk\";\nimport { ClerkProvider, useAuth } from \"@clerk\u002Fnextjs\";\nimport { ConvexReactClient } from \"convex\u002Freact\";\n\nconst convex = new ConvexReactClient(process.env.NEXT_PUBLIC_CONVEX_URL!);\n\nexport function ConvexClientProvider({ children }: { children: ReactNode }) {\n  return (\n    \u003CClerkProvider publishableKey={process.env.NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY!}>\n      \u003CConvexProviderWithClerk client={convex} useAuth={useAuth}>\n        {children}\n      \u003C\u002FConvexProviderWithClerk>\n    \u003C\u002FClerkProvider>\n  );\n}\n```\n\n### With Auth (Better Auth Component)\n\nConvex also has a community component (`@convex-dev\u002Fbetter-auth`) that integrates the Better Auth library directly into the Convex backend. This is currently in **early alpha**.\n\n```bash\nnpm install better-auth @convex-dev\u002Fbetter-auth\nnpx convex env set BETTER_AUTH_SECRET your-secret-here\nnpx convex env set SITE_URL http:\u002F\u002Flocalhost:3000\n```\n\nBetter Auth provides email\u002Fpassword, social logins, two-factor authentication, and session management — all running inside Convex functions rather than an external auth server.\n\n### Angular Integration\n\nConvex does not have an official Angular client library, but Angular apps can use the core `convex` package directly with Angular's Dependency Injection and Signals.\n\n```typescript\n\u002F\u002F services\u002Fconvex.service.ts\nimport { Injectable, signal, effect, OnDestroy } from \"@angular\u002Fcore\";\nimport { ConvexClient } from \"convex\u002Fbrowser\";\nimport { api } from \"..\u002F..\u002Fconvex\u002F_generated\u002Fapi\";\nimport { FunctionReturnType } from \"convex\u002Fserver\";\n\n@Injectable({ providedIn: \"root\" })\nexport class ConvexService implements OnDestroy {\n  private client = new ConvexClient(environment.convexUrl);\n\n  \u002F\u002F Reactive signal — updates automatically when data changes\n  tasks = signal\u003CFunctionReturnType\u003Ctypeof api.tasks.list> | undefined>(\n    undefined,\n  );\n\n  constructor() {\n    \u002F\u002F Subscribe to a reactive query\n    this.client.onUpdate(api.tasks.list, {}, (result) => {\n      this.tasks.set(result);\n    });\n  }\n\n  async addTask(text: string) {\n    await this.client.mutation(api.tasks.create, {\n      text,\n      isCompleted: false,\n    });\n  }\n\n  ngOnDestroy() {\n    this.client.close();\n  }\n}\n```\n\n```typescript\n\u002F\u002F Component usage\nimport { Component, inject } from \"@angular\u002Fcore\";\nimport { ConvexService } from \".\u002Fservices\u002Fconvex.service\";\n\n@Component({\n  selector: \"app-task-list\",\n  template: `\n    @if (convex.tasks(); as tasks) {\n      @for (task of tasks; track task._id) {\n        \u003Cdiv>{{ task.text }}\u003C\u002Fdiv>\n      }\n    } @else {\n      \u003Cp>Loading...\u003C\u002Fp>\n    }\n    \u003Cbutton (click)=\"convex.addTask('New task')\">Add Task\u003C\u002Fbutton>\n  `,\n})\nexport class TaskListComponent {\n  convex = inject(ConvexService);\n}\n```\n\n> **Note:** The community library `@robmanganelly\u002Fngx-convex` provides a more Angular-native experience with React-like hooks adapted for Angular DI and Signals.\n\n## Scheduling & Cron Jobs\n\n### One-off Scheduled Functions\n\n```typescript\nimport { mutation } from \".\u002F_generated\u002Fserver\";\nimport { api } from \".\u002F_generated\u002Fapi\";\n\nexport const sendReminder = mutation({\n  args: { userId: v.id(\"users\"), message: v.string(), delayMs: v.number() },\n  handler: async (ctx, args) => {\n    await ctx.scheduler.runAfter(args.delayMs, api.notifications.send, {\n      userId: args.userId,\n      message: args.message,\n    });\n  },\n});\n```\n\n### Cron Jobs\n\n```typescript\n\u002F\u002F convex\u002Fcrons.ts\nimport { cronJobs } from \"convex\u002Fserver\";\nimport { api } from \".\u002F_generated\u002Fapi\";\n\nconst crons = cronJobs();\n\ncrons.interval(\"clear old logs\", { hours: 24 }, api.logs.clearOld);\n\ncrons.cron(\n  \"weekly digest\",\n  \"0 9 * * 1\", \u002F\u002F Every Monday at 9 AM\n  api.emails.sendWeeklyDigest,\n);\n\nexport default crons;\n```\n\n## File Storage\n\n```typescript\n\u002F\u002F Generate an upload URL (mutation)\nexport const generateUploadUrl = mutation({\n  args: {},\n  handler: async (ctx) => {\n    return await ctx.storage.generateUploadUrl();\n  },\n});\n\n\u002F\u002F Save file reference after upload (mutation)\nexport const saveFile = mutation({\n  args: { storageId: v.id(\"_storage\"), name: v.string() },\n  handler: async (ctx, args) => {\n    await ctx.db.insert(\"files\", {\n      storageId: args.storageId,\n      name: args.name,\n    });\n  },\n});\n\n\u002F\u002F Get a URL to serve a file (query)\nexport const getFileUrl = query({\n  args: { storageId: v.id(\"_storage\") },\n  handler: async (ctx, args) => {\n    return await ctx.storage.getUrl(args.storageId);\n  },\n});\n```\n\n## Environment Variables\n\n```bash\n# Set environment variables for your deployment\nnpx convex env set OPENAI_API_KEY sk-...\nnpx convex env set SENDGRID_API_KEY SG...\n\n# List current env vars\nnpx convex env list\n\n# Remove an env var\nnpx convex env unset OPENAI_API_KEY\n```\n\nAccess in actions (NOT in queries or mutations):\n\n```typescript\n\u002F\u002F Only available in actions\nconst apiKey = process.env.OPENAI_API_KEY;\n```\n\n## Deployment & CLI\n\n```bash\n# Development (watches for changes, syncs to dev deployment)\nnpx convex dev\n\n# Deploy to production\nnpx convex deploy\n\n# Import data\nnpx convex import --table tasks data.jsonl\n\n# Export data\nnpx convex export --path .\u002Fbackup\n\n# Open Convex dashboard\nnpx convex dashboard\n\n# Run a function from CLI\nnpx convex run tasks:list\n\n# View logs\nnpx convex logs\n```\n\n## Best Practices\n\n- ✅ Define schemas — adds type safety across your entire stack\n- ✅ Use indexes for queries — avoids full table scans\n- ✅ Use compound indexes with equality filters first, range filter last\n- ✅ 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!\n- ✅ Use `v.id(\"tableName\")` for document references instead of plain strings\n- ✅ Use actions for external API calls (never call external APIs from queries or mutations)\n- ✅ Use `ctx.runQuery` \u002F `ctx.runMutation` from actions — never access `ctx.db` directly in actions\n- ✅ Add argument validators to all functions — they enforce runtime type safety\n- ✅ Return `null` when a document isn't found instead of throwing an error unless missing is exceptional\n- ✅ Prefer `withIndex` over `.filter()` for query performance\n\n## Anti-Patterns to Avoid\n\n1. **❌ External API calls in queries\u002Fmutations**: Only actions can call external services. Queries and mutations run in the Convex transaction engine.\n2. **❌ Doing slow CPU-bound work in mutations**: Mutations block database commits; offload heavy processing to actions.\n3. **❌ Using `.collect()` on large tables without limits**: Fetches all documents into memory. Use `.take(N)` or `.paginate()`.\n4. **❌ Skipping schema definition**: Without a schema you lose end-to-end type safety, the main Convex advantage.\n5. **❌ Using `.filter()` instead of indexes**: `.filter()` does a full table scan. Define an index and use `.withIndex()`.\n6. **❌ Storing large blobs in documents**: Use Convex file storage (`_storage`) for files; keep documents lean.\n7. **❌ Circular `runQuery`\u002F`runMutation` chains**: Actions calling mutations that schedule actions can create infinite loops.\n\n## Common Pitfalls\n\n- **Problem:** \"Query returns `undefined` on first render\"\n  **Solution:** This is expected — Convex queries are async. Check for `undefined` before rendering (this means loading, not empty).\n\n- **Problem:** \"Mutation throws `Document not found`\"\n  **Solution:** Documents may have been deleted between your read and write due to optimistic concurrency. Re-read inside the mutation.\n\n- **Problem:** \"`process.env` is undefined in query\u002Fmutation\"\n  **Solution:** Environment variables are only accessible in **actions** (not queries or mutations) because queries\u002Fmutations run in the deterministic transaction engine.\n\n- **Problem:** \"Function handler is too slow\"\n  **Solution:** Add indexes for your query patterns. Use `withIndex()` instead of `.filter()`. For complex operations, break into smaller mutations.\n\n- **Problem:** \"Schema push fails with existing data\"\n  **Solution:** Convex validates existing data against new schemas. Either migrate existing documents first, or use `v.optional()` for new fields.\n\n## Limitations\n\n- Queries and mutations cannot call external HTTP APIs (use actions instead)\n- No raw SQL — you work with the Convex query builder API\n- Environment variables only available in actions, not in queries or mutations\n- Document size limit of 1MB\n- Maximum function execution time limits apply\n- No server-side rendering of Convex data without specific SSR patterns (use preloading)\n- Schemas are enforced at write-time; changing schemas requires data migration for existing documents\n\n## Related Skills\n\n- `@firebase` — Alternative BaaS with Firestore (compare: Convex is TypeScript-first with ACID transactions)\n- `@supabase-automation` — Alternative with PostgreSQL backend (compare: Convex is document-relational with built-in reactivity)\n- `@prisma-expert` — ORM for traditional databases (Convex replaces both ORM and database)\n- `@react-patterns` — Frontend patterns that pair well with Convex React hooks\n- `@nextjs-app-router` — Next.js App Router integration patterns\n- `@authentication-oauth` — Auth patterns (Convex supports Clerk, Auth0, Convex Auth)\n- `@stripe` — Payment integration via Convex actions and HTTP webhooks\n\n## Resources\n\n- [Official Docs](https:\u002F\u002Fdocs.convex.dev)\n- [Convex Stack (Blog)](https:\u002F\u002Fstack.convex.dev)\n- [GitHub](https:\u002F\u002Fgithub.com\u002Fget-convex\u002Fconvex-backend)\n- [Discord Community](https:\u002F\u002Fconvex.dev\u002Fcommunity)\n- [Convex Chef (AI Starter)](https:\u002F\u002Fchef.convex.dev)\n","# Convex\n\nYou 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.\n\n## When to Use\n- Use when building a new project with Convex as the backend\n- Use when adding Convex to an existing React, Next.js, Angular, Vue, Svelte, or React Native app\n- Use when designing schemas for a Convex document-relational database\n- Use when writing or debugging Convex functions (queries, mutations, actions)\n- Use when implementing real-time\u002Freactive data patterns\n- Use when setting up authentication with Convex Auth or third-party providers (Clerk, Auth0, etc.)\n- Use when working with Convex file storage, scheduled functions, or cron jobs\n- Use when deploying or managing Convex projects\n\n## Core Concepts\n\nConvex is a **document-relational** database with a fully managed backend. Key differentiators:\n\n- **Reactive by default**: Queries automatically re-run and push updates to all connected clients when underlying data changes\n- **TypeScript-first**: All backend logic — queries, mutations, actions, schemas — is written in TypeScript\n- **ACID transactions**: Serializable isolation with optimistic concurrency control\n- **No infrastructure to manage**: Serverless, scales automatically, zero config\n- **End-to-end type safety**: Types flow from schema → backend functions → client hooks\n\n### Function Types\n\n| Type            | Purpose                   | Can Read DB    | Can Write DB      | Can Call External APIs | Cached\u002FReactive |\n| :-------------- | :------------------------ | :------------- | :---------------- | :--------------------- | :-------------- |\n| **Query**       | Read data                 | ✅             | ❌                | ❌                     | ✅              |\n| **Mutation**    | Write data                | ✅             | ✅                | ❌                     | ❌              |\n| **Action**      | Side effects              | via `runQuery` | via `runMutation` | ✅                     | ❌              |\n| **HTTP Action** | Webhooks\u002Fcustom endpoints | via `runQuery` | via `runMutation` | ✅                     | ❌              |\n\n## Project Setup\n\n### New Project (Next.js)\n\n```bash\nnpx create-next-app@latest my-app\ncd my-app && npm install convex\nnpx convex dev\n```\n\n### Add to Existing Project\n\n```bash\nnpm install convex\nnpx convex dev\n```\n\nThe `npx convex dev` command:\n\n1. Prompts you to log in (GitHub)\n2. Creates a project and deployment\n3. Generates `convex\u002F` folder for backend functions\n4. Syncs functions to your dev deployment in real-time\n5. Creates `.env.local` with `CONVEX_DEPLOYMENT` and `NEXT_PUBLIC_CONVEX_URL`\n\n### Folder Structure\n\n```\nmy-app\u002F\n├── convex\u002F\n│   ├── _generated\u002F        ← Auto-generated (DO NOT EDIT)\n│   │   ├── api.d.ts\n│   │   ├── dataModel.d.ts\n│   │   └── server.d.ts\n│   ├── schema.ts          ← Database schema definition\n│   ├── tasks.ts           ← Query\u002Fmutation functions\n│   └── http.ts            ← HTTP actions (optional)\n├── .env.local             ← CONVEX_DEPLOYMENT, NEXT_PUBLIC_CONVEX_URL\n└── convex.json            ← Project config (optional)\n```\n\n## Schema Design\n\nDefine your schema in `convex\u002Fschema.ts` using the validator library:\n\n```typescript\nimport { defineSchema, defineTable } from \"convex\u002Fserver\";\nimport { v } from \"convex\u002Fvalues\";\n\nexport default defineSchema({\n  users: defineTable({\n    name: v.string(),\n    email: v.string(),\n    avatarUrl: v.optional(v.string()),\n    tokenIdentifier: v.string(),\n  })\n    .index(\"by_token\", [\"tokenIdentifier\"])\n    .index(\"by_email\", [\"email\"]),\n\n  messages: defineTable({\n    authorId: v.id(\"users\"),\n    channelId: v.id(\"channels\"),\n    body: v.string(),\n    attachmentId: v.optional(v.id(\"_storage\")),\n  })\n    .index(\"by_channel\", [\"channelId\"])\n    .searchIndex(\"search_body\", { searchField: \"body\" }),\n\n  channels: defineTable({\n    name: v.string(),\n    description: v.optional(v.string()),\n    isPrivate: v.boolean(),\n  }),\n});\n```\n\n### Validator Types\n\n| Validator                         | TypeScript Type       | Notes                                          |\n| :-------------------------------- | :-------------------- | :--------------------------------------------- |\n| `v.string()`                      | `string`              |                                                |\n| `v.number()`                      | `number`              | IEEE 754 float                                 |\n| `v.bigint()`                      | `bigint`              |                                                |\n| `v.boolean()`                     | `boolean`             |                                                |\n| `v.null()`                        | `null`                |                                                |\n| `v.id(\"tableName\")`               | `Id\u003C\"tableName\">`     | Document reference                             |\n| `v.array(v.string())`             | `string[]`            |                                                |\n| `v.object({...})`                 | `{...}`               | Nested objects                                 |\n| `v.optional(v.string())`          | `string \\| undefined` |                                                |\n| `v.union(v.string(), v.number())` | `string \\| number`    |                                                |\n| `v.literal(\"active\")`             | `\"active\"`            | Literal types                                  |\n| `v.bytes()`                       | `ArrayBuffer`         | Binary data                                    |\n| `v.float64()`                     | `number`              | Explicit 64-bit float (used in vector indexes) |\n| `v.any()`                         | `any`                 | Escape hatch                                   |\n\n### Indexes\n\n```typescript\n\u002F\u002F Single-field index\ndefineTable({ email: v.string() }).index(\"by_email\", [\"email\"]);\n\n\u002F\u002F Compound index (order matters for range queries)\ndefineTable({\n  orgId: v.string(),\n  createdAt: v.number(),\n}).index(\"by_org_and_date\", [\"orgId\", \"createdAt\"]);\n\n\u002F\u002F Full-text search index\ndefineTable({ body: v.string(), channelId: v.id(\"channels\") }).searchIndex(\n  \"search_body\",\n  {\n    searchField: \"body\",\n    filterFields: [\"channelId\"],\n  },\n);\n\n\u002F\u002F Vector search index (for AI\u002Fembeddings)\ndefineTable({ embedding: v.array(v.float64()), text: v.string() }).vectorIndex(\n  \"by_embedding\",\n  {\n    vectorField: \"embedding\",\n    dimensions: 1536,\n  },\n);\n```\n\n## Writing Functions\n\n### Queries (Read Data)\n\nQueries are reactive — clients automatically get updates when data changes.\n\n````typescript\nimport { query } from \".\u002F_generated\u002Fserver\";\nimport { v } from \"convex\u002Fvalues\";\n\n\u002F\u002F Simple query — list all tasks\nexport const list = query({\n  args: {},\n  handler: async (ctx) => {\n    return await ctx.db.query(\"tasks\").collect();\n  },\n});\n\n\u002F\u002F Query with arguments and filtering\nexport const getByChannel = query({\n  args: { channelId: v.id(\"channels\") },\n  handler: async (ctx, args) => {\n    return await ctx.db\n      .query(\"messages\")\n      .withIndex(\"by_channel\", (q) => q.eq(\"channelId\", args.channelId))\n      .order(\"desc\")\n      .take(50);\n  },\n});\n\n\u002F\u002F Query with auth check\nexport const getMyProfile = query({\n  args: {},\n  handler: async (ctx) => {\n    const identity = await ctx.auth.getUserIdentity();\n    if (!identity) return null;\n\n    return await ctx.db\n      .query(\"users\")\n      .withIndex(\"by_token\", (q) =>\n        q.eq(\"tokenIdentifier\", identity.tokenIdentifier),\n      )\n      .unique();\n  },\n});\n\n### Paginated Queries\n\nUse cursor-based pagination for lists or infinite scroll UIs.\n\n```typescript\nimport { query } from \".\u002F_generated\u002Fserver\";\nimport { paginationOptsValidator } from \"convex\u002Fserver\";\n\nexport const listPaginated = query({\n  args: {\n    paginationOpts: paginationOptsValidator\n  },\n  handler: async (ctx, args) => {\n    return await ctx.db\n      .query(\"messages\")\n      .order(\"desc\")\n      .paginate(args.paginationOpts);\n  },\n});\n```\n\n### Mutations (Write Data)\n\nMutations run as ACID transactions with serializable isolation.\n\n```typescript\nimport { mutation } from \".\u002F_generated\u002Fserver\";\nimport { v } from \"convex\u002Fvalues\";\n\n\u002F\u002F Insert a document\nexport const create = mutation({\n  args: { text: v.string(), isCompleted: v.boolean() },\n  handler: async (ctx, args) => {\n    const taskId = await ctx.db.insert(\"tasks\", {\n      text: args.text,\n      isCompleted: args.isCompleted,\n    });\n    return taskId;\n  },\n});\n\n\u002F\u002F Update a document\nexport const update = mutation({\n  args: { id: v.id(\"tasks\"), isCompleted: v.boolean() },\n  handler: async (ctx, args) => {\n    await ctx.db.patch(args.id, { isCompleted: args.isCompleted });\n  },\n});\n\n\u002F\u002F Delete a document\nexport const remove = mutation({\n  args: { id: v.id(\"tasks\") },\n  handler: async (ctx, args) => {\n    await ctx.db.delete(args.id);\n  },\n});\n\n\u002F\u002F Multi-document transaction (automatically atomic)\nexport const transferCredits = mutation({\n  args: {\n    fromUserId: v.id(\"users\"),\n    toUserId: v.id(\"users\"),\n    amount: v.number(),\n  },\n  handler: async (ctx, args) => {\n    const fromUser = await ctx.db.get(args.fromUserId);\n    const toUser = await ctx.db.get(args.toUserId);\n    if (!fromUser || !toUser) throw new Error(\"User not found\");\n    if (fromUser.credits \u003C args.amount) throw new Error(\"Insufficient credits\");\n\n    await ctx.db.patch(args.fromUserId, {\n      credits: fromUser.credits - args.amount,\n    });\n    await ctx.db.patch(args.toUserId, {\n      credits: toUser.credits + args.amount,\n    });\n  },\n});\n````\n\n### Actions (External APIs & Side Effects)\n\nActions can call third-party services but cannot directly access the database — they must use `ctx.runQuery` and `ctx.runMutation`.\n\n```typescript\nimport { action } from \".\u002F_generated\u002Fserver\";\nimport { v } from \"convex\u002Fvalues\";\nimport { api } from \".\u002F_generated\u002Fapi\";\n\nexport const sendEmail = action({\n  args: { to: v.string(), subject: v.string(), body: v.string() },\n  handler: async (ctx, args) => {\n    \u002F\u002F Call external API\n    const response = await fetch(\"https:\u002F\u002Fapi.sendgrid.com\u002Fv3\u002Fmail\u002Fsend\", {\n      method: \"POST\",\n      headers: {\n        Authorization: `Bearer ${process.env.SENDGRID_API_KEY}`,\n        \"Content-Type\": \"application\u002Fjson\",\n      },\n      body: JSON.stringify({\n        personalizations: [{ to: [{ email: args.to }] }],\n        from: { email: \"noreply@example.com\" },\n        subject: args.subject,\n        content: [{ type: \"text\u002Fplain\", value: args.body }],\n      }),\n    });\n\n    if (!response.ok) throw new Error(\"Failed to send email\");\n\n    \u002F\u002F Write result back to database via mutation\n    await ctx.runMutation(api.emails.recordSent, {\n      to: args.to,\n      subject: args.subject,\n      sentAt: Date.now(),\n    });\n  },\n});\n\n\u002F\u002F Generate AI embeddings\nexport const generateEmbedding = action({\n  args: { text: v.string(), documentId: v.id(\"documents\") },\n  handler: async (ctx, args) => {\n    const response = await fetch(\"https:\u002F\u002Fapi.openai.com\u002Fv1\u002Fembeddings\", {\n      method: \"POST\",\n      headers: {\n        Authorization: `Bearer ${process.env.OPENAI_API_KEY}`,\n        \"Content-Type\": \"application\u002Fjson\",\n      },\n      body: JSON.stringify({\n        model: \"text-embedding-3-small\",\n        input: args.text,\n      }),\n    });\n\n    const { data } = await response.json();\n    await ctx.runMutation(api.documents.saveEmbedding, {\n      documentId: args.documentId,\n      embedding: data[0].embedding,\n    });\n  },\n});\n```\n\n### HTTP Actions (Webhooks)\n\n```typescript\nimport { httpRouter } from \"convex\u002Fserver\";\nimport { httpAction } from \".\u002F_generated\u002Fserver\";\nimport { api } from \".\u002F_generated\u002Fapi\";\n\nconst http = httpRouter();\n\nhttp.route({\n  path: \"\u002Fwebhooks\u002Fstripe\",\n  method: \"POST\",\n  handler: httpAction(async (ctx, request) => {\n    const body = await request.text();\n    const signature = request.headers.get(\"stripe-signature\");\n\n    \u002F\u002F Verify webhook signature here...\n\n    const event = JSON.parse(body);\n    await ctx.runMutation(api.payments.handleWebhook, { event });\n\n    return new Response(\"OK\", { status: 200 });\n  }),\n});\n\nexport default http;\n```\n\n## Client-Side Integration\n\n### React \u002F Next.js\n\n```typescript\n\u002F\u002F app\u002FConvexClientProvider.tsx\n\"use client\";\nimport { ConvexProvider, ConvexReactClient } from \"convex\u002Freact\";\nimport { ReactNode } from \"react\";\n\nconst convex = new ConvexReactClient(process.env.NEXT_PUBLIC_CONVEX_URL!);\n\nexport function ConvexClientProvider({ children }: { children: ReactNode }) {\n  return \u003CConvexProvider client={convex}>{children}\u003C\u002FConvexProvider>;\n}\n```\n\n```typescript\n\u002F\u002F app\u002Flayout.tsx — wrap children\nimport { ConvexClientProvider } from \".\u002FConvexClientProvider\";\n\nexport default function RootLayout({ children }: { children: React.ReactNode }) {\n  return (\n    \u003Chtml lang=\"en\">\n      \u003Cbody>\n        \u003CConvexClientProvider>{children}\u003C\u002FConvexClientProvider>\n      \u003C\u002Fbody>\n    \u003C\u002Fhtml>\n  );\n}\n```\n\n```typescript\n\u002F\u002F Component using Convex hooks\n\"use client\";\nimport { useQuery, useMutation } from \"convex\u002Freact\";\nimport { api } from \"@\u002Fconvex\u002F_generated\u002Fapi\";\n\nexport function TaskList() {\n  \u002F\u002F Reactive query — auto-updates when data changes\n  const tasks = useQuery(api.tasks.list);\n  const addTask = useMutation(api.tasks.create);\n  const toggleTask = useMutation(api.tasks.update);\n\n  if (tasks === undefined) return \u003Cp>Loading...\u003C\u002Fp>;\n\n  return (\n    \u003Cdiv>\n      {tasks.map((task) => (\n        \u003Cdiv key={task._id}>\n          \u003Cinput\n            type=\"checkbox\"\n            checked={task.isCompleted}\n            onChange={() =>\n              toggleTask({ id: task._id, isCompleted: !task.isCompleted })\n            }\n          \u002F>\n          {task.text}\n        \u003C\u002Fdiv>\n      ))}\n      \u003Cbutton onClick={() => addTask({ text: \"New task\", isCompleted: false })}>\n        Add Task\n      \u003C\u002Fbutton>\n    \u003C\u002Fdiv>\n  );\n}\n```\n\n```typescript\n\u002F\u002F Component using Paginated Queries\n\"use client\";\nimport { usePaginatedQuery } from \"convex\u002Freact\";\nimport { api } from \"@\u002Fconvex\u002F_generated\u002Fapi\";\n\nexport function MessageLog() {\n  const { results, status, loadMore } = usePaginatedQuery(\n    api.messages.listPaginated,\n    {}, \u002F\u002F args\n    { initialNumItems: 20 }\n  );\n\n  return (\n    \u003Cdiv>\n      {results.map((msg) => (\n        \u003Cdiv key={msg._id}>{msg.body}\u003C\u002Fdiv>\n      ))}\n\n      {status === \"LoadingFirstPage\" && \u003Cp>Loading...\u003C\u002Fp>}\n\n      {status === \"CanLoadMore\" && (\n        \u003Cbutton onClick={() => loadMore(20)}>Load More\u003C\u002Fbutton>\n      )}\n    \u003C\u002Fdiv>\n  );\n}\n```\n\n### With Auth (First-Party Convex Auth)\n\nConvex provides a robust, native authentication library (`@convex-dev\u002Fauth`) featuring Magic Links, Passwords, and 80+ OAuth providers without needing a third-party service.\n\n```typescript\n\u002F\u002F app\u002FConvexClientProvider.tsx\n\"use client\";\nimport { ConvexAuthProvider } from \"@convex-dev\u002Fauth\u002Freact\";\nimport { ConvexReactClient } from \"convex\u002Freact\";\nimport { ReactNode } from \"react\";\n\nconst convex = new ConvexReactClient(process.env.NEXT_PUBLIC_CONVEX_URL!);\n\nexport function ConvexClientProvider({ children }: { children: ReactNode }) {\n  return (\n    \u003CConvexAuthProvider client={convex}>\n      {children}\n    \u003C\u002FConvexAuthProvider>\n  );\n}\n```\n\n```typescript\n\u002F\u002F Client-side sign in\nimport { useAuthActions } from \"@convex-dev\u002Fauth\u002Freact\";\n\nexport function Login() {\n  const { signIn } = useAuthActions();\n  return \u003Cbutton onClick={() => signIn(\"github\")}>Sign in with GitHub\u003C\u002Fbutton>;\n}\n```\n\n### With Auth (Third-Party Clerk Example)\n\nIf you prefer a hosted third-party solution like Clerk:\n\n```typescript\n\u002F\u002F app\u002FConvexClientProvider.tsx\n\"use client\";\nimport { ConvexProviderWithClerk } from \"convex\u002Freact-clerk\";\nimport { ClerkProvider, useAuth } from \"@clerk\u002Fnextjs\";\nimport { ConvexReactClient } from \"convex\u002Freact\";\n\nconst convex = new ConvexReactClient(process.env.NEXT_PUBLIC_CONVEX_URL!);\n\nexport function ConvexClientProvider({ children }: { children: ReactNode }) {\n  return (\n    \u003CClerkProvider publishableKey={process.env.NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY!}>\n      \u003CConvexProviderWithClerk client={convex} useAuth={useAuth}>\n        {children}\n      \u003C\u002FConvexProviderWithClerk>\n    \u003C\u002FClerkProvider>\n  );\n}\n```\n\n### With Auth (Better Auth Component)\n\nConvex also has a community component (`@convex-dev\u002Fbetter-auth`) that integrates the Better Auth library directly into the Convex backend. This is currently in **early alpha**.\n\n```bash\nnpm install better-auth @convex-dev\u002Fbetter-auth\nnpx convex env set BETTER_AUTH_SECRET your-secret-here\nnpx convex env set SITE_URL http:\u002F\u002Flocalhost:3000\n```\n\nBetter Auth provides email\u002Fpassword, social logins, two-factor authentication, and session management — all running inside Convex functions rather than an external auth server.\n\n### Angular Integration\n\nConvex does not have an official Angular client library, but Angular apps can use the core `convex` package directly with Angular's Dependency Injection and Signals.\n\n```typescript\n\u002F\u002F services\u002Fconvex.service.ts\nimport { Injectable, signal, effect, OnDestroy } from \"@angular\u002Fcore\";\nimport { ConvexClient } from \"convex\u002Fbrowser\";\nimport { api } from \"..\u002F..\u002Fconvex\u002F_generated\u002Fapi\";\nimport { FunctionReturnType } from \"convex\u002Fserver\";\n\n@Injectable({ providedIn: \"root\" })\nexport class ConvexService implements OnDestroy {\n  private client = new ConvexClient(environment.convexUrl);\n\n  \u002F\u002F Reactive signal — updates automatically when data changes\n  tasks = signal\u003CFunctionReturnType\u003Ctypeof api.tasks.list> | undefined>(\n    undefined,\n  );\n\n  constructor() {\n    \u002F\u002F Subscribe to a reactive query\n    this.client.onUpdate(api.tasks.list, {}, (result) => {\n      this.tasks.set(result);\n    });\n  }\n\n  async addTask(text: string) {\n    await this.client.mutation(api.tasks.create, {\n      text,\n      isCompleted: false,\n    });\n  }\n\n  ngOnDestroy() {\n    this.client.close();\n  }\n}\n```\n\n```typescript\n\u002F\u002F Component usage\nimport { Component, inject } from \"@angular\u002Fcore\";\nimport { ConvexService } from \".\u002Fservices\u002Fconvex.service\";\n\n@Component({\n  selector: \"app-task-list\",\n  template: `\n    @if (convex.tasks(); as tasks) {\n      @for (task of tasks; track task._id) {\n        \u003Cdiv>{{ task.text }}\u003C\u002Fdiv>\n      }\n    } @else {\n      \u003Cp>Loading...\u003C\u002Fp>\n    }\n    \u003Cbutton (click)=\"convex.addTask('New task')\">Add Task\u003C\u002Fbutton>\n  `,\n})\nexport class TaskListComponent {\n  convex = inject(ConvexService);\n}\n```\n\n> **Note:** The community library `@robmanganelly\u002Fngx-convex` provides a more Angular-native experience with React-like hooks adapted for Angular DI and Signals.\n\n## Scheduling & Cron Jobs\n\n### One-off Scheduled Functions\n\n```typescript\nimport { mutation } from \".\u002F_generated\u002Fserver\";\nimport { api } from \".\u002F_generated\u002Fapi\";\n\nexport const sendReminder = mutation({\n  args: { userId: v.id(\"users\"), message: v.string(), delayMs: v.number() },\n  handler: async (ctx, args) => {\n    await ctx.scheduler.runAfter(args.delayMs, api.notifications.send, {\n      userId: args.userId,\n      message: args.message,\n    });\n  },\n});\n```\n\n### Cron Jobs\n\n```typescript\n\u002F\u002F convex\u002Fcrons.ts\nimport { cronJobs } from \"convex\u002Fserver\";\nimport { api } from \".\u002F_generated\u002Fapi\";\n\nconst crons = cronJobs();\n\ncrons.interval(\"clear old logs\", { hours: 24 }, api.logs.clearOld);\n\ncrons.cron(\n  \"weekly digest\",\n  \"0 9 * * 1\", \u002F\u002F Every Monday at 9 AM\n  api.emails.sendWeeklyDigest,\n);\n\nexport default crons;\n```\n\n## File Storage\n\n```typescript\n\u002F\u002F Generate an upload URL (mutation)\nexport const generateUploadUrl = mutation({\n  args: {},\n  handler: async (ctx) => {\n    return await ctx.storage.generateUploadUrl();\n  },\n});\n\n\u002F\u002F Save file reference after upload (mutation)\nexport const saveFile = mutation({\n  args: { storageId: v.id(\"_storage\"), name: v.string() },\n  handler: async (ctx, args) => {\n    await ctx.db.insert(\"files\", {\n      storageId: args.storageId,\n      name: args.name,\n    });\n  },\n});\n\n\u002F\u002F Get a URL to serve a file (query)\nexport const getFileUrl = query({\n  args: { storageId: v.id(\"_storage\") },\n  handler: async (ctx, args) => {\n    return await ctx.storage.getUrl(args.storageId);\n  },\n});\n```\n\n## Environment Variables\n\n```bash\n# Set environment variables for your deployment\nnpx convex env set OPENAI_API_KEY sk-...\nnpx convex env set SENDGRID_API_KEY SG...\n\n# List current env vars\nnpx convex env list\n\n# Remove an env var\nnpx convex env unset OPENAI_API_KEY\n```\n\nAccess in actions (NOT in queries or mutations):\n\n```typescript\n\u002F\u002F Only available in actions\nconst apiKey = process.env.OPENAI_API_KEY;\n```\n\n## Deployment & CLI\n\n```bash\n# Development (watches for changes, syncs to dev deployment)\nnpx convex dev\n\n# Deploy to production\nnpx convex deploy\n\n# Import data\nnpx convex import --table tasks data.jsonl\n\n# Export data\nnpx convex export --path .\u002Fbackup\n\n# Open Convex dashboard\nnpx convex dashboard\n\n# Run a function from CLI\nnpx convex run tasks:list\n\n# View logs\nnpx convex logs\n```\n\n## Best Practices\n\n- ✅ Define schemas — adds type safety across your entire stack\n- ✅ Use indexes for queries — avoids full table scans\n- ✅ Use compound indexes with equality filters first, range filter last\n- ✅ 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!\n- ✅ Use `v.id(\"tableName\")` for document references instead of plain strings\n- ✅ Use actions for external API calls (never call external APIs from queries or mutations)\n- ✅ Use `ctx.runQuery` \u002F `ctx.runMutation` from actions — never access `ctx.db` directly in actions\n- ✅ Add argument validators to all functions — they enforce runtime type safety\n- ✅ Return `null` when a document isn't found instead of throwing an error unless missing is exceptional\n- ✅ Prefer `withIndex` over `.filter()` for query performance\n\n## Anti-Patterns to Avoid\n\n1. **❌ External API calls in queries\u002Fmutations**: Only actions can call external services. Queries and mutations run in the Convex transaction engine.\n2. **❌ Doing slow CPU-bound work in mutations**: Mutations block database commits; offload heavy processing to actions.\n3. **❌ Using `.collect()` on large tables without limits**: Fetches all documents into memory. Use `.take(N)` or `.paginate()`.\n4. **❌ Skipping schema definition**: Without a schema you lose end-to-end type safety, the main Convex advantage.\n5. **❌ Using `.filter()` instead of indexes**: `.filter()` does a full table scan. Define an index and use `.withIndex()`.\n6. **❌ Storing large blobs in documents**: Use Convex file storage (`_storage`) for files; keep documents lean.\n7. **❌ Circular `runQuery`\u002F`runMutation` chains**: Actions calling mutations that schedule actions can create infinite loops.\n\n## Common Pitfalls\n\n- **Problem:** \"Query returns `undefined` on first render\"\n  **Solution:** This is expected — Convex queries are async. Check for `undefined` before rendering (this means loading, not empty).\n\n- **Problem:** \"Mutation throws `Document not found`\"\n  **Solution:** Documents may have been deleted between your read and write due to optimistic concurrency. Re-read inside the mutation.\n\n- **Problem:** \"`process.env` is undefined in query\u002Fmutation\"\n  **Solution:** Environment variables are only accessible in **actions** (not queries or mutations) because queries\u002Fmutations run in the deterministic transaction engine.\n\n- **Problem:** \"Function handler is too slow\"\n  **Solution:** Add indexes for your query patterns. Use `withIndex()` instead of `.filter()`. For complex operations, break into smaller mutations.\n\n- **Problem:** \"Schema push fails with existing data\"\n  **Solution:** Convex validates existing data against new schemas. Either migrate existing documents first, or use `v.optional()` for new fields.\n\n## Limitations\n\n- Queries and mutations cannot call external HTTP APIs (use actions instead)\n- No raw SQL — you work with the Convex query builder API\n- Environment variables only available in actions, not in queries or mutations\n- Document size limit of 1MB\n- Maximum function execution time limits apply\n- No server-side rendering of Convex data without specific SSR patterns (use preloading)\n- Schemas are enforced at write-time; changing schemas requires data migration for existing documents\n\n## Related Skills\n\n- `@firebase` — Alternative BaaS with Firestore (compare: Convex is TypeScript-first with ACID transactions)\n- `@supabase-automation` — Alternative with PostgreSQL backend (compare: Convex is document-relational with built-in reactivity)\n- `@prisma-expert` — ORM for traditional databases (Convex replaces both ORM and database)\n- `@react-patterns` — Frontend patterns that pair well with Convex React hooks\n- `@nextjs-app-router` — Next.js App Router integration patterns\n- `@authentication-oauth` — Auth patterns (Convex supports Clerk, Auth0, Convex Auth)\n- `@stripe` — Payment integration via Convex actions and HTTP webhooks\n\n## Resources\n\n- [Official Docs](https:\u002F\u002Fdocs.convex.dev)\n- [Convex Stack (Blog)](https:\u002F\u002Fstack.convex.dev)\n- [GitHub](https:\u002F\u002Fgithub.com\u002Fget-convex\u002Fconvex-backend)\n- [Discord Community](https:\u002F\u002Fconvex.dev\u002Fcommunity)\n- [Convex Chef (AI Starter)](https:\u002F\u002Fchef.convex.dev)\n",{"name":5,"risk":42,"source":48,"date_added":49,"description":7},"https:\u002F\u002Fdocs.convex.dev","2026-02-27","main","7a38d8e7073e61c16eda85681cb32fb68a8657a5","skills-store\u002Fcursor\u002Fconvex","https:\u002F\u002Fgithub.com\u002Fsapsapshen\u002Fai-memory-router","https:\u002F\u002Fgithub.com\u002Fsapsapshen","2026-04-14 21:12:12",{"source":8,"skillName":5,"commands":57,"prompt":67,"zipAvailable":68},[58,61,64],{"tool":59,"command":60},"npx","npx skills add sapsapshen\u002Fai-memory-router --skill convex",{"tool":62,"command":63},"bunx","bunx skills add sapsapshen\u002Fai-memory-router --skill convex",{"tool":65,"command":66},"pnpm","pnpm dlx skills add sapsapshen\u002Fai-memory-router --skill convex","安装技能，技能地址：https:\u002F\u002Fgithub.com\u002Fsapsapshen\u002Fai-memory-router\u002Fblob\u002Fmain\u002Fskills-store\u002Fcursor\u002Fconvex\u002FSKILL.md",true,[],[71,94,119,129,142,159],{"skillNo":72,"name":73,"nameZh":6,"description":74,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":75,"categories":76,"keywords":82,"publishedAt":43},"01M71ATQRKG48SH06FQ697T5G9","wechat-search","Search WeChat Official Account articles using OpenClaw's web search and fetch capabilities with compliance-focused design.","https:\u002F\u002Fgithub.com\u002Fsapsapshen\u002Fai-memory-router\u002Fblob\u002Fmain\u002Fskills-store\u002Fopenclaw\u002Fwechat-search-release\u002FSKILL.md",[77],{"categoryNo":78,"categoryName":79,"taxonomyType":80,"taxonomyTypeDesc":81},"01JBSEEDCAT000000000000037","律师与法务","PROFESSION","适用职业",[83,84,85,86,87,25,88,89,90,91,92,93],"wechat search","wechat","search","capabilities","openclaw","web search","compliance","web","official","fetch","with",{"skillNo":95,"name":96,"nameZh":6,"description":97,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":98,"categories":99,"keywords":100,"publishedAt":43},"01M71ATNAVGZB35PFYM2GC5N00","trend-tap","Real-time trending topics aggregator across 7 platforms (X\u002FTwitter, Reddit, Google Trends, Hacker News, Zhihu, Bilibili, Weibo). Trigger: when user says 'trend', 'trends', 'trend tap', '热点', '热搜', or asks what is trending\u002Fhot. Do NOT use web_search — this skill already fetches live data from all platforms with zero API keys.","https:\u002F\u002Fgithub.com\u002Fsapsapshen\u002Fai-memory-router\u002Fblob\u002Fmain\u002Fskills-store\u002Fopenclaw\u002Ftrend-tap\u002FSKILL.md",[],[101,102,103,104,105,27,85,106,107,108,109,110,111,112,113,114,115,116,117,118],"trend","trend tap","tap","api keys","Bash","trending","hacker","aggregator","do","when","brew","live","data","google","install","read","across","api",{"skillNo":120,"name":121,"nameZh":6,"description":122,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":123,"categories":124,"keywords":125,"publishedAt":43},"01M71ATJ9AM5KM15AA8PS302EB","trend-radar","Real-time trending topics aggregator across 7 platforms (X\u002FTwitter, Reddit, Google Trends, Hacker News, Zhihu, Bilibili, Weibo). Trigger: when user says 'trend', 'trends', 'trend pulse', '热点', '热搜', or asks what is trending\u002Fhot. Do NOT use web_search — this skill already fetches live data from all platforms with zero API keys.","https:\u002F\u002Fgithub.com\u002Fsapsapshen\u002Fai-memory-router\u002Fblob\u002Fmain\u002Fskills-store\u002Fopenclaw\u002Ftrend-radar\u002FSKILL.md",[],[126,127,101,111,112,113,114,105,85,116,128,117,118,110,108,107,106,115,27,104],"trend radar","radar","pulse",{"skillNo":130,"name":131,"nameZh":6,"description":132,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":133,"categories":134,"keywords":135,"publishedAt":43},"01M71ATGF095N3EJM9G8SD0BB5","tech-news-daily","每日科技新闻汇——从全球多个 RSS 源抓取最新科技新闻，翻译（英→中）、去重、排版成每日简报。当用户询问科技新闻、科技新鲜事、每日科技、技术动态、Daily Tech News 或类似表达时触发此技能。","https:\u002F\u002Fgithub.com\u002Fsapsapshen\u002Fai-memory-router\u002Fblob\u002Fmain\u002Fskills-store\u002Fopenclaw\u002Ftech-news-daily\u002FSKILL.md",[],[136,137,138,139,140,141],"news","tech","tech news daily","daily","rss","翻译",{"skillNo":143,"name":144,"nameZh":6,"description":145,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":146,"categories":147,"keywords":151,"publishedAt":43},"01M71ATFAEAMQ17M70NQNY7SR8","summarize","Summarize URLs or files with the summarize CLI (web, PDFs, images, audio, YouTube).","https:\u002F\u002Fgithub.com\u002Fsapsapshen\u002Fai-memory-router\u002Fblob\u002Fmain\u002Fskills-store\u002Fopenclaw\u002Fsummarize\u002FSKILL.md",[148],{"categoryNo":149,"categoryName":150,"taxonomyType":21,"taxonomyTypeDesc":22},"01JBSEEDCAT000000000000021","资料检索与研究",[144,90,152,153,154,93,111,155,156,157,103,158,115],"Audio","cli","youtube","images","https","files","pdfs",{"skillNo":160,"name":161,"nameZh":6,"description":162,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":163,"categories":164,"keywords":165,"publishedAt":43},"01M71ATDNS3R0M4H936PACQAWZ","sonoscli","Control Sonos speakers (discover\u002Fstatus\u002Fplay\u002Fvolume\u002Fgroup).","https:\u002F\u002Fgithub.com\u002Fsapsapshen\u002Fai-memory-router\u002Fblob\u002Fmain\u002Fskills-store\u002Fopenclaw\u002Fsonoscli\u002FSKILL.md",[],[161,166,167,168,115,169,156,170,171,172,173,174],"status","play","go","latest","github","discover","control","cmd","volume"]