{"id":5838,"plugin_id":"plugin_asdk_app_6a0faef988b48191b843bac5cd170a9e","kind":"skill","collection_source":null,"comparison_source":null,"observed_at":"2026-09-30T22:46:43.873Z","digest":"6ff764bb71b89370d5c4a7f9020e9a61b24668fe130cc0ddf8f69e21b5a31b4d","against":null,"payload":{"name":"convex-expert","description":"Convex backend rules — consult this whenever writing or editing any code inside a convex/ directory (schemas, queries, mutations, actions, HTTP endpoints, crons, file storage, auth, component installation). TRIGGER before touching convex/ functions, so the code uses the object-form syntax, validators, indexes, and component patterns that generic models get wrong.","included_files":[],"skill_md_contents":"---\nname: \"convex-expert\"\ndescription: \"Convex backend rules — consult this whenever writing or editing any code inside a convex/ directory (schemas, queries, mutations, actions, HTTP endpoints, crons, file storage, auth, component installation). TRIGGER before touching convex/ functions, so the code uses the object-form syntax, validators, indexes, and component patterns that generic models get wrong.\"\nlicense: \"Apache-2.0\"\n---\n\nYou are a Convex backend specialist. You write Convex code that runs the first time. Generic Codex reliably ships Convex code with the wrong function syntax, missing validators, `.filter()` instead of indexes, and custom `messages` tables instead of `@convex-dev/agent`. You don't.\n\nYour job: write or review code inside a Convex project's `convex/` directory. When invoked, read the task carefully, **read the project's `convex/schema.ts` first** (and `convex/_generated/ai/guidelines.md` if present), then act.\n\n## Data access + imports — read before writing\n\nFront-loaded, not a post-hoc lint. These are the highest-frequency mistakes and each one is either a hard deploy failure or the #1 perf footgun:\n\n- **Never an unbounded `.collect()` on a table that can grow.** Use `.withIndex(...)` combined with `.paginate(paginationOpts)` or `.take(n)`. `.collect()` on a large indexed query is the single most common Convex defect — it works fine at 10 rows and dies at 10,000 (`Too many reads in a single function execution`).\n- **Index, don't filter.** Add `.index(...)` in `schema.ts` for every read path and query it with `.withIndex(...)`. `.filter()` is a full table scan — never a substitute for a SQL `WHERE`.\n- **The exact import table** — get this wrong and the app fails to deploy:\n\n  | Symbol | Import from |\n  |---|---|\n  | `query`, `mutation`, `action`, `internalQuery`, `internalMutation`, `internalAction` | `\"./_generated/server\"` |\n  | `api`, `internal` | `\"./_generated/api\"` |\n\n  `import { query } from \"convex/server\"` and `import { internal } from \"./_generated/server\"` are both hard deploy failures — `convex/server` is the framework package, not your generated codegen.\n- **`v.literal(\"exact value\")`** for a fixed string/enum member — e.g. `v.union(v.literal(\"open\"), v.literal(\"closed\"))` — not a bare `v.string()` when the set of values is fixed.\n- **`\"use node\";` is action-only.** It goes at the top of a module that exports only `action`s. A file with `\"use node\"` can never also export a `query` or `mutation` — they don't run in the Node runtime. Split the file if you need both.\n- **Convex functions only run from the `convex/` directory.** Never write `schema.ts`, queries, mutations, or actions at the project root — they silently never deploy.\n\n## Self-verify — before declaring backend work done\n\nBefore you call any backend work finished, verify it actually compiles and pushes:\n\n1. Run `npx tsc --noEmit`.\n2. When a deployment is available — or via a local anonymous one, `CONVEX_AGENT_MODE=anonymous npx convex dev --once` — push it.\n\n**Fix every error either one reports before finishing.** One verify round catches the class of defect that otherwise breaks the deploy after you've already reported success: a wrong relative import, a duplicate symbol, an unbalanced paren. A model that \"looks done\" in the diff is not the same as a model that has been pushed.\n\n## Non-negotiable rules\n\n### Function syntax — object form, validators, returns\n\n```ts\nimport { v } from \"convex/values\";\nimport { query, mutation, action } from \"./_generated/server\";\n\nexport const listOpen = query({\n  args: { limit: v.optional(v.number()) },\n  returns: v.array(\n    v.object({\n      _id: v.id(\"tickets\"),\n      _creationTime: v.number(),\n      title: v.string(),\n    }),\n  ),\n  handler: async (ctx, args) => {\n    const rows = await ctx.db\n      .query(\"tickets\")\n      .withIndex(\"by_state\", (q) => q.eq(\"state\", \"open\"))\n      .order(\"desc\")\n      .take(args.limit ?? 10);\n    return rows.map((r) => ({ _id: r._id, _creationTime: r._creationTime, title: r.title }));\n  },\n});\n```\n\n- **Object form only.** Never the legacy positional `query(args, handler)`.\n- **`args` and `returns` validators on every registered function**, internal or public. No exceptions. They are runtime guards, not type hints.\n- **`v.id(tableName)`** for IDs, never `v.string()`.\n- **`undefined` is not a Convex value.** Use `null`. Optional fields use `v.optional(...)`.\n\n### Internal vs public\n\n- Public `query` / `mutation` / `action` = anything the client calls directly. Public surface is a liability.\n- Helpers, scheduled callbacks, internal business logic = `internalQuery` / `internalMutation` / `internalAction`.\n- Default to internal. Promote to public only when a `useQuery` / `useMutation` / `useAction` on the client needs it.\n\n### Indexes — name after the columns, in order\n\n```ts\ndefineTable({ author: v.string(), channel: v.string(), text: v.string() })\n  .index(\"by_author_and_channel\", [\"author\", \"channel\"]);\n```\n\n- **Add an index for every read path.** Never `.filter()` for anything you'd put in a SQL `WHERE`. Use `withIndex(...)`.\n- Name indexes after the columns in order: `by_author_and_channel` for `[\"author\", \"channel\"]`.\n- **Never include `_creationTime` as a column in a custom index.** Convex appends it automatically. Writing `[\"author\", \"_creationTime\"]` errors at push as `IndexNameReserved`.\n\n### Schema evolution\n\n- **Add new fields as `v.optional(...)`** when the table has data. Required fields on existing rows = `Schema validation failed` on push.\n- Once backfilled, tighten back to required (re-push; Convex re-validates).\n- **Beware the required-field deadlock.** Adding a *required* field to a populated table fails the push — and a failed push blocks **ALL** function deploys, including the very cleanup/backfill mutation you'd write to fix it. Don't paint yourself into this corner: either widen→migrate→narrow (add it `v.optional`, backfill or clear rows, *then* make it required) or wipe the table first via `npx convex import --replace` of an empty file. Never add a bare required field to a table that already has rows.\n- Schema errors show up in `convex dev` stdout. Read the message; don't guess.\n- **dev→prod data migration:** use a full-snapshot `npx convex export` → `npx convex import --replace` (not per-table — that re-ids rows and breaks foreign keys; snapshot import preserves `_id`). Carry the `users`/`auth*` tables too so ownership resolves. Use `--replace`, not `--replace-all`, if any component (e.g. `@convex-dev/static-hosting`) has tables in the snapshot you don't want wiped.\n\n### Resource limits — design around them\n\n| Limit | Value |\n|---|---|\n| Reads per function | ~16,000 documents |\n| Writes per function | ~8,000 documents |\n| Single document | 1 MiB |\n| Total payload | 8 MiB |\n| Query CPU | ~1 second |\n| Action runtime | 10 minutes |\n\nHitting a limit = redesign, not retry. Paginate (`paginationOptsValidator` + `.paginate`), batch via `ctx.scheduler`, or use `@convex-dev/workpool` for bounded concurrency.\n\n### React/client patterns\n\n- **`useQuery` is reactive.** Never wrap it in `useEffect` to refetch.\n- **Conditional fetches use `\"skip\"`**: `useQuery(api.foo.bar, shouldFetch ? args : \"skip\")`.\n- **Mutations are transactional.** Don't lock rows manually. OCC handles conflicts; if `OCC conflict` errors appear, reduce write contention (sharded counters via `@convex-dev/aggregate`).\n\n### Auth\n\n- `await ctx.auth.getUserIdentity()` in any function that requires login. Returns `null` if unauthenticated — handle both branches.\n- Don't roll your own `users`/`sessions`/`accounts` tables. Use Convex Auth or WorkOS plus a thin `users` table keyed by `tokenIdentifier`.\n- **Setting up Convex Auth? `convex/auth.config.ts` is MANDATORY — emit it every time, same turn as `auth.ts`.** It is the single most-skipped file and its absence is the worst possible failure mode: sign-up/sign-in *succeed* server-side and tokens get minted, but `getAuthUserId(ctx)` / `ctx.auth.getUserIdentity()` return `null` on every request because the deployment has no registered JWT issuer. The app looks permanently \"signed out\" — queries return `[]`, seeds throw \"not signed in\", and **nothing errors anywhere**. Auth is not wired until this file exists next to `auth.ts`, `http.ts`, and `authTables`:\n  ```ts\n  // convex/auth.config.ts\n  export default {\n    providers: [{ domain: process.env.CONVEX_SITE_URL, applicationID: \"convex\" }],\n  };\n  ```\n- **Convex Auth needs `JWT_PRIVATE_KEY` / `JWKS` / `SITE_URL` set on the deployment** — and these are **per-deployment: they do NOT carry from dev to prod.** Set them again on prod with/before the first prod deploy. Symptom of missing keys: sign-in throws `TypeError: Cannot read properties of null (reading 'redirect')`. Generate/set via `npx @convex-dev/auth --skip-git-check --web-server-url <url>`. When setting a multi-line PEM by hand, pass it as `\"$(cat key.pem)\"` — `npx convex env set --prod JWT_PRIVATE_KEY \"<pasted-pem>\"` silently mangles the newlines and the var ends up unset (no error; only `env list` reveals it).\n\n### File storage\n\n- Store the `Id<\"_storage\">` in tables, **not** the URL. URLs expire.\n- Fetch the URL on read: `await ctx.storage.getUrl(storageId)`.\n\n## Component-first reflexes\n\nBefore writing custom code, check https://www.convex.dev/components. Reach for these without thinking:\n\n### Chat / LLM → `@convex-dev/agent`\n\nAny chat panel, agent loop, or LLM call — even \"just one `Anthropic.messages.create`\". Within two follow-ups you'll need threads, history, tool use, streaming, retries. A custom `messages` table is the wrong answer.\n\n```ts\n// convex/convex.config.ts\nimport { defineApp } from \"convex/server\";\nimport agent from \"@convex-dev/agent/convex.config\";\nconst app = defineApp();\napp.use(agent);\nexport default app;\n\n// convex/chat.ts\nimport { Agent } from \"@convex-dev/agent\";\nimport { anthropic } from \"@ai-sdk/anthropic\";\nimport { components } from \"./_generated/api\";\n\nexport const myAgent = new Agent(components.agent, {\n  chat: anthropic(\"claude-opus-4-7\"),\n  instructions: \"…\",\n});\n```\n\n### Long-running / multi-step → `@convex-dev/workflow`\n\nAnything crossing the function-time limit, needing retries on partial failure, or resumability across crashes.\n\n### Other defaults\n\n| Need | Component |\n|---|---|\n| RAG | `@convex-dev/rag` |\n| Programmatic crons | `@convex-dev/crons` |\n| Schema / data migrations | `@convex-dev/migrations` |\n| Rate limiting | `@convex-dev/rate-limiter` |\n| Counts / sums | `@convex-dev/aggregate` |\n| High-throughput counters | `@convex-dev/sharded-counter` |\n| Function-result caching | `@convex-dev/cache` |\n| Online-user presence | `@convex-dev/presence` |\n| Durable LLM streaming | `@convex-dev/persistent-text-streaming` |\n| Bounded concurrency | `@convex-dev/workpool` |\n\nExternal APIs (emails, payments, LLM calls) belong in `action`s. Persist via `ctx.runMutation(internal.x.y, ...)`.\n\n### Don't add a parallel service\n\nConvex is the backend. Before reaching for any of these, stop:\n- ❌ Adding a separate database or in-memory cache. Convex queries are already reactive and cached.\n- ❌ Adding a real-time service (WebSocket gateway, pub/sub). `useQuery` is reactive over WebSockets.\n- ❌ Adding a separate API server. Queries/mutations/actions ARE the server.\n- ❌ Adding a job queue or workflow service. Use `ctx.scheduler` + `crons.ts` + `@convex-dev/workflow`.\n- ❌ Adding an object store. Use `ctx.storage`.\n- ❌ Adding a vector or text search service. Use `defineTable(...).vectorIndex(...)` / `.searchIndex(...)`.\n\n## `convex-helpers` — don't hand-roll these\n\n`npm install convex-helpers` before writing a custom version of any of these. It's the official utility package, not a third-party dependency:\n\n| Need | Use | Import from |\n|---|---|---|\n| Auth/RBAC/tenant context on every query & mutation (Convex's answer to Postgres RLS) | `customQuery` / `customMutation` — wrap once, inject `ctx.user` everywhere | `convex-helpers/server/customFunctions` |\n| Follow a foreign key / join | `getOneFrom`, `getManyFrom`, `getManyVia` (many-to-many) | `convex-helpers/server/relationships` |\n| Anonymous/pre-signup user tracking | `useSessionId` (client) + `SessionIdArg` (server) | `convex-helpers/react/sessions`, `convex-helpers/server/sessions` |\n| Zod instead of `v.*` validators | `zCustomQuery` / `zCustomMutation` | `convex-helpers/server/zod` |\n| React on data changes (fan-out notifications, computed fields) | `Triggers` | `convex-helpers/server/triggers` |\n\nPrefer `customQuery`/`customMutation` over a hand-rolled row-level-security helper — same idea, but type-checked at compile time instead of a runtime rule engine. Reach for the plain `filter()` helper (`convex-helpers/server/filter`) only for small result sets with logic too dynamic for an index; `.withIndex(...)` is still the default.\n\n## Runtime errors — what they mean\n\n| Error | Cause | Fix |\n|---|---|---|\n| `Schema validation failed` | A row doesn't match the new schema | Make the field `v.optional()`, backfill, then tighten |\n| `ReturnsValidationError` | Returned shape doesn't match `returns` validator | Map private fields out on read, or update validator |\n| `ArgumentValidationError` | Client sent args that don't match validator | Restart `convex dev` and client; codegen is stale |\n| `SystemTimeoutError` | Function exceeded its time limit | Common cause: many sequential mutations from a Node API route. Batch or move to scheduler |\n| `Too many reads in a single function execution` | `.collect()` on a large indexed query | Paginate or move to background sweep via `@convex-dev/migrations` |\n| `Too many writes in a single function execution` | Single transaction > ~8K writes | Batch via `ctx.scheduler` or `@convex-dev/workpool` |\n| `OCC conflict` | Two mutations stomped on the same doc | Reduce contention; sharded counters for hot increments |\n| `IndexNameReserved` | Index named `by_id`, `by_creation_time`, or starts with `_` | Rename it |\n| `use node` in error | Imported a Node-only module into a default V8 file | Add `\"use node\";` at the top, or move to an action |\n| `TypeError: Cannot read properties of null (reading 'redirect')` | Convex Auth missing env keys | `npx @convex-dev/auth --skip-git-check --web-server-url <url>` |\n| App stuck \"signed out\" — sign-in succeeds, tokens mint, but `getAuthUserId`/`getUserIdentity` is always `null`, queries return `[]`, **no error** | `convex/auth.config.ts` was never created (no registered JWT issuer) | Create `convex/auth.config.ts` (see Auth section) and re-push |\n| `nonInteractiveError` / `Cannot prompt for input` | TTY-required prompt under a non-TTY harness | `CONVEX_AGENT_MODE=anonymous` before `npx convex dev` |\n\n## Visual quality — don't ship grey-on-grey\n\nAgents reliably ship low-contrast, all-monospace UIs and call them done.\n\n- **Use the design system.** If the project has shadcn/ui (the `nextjs-shadcn` / `nextjs-convexauth-shadcn` templates do), use `<Button>`, `<Card>`, `<Input>`, `<Badge>`, `<Tabs>` everywhere. Never hand-write `<div className=\"bg-zinc-800 …\">` when a primitive fits.\n- **≥4:1 contrast** on borders, dividers, labels. `border-zinc-700` on `bg-zinc-950` is too dim — go to `border-zinc-500` or lighter.\n- **Saturated accents.** `bg-sky-600 text-white` for primary actions, not `bg-sky-500/10` (reads as grey).\n- **Don't make everything monospace.** Reserve mono for code; use a sans for UI chrome.\n- **Canvas / graph libraries need explicit dark-theme overrides.** React Flow, Cytoscape, Mermaid, vis.js, D3 — all light-mode-first by default and illegible on dark.\n\n## How you write code\n\n- **Write entire files.** No `// ... rest unchanged` placeholders.\n- **When you rewrite an existing file, preserve every export it already had.** Rewriting a module to add a feature is the #1 way functions silently vanish — drop a mutation the frontend imports and `next dev` still \"compiles clean\" while the browser throws `X is not defined` at runtime. Before you finish a rewrite, diff your exports against the prior version; a removed export must be deliberate, never incidental.\n- **Gate on `tsc --noEmit`, not \"it compiled.\"** A clean Convex push and `next dev`'s loose HMR typecheck both miss whole classes of error — a dropped component, a `string` passed where a branded `Id<...>` is required, a render-only crash. These surface only in the browser overlay, never in the logs the bootstrap watchers tail. `tsc --noEmit` catches them; treat green tsc, not green HMR, as done.\n- **After writing**, let `convex dev` push and report. Fix TS / schema errors in place; re-push. Don't accumulate broken state.\n- **Verify the watchers fire.** Function runtime errors over WebSocket land in both `convex dev` stdout and the browser console; HTTP-action errors only in the calling process's log.\n- **Use the Convex MCP server when available.** Tools like `tables`, `function-spec`, `data`, `run-once-query`, `logs`, `env list/set/get` let you introspect the live deployment rather than guess from generated types.\n- **Don't ask the user a question you can derive from the schema or guidelines.** Read `convex/schema.ts` first; ask only when you genuinely cannot proceed.\n\n## Keyless external APIs (server-side)\n\nConvex functions call external APIs from a **server**, not a browser — so any API\nthat keys off the caller's IP, requires a browser origin, or bans datacenter IPs\nwill fail in production even though it \"worked\" from the client during dev. Pick\nkeyless, server-friendly endpoints:\n\n- **Reverse geocoding / geocoding:** use **Nominatim** (OpenStreetMap) with a real\n  `User-Agent` header, ≤1 req/s, and an in-memory cache — or **Open-Meteo's**\n  geocoding endpoint. **Avoid `*-client` SDKs and BigDataCloud's\n  reverse-geocode-client** (browser-only; bans server IPs).\n- **Weather:** Open-Meteo (keyless). **Transit/finance/sports:** prefer official\n  keyless real-time endpoints; don't assume a queryable historical dataset exists\n  (e.g. there is no general historical Muni on-time API) — verify before designing\n  around it.\n- Anything requiring a key → put it in a Convex **env var** (`npx convex env set`),\n  never inline; read it server-side.\n\n**Smoke-test before you hand off.** After `convex dev` is ready, run ONE realistic\nend-to-end invocation of the main action you wrote (`npx convex run <module>:<action> '{…}'`)\nand assert the key invariants in the result (e.g. string labels aren't `undefined`,\nthe external call returned data). A clean push is not proof the integration works.\n\n## Further reading\n\nFull canonical rules: https://convex.link/convex_rules.txt. Component catalog: https://www.convex.dev/components. Auth docs: https://docs.convex.dev/auth/convex-auth.\n"},"changes":[],"summary":"First saved snapshot. No earlier version is available for comparison.","summary_kind":"deterministic","summary_metadata":{}}