.claude/skills/convex-doctor/SKILL.md
Static analysis checklist for Convex backends covering 72 rules across security, performance, correctness, schema, architecture, configuration, and client-side patterns. Use when writing, reviewing, or auditing Convex code. Trigger on mentions of "convex-doctor", "health score", "static analysis", "anti-patterns", "audit convex", or before shipping backend changes.
npx skillsauth add get-convex/components-submissions-directory convex-doctorInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Run through these checks when writing or reviewing Convex backend and client code. Based on the convex-doctor CLI which scores projects 0-100 across 72 rules in 7 categories.
convex-doctor installed, recommend running npx convex-doctor -v for a full report.| ID | Severity | Rule |
|---|---|---|
| missing-arg-validators | error | All query/mutation/action and internal variants must have args validators |
| missing-return-validators | warning | Public functions should have returns validators |
| missing-auth-check | warning | Public functions should call ctx.auth.getUserIdentity() |
| internal-api-misuse | error | Server-to-server calls must use internal.*, not api.* |
| hardcoded-secrets | error | No API keys, tokens, or secrets hardcoded in source |
| env-not-gitignored | error | .env.local must be in .gitignore |
| spoofable-access-control | warning | Do not trust client args like userId or role for access control |
| missing-table-id | warning | Use v.id("table") instead of v.string() for document references |
| missing-http-auth | error | HTTP action endpoints must include authentication |
| conditional-function-export | error | Do not conditionally export Convex functions based on environment |
| generic-mutation-args | warning | Do not use v.any() in public mutation args |
| overly-broad-patch | warning | Do not ctx.db.patch with spread args that bypass validation |
| http-missing-cors | warning | HTTP routes should include CORS headers |
| ID | Severity | Rule |
|---|---|---|
| unbounded-collect | error | .collect() without .take(n) limit |
| filter-without-index | warning | .filter() scanning entire tables instead of using .withIndex() |
| date-now-in-query | error | Date.now() in query functions breaks caching |
| loop-run-mutation | error | ctx.runMutation/ctx.runQuery inside loops (N+1) |
| sequential-run-calls | warning | Multiple sequential ctx.run* calls in an action |
| unnecessary-run-action | warning | ctx.runAction from within an action (same runtime) |
| helper-vs-run | warning | ctx.runQuery/ctx.runMutation inside a query or mutation |
| missing-index-on-foreign-key | warning | v.id("table") schema field without a corresponding index |
| action-from-client | warning | Client calling actions directly instead of mutations |
| collect-then-filter | warning | .collect() followed by JS .filter() instead of DB query filters |
| large-document-write | info | Inserting documents with 20+ fields |
| no-pagination-for-list | warning | Public query with .collect() returning unbounded results |
| missing-pagination-opts-validator | warning | .paginate(...) without paginationOptsValidator in args |
| ID | Severity | Rule |
|---|---|---|
| unwaited-promise | error | ctx.db.insert, ctx.runMutation, etc. without await |
| old-function-syntax | warning | Legacy function registration syntax |
| db-in-action | error | Direct ctx.db.* calls inside actions |
| deprecated-api | warning | Deprecated APIs like v.bigint() |
| wrong-runtime-import | warning | Incompatible runtime imports |
| direct-function-ref | warning | Direct function refs instead of api.*/internal.* |
| missing-unique | warning | .first() where .unique() is appropriate |
| query-side-effect | error | Side effects (ctx.db.insert/patch/delete) inside queries |
| mutation-in-query | error | ctx.runMutation from within a query |
| cron-uses-public-api | error | Cron jobs referencing api.* instead of internal.* |
| node-query-mutation | error | Queries/mutations in "use node" files |
| scheduler-return-ignored | info | ctx.scheduler.runAfter return value not captured |
| non-deterministic-in-query | warning | Math.random(), new Date(), crypto in queries |
| replace-vs-patch | info | ctx.db.replace semantics reminder |
| generated-code-modified | error | Manual edits to _generated/ files |
| unsupported-validator-type | error | Unsupported validators (v.map(), v.set()) |
| query-delete-unsupported | error | .delete() on query chains |
| cron-helper-method-usage | warning | Deprecated crons.hourly/daily/weekly |
| cron-direct-function-reference | error | Direct function identifiers in cron methods |
| storage-get-metadata-deprecated | warning | Deprecated ctx.storage.getMetadata |
| ID | Severity | Rule |
|---|---|---|
| missing-schema | warning | No schema.ts in convex/ |
| deep-nesting | warning | Validators nested more than 3 levels deep |
| array-relationships | warning | v.array(v.id(...)) that may grow unbounded |
| redundant-index | warning | Index that is a prefix of another on the same table |
| too-many-indexes | info | Table with 8+ indexes |
| missing-search-index-filter | info | Search index without filterFields |
| optional-field-no-default-handling | warning | 5+ optional fields without undefined handling |
| missing-index-for-query | warning | Query filters on a field with no matching index |
| index-name-includes-fields | warning | Index name does not include all indexed fields in order |
| ID | Severity | Rule |
|---|---|---|
| large-handler | warning | Handler exceeding 50 lines |
| monolithic-file | warning | File with 10+ exported functions |
| duplicated-auth | warning | 3+ inline auth checks in the same file |
| action-without-scheduling | info | Action that could use ctx.scheduler instead |
| no-convex-error | info | throw new Error(...) instead of throw new ConvexError(...) |
| mixed-function-types | info | File mixing public and internal exports |
| no-helper-functions | info | Multiple large handlers with no shared helpers |
| deep-function-chain | warning | Action with 5+ ctx.run* calls |
| ID | Severity | Rule |
|---|---|---|
| missing-convex-json | warning | No convex.json in project root |
| missing-auth-config | error | Functions use ctx.auth but no auth.config.ts exists |
| missing-generated-code | warning | No _generated/ directory |
| outdated-node-version | warning | Node version in config is outdated |
| missing-tsconfig | info | No tsconfig.json in convex directory |
| ID | Severity | Rule |
|---|---|---|
| mutation-in-render | error | Mutation invocation during render |
| unhandled-loading-state | warning | useQuery result used without checking for undefined |
| action-instead-of-mutation | info | useAction where useMutation may suffice |
| missing-convex-provider | info | Convex hooks without ConvexProvider in component tree |
Health score is 0-100. Each finding deducts points based on severity and category weight, with per-rule caps.
| Score | Label | Meaning | |---|---|---| | 85-100 | Healthy | Few or no issues | | 70-84 | Needs attention | Some issues worth addressing | | 50-69 | Unhealthy | Significant problems | | 0-49 | Critical | Serious issues requiring immediate attention |
.filter() with .withIndex()// Bad
const docs = await ctx.db.query("tasks").filter((q) => q.eq(q.field("userId"), userId)).collect();
// Good (add index "by_userId" on ["userId"] in schema)
const docs = await ctx.db.query("tasks").withIndex("by_userId", (q) => q.eq("userId", userId)).collect();
// Bad
for (const id of ids) {
await ctx.runMutation(internal.tasks.complete, { taskId: id });
}
// Good: single mutation that handles the batch
const updates = ids.map((id) => ctx.db.patch(id, { completed: true }));
await Promise.all(updates);
// Bad (in a query)
const cutoff = Date.now() - 86400000;
// Good: pass timestamp as an argument from the caller
args: { cutoff: v.number() },
// Bad
const all = await ctx.db.query("messages").collect();
// Good: paginate or limit
const recent = await ctx.db.query("messages").order("desc").take(50);
npx convex-doctor # basic scan
npx convex-doctor -v # verbose with file paths and line numbers
npx convex-doctor --format json # JSON output for CI
npx convex-doctor --score # score only (prints a number)
npx convex-doctor --diff main # only files changed vs base branch
Source: https://github.com/nooesc/convex-doctor
development
Debug and troubleshoot WorkOS AuthKit authentication issues with Convex. Use when authentication fails, JWT validation errors occur, user identity returns null, email claims are missing, admin access checks fail, or sign in button does not work. Supports Netlify deployment.
development
Set up and configure WorkOS AuthKit authentication with Convex backend. Use when integrating AuthKit, configuring JWT providers, setting up environment variables, or implementing sign in and sign out flows with React and Vite. Supports Netlify deployment.
documentation
# Update project docs Use this skill after completing any feature, fix, or migration to keep the three core project tracking files in sync. Activate with: `@update-project-docs` ## Step 1: Get real dates Run this first: ```bash git log --date=short -n 10 ``` Use actual commit dates. Never use placeholder dates or future months. ## Step 2: Update TASK.md Move completed items into `## Completed` with date and time: ```markdown - [x] Feature name (YYYY-MM-DD HH:mm UTC) - [x] Sub-task det
tools
# Create a PRD Use this skill before any multi-file feature, architectural decision, or complex bug fix. Activate with: `@create-prd` ## Location and naming - All PRDs live in `prds/` folder - File name: `prds/<feature-or-problem-slug>.md` - Extension is always `.md`, not `.prd` - Use kebab-case for the filename (e.g., `prds/adding-email-auth.md`) ## Template Copy and fill in this template: ```markdown # [Feature or problem name] Created: YYYY-MM-DD HH:mm UTC Last Updated: YYYY-MM-DD HH: