2 unstable releases
| 0.2.0 | Feb 24, 2026 |
|---|---|
| 0.1.0 | Feb 24, 2026 |
#7 in #convex
310KB
4.5K
SLoC
convex-doctor

Diagnose your Convex backend for anti-patterns, security issues, and performance problems.
convex-doctor is a static analysis CLI for Convex projects. It parses your convex/ directory, runs 65 rules across 7 categories, and produces a weighted 0-100 health score. Think of it as ESLint, but purpose-built for Convex backends.
Benchmarks
Tested against 18 open-source Convex applications
| Project | Stars | Score | Errors | Warnings | Info | Total |
|---|---|---|---|---|---|---|
| google-docs | 73 | 94/100 | 0 | 15 | 2 | 17 |
| Ai-Website-Builder | 122 | 92/100 | 0 | 15 | 1 | 16 |
| miro-clone | 82 | 89/100 | 3 | 18 | 13 | 34 |
| replicate | 197 | 88/100 | 3 | 15 | 0 | 18 |
| livecanvas | 192 | 84/100 | 3 | 31 | 0 | 34 |
| quizzex | 1 | 83/100 | 3 | 65 | 4 | 72 |
| podcastr | 782 | 77/100 | 12 | 63 | 6 | 81 |
| convex-saas | 338 | 76/100 | 5 | 51 | 26 | 82 |
| convjobs | 2 | 76/100 | 6 | 58 | 4 | 68 |
| BloxAI | 91 | 76/100 | 9 | 81 | 0 | 90 |
| notion-clone | 141 | 76/100 | 10 | 38 | 28 | 76 |
| react-starter-kit | 615 | 69/100 | 12 | 39 | 4 | 55 |
| ticket-marketplace | 210 | 64/100 | 22 | 131 | 20 | 173 |
| OpenChat | 138 | 56/100 | 49 | 188 | 15 | 252 |
| travel-planner-ai | 241 | 55/100 | 28 | 173 | 9 | 210 |
| youpac-ai | 315 | 55/100 | 50 | 192 | 120 | 362 |
| opensync | 301 | 48/100 | 70 | 277 | 58 | 405 |
| markdown-site | 547 | 36/100 | 77 | 316 | 48 | 441 |
Benchmarks run on Feb 24, 2026 against each project's main branch. Sorted by score.
Installation
Download prebuilt binary (recommended)
Grab the latest release for your platform from GitHub Releases:
# macOS (Apple Silicon)
curl -L https://github.com/nooesc/convex-doctor/releases/latest/download/convex-doctor-aarch64-darwin -o convex-doctor
chmod +x convex-doctor && sudo mv convex-doctor /usr/local/bin/
# macOS (Intel)
curl -L https://github.com/nooesc/convex-doctor/releases/latest/download/convex-doctor-x86_64-darwin -o convex-doctor
chmod +x convex-doctor && sudo mv convex-doctor /usr/local/bin/
# Linux (x86_64)
curl -L https://github.com/nooesc/convex-doctor/releases/latest/download/convex-doctor-x86_64-linux -o convex-doctor
chmod +x convex-doctor && sudo mv convex-doctor /usr/local/bin/
# Linux (aarch64)
curl -L https://github.com/nooesc/convex-doctor/releases/latest/download/convex-doctor-aarch64-linux -o convex-doctor
chmod +x convex-doctor && sudo mv convex-doctor /usr/local/bin/
Install from crates.io
cargo install convex-doctor
Build from source
git clone https://github.com/nooesc/convex-doctor.git
cd convex-doctor
cargo build --release
# Binary is at ./target/release/convex-doctor
Usage
Run from your project root (the directory containing convex/):
# Basic scan
convex-doctor
# Verbose output with file paths and line numbers
convex-doctor -v
# JSON output (for CI or tooling)
convex-doctor --format json
# Score only (prints just the number, e.g. "87")
convex-doctor --score
# Diff mode: only analyze files changed vs a base branch
convex-doctor --diff main
# Note: diff mode only runs file-level rules on changed files.
# Scan a specific project path
convex-doctor /path/to/my-project
Rules
convex-doctor runs 65 rules organized into 7 categories. Each category carries a different weight in the final score.
| Category | Weight | Rules | Description |
|---|---|---|---|
| Security | 1.5x | 13 | Arg/return validators, auth checks, internal API misuse, secrets, CORS, access control, generic args |
| Performance | 1.2x | 12 | Unbounded collect, missing indexes, Date.now() in queries, loop mutations, N+1 patterns, pagination |
| Correctness | 1.5x | 15 | Unwaited promises, deprecated APIs, side effects in queries, scheduler issues, non-determinism |
| Schema | 1.0x | 8 | Missing schema, deep nesting, redundant indexes, search index filters, optional field handling |
| Architecture | 0.8x | 8 | Large handlers, monolithic files, function chains, mixed types, missing helpers |
| Configuration | 1.0x | 5 | Missing convex.json, auth config, generated code, tsconfig, node version |
| Client-Side | 1.0x | 4 | Mutation in render, unhandled loading states, missing ConvexProvider |
Rule reference
Security (13 rules)
| Rule ID | Severity | What it detects |
|---|---|---|
security/missing-arg-validators |
error | Public functions without args validators |
security/missing-return-validators |
warning | Functions without returns validators |
security/missing-auth-check |
warning | Public functions that never call ctx.auth.getUserIdentity() |
security/internal-api-misuse |
error | Server-to-server calls using api.* instead of internal.* |
security/hardcoded-secrets |
error | API keys, tokens, or secrets hardcoded in source |
security/env-not-gitignored |
error | .env.local exists but is not in .gitignore |
security/spoofable-access-control |
warning | Access control trusting spoofable client args (e.g. userId, role) |
security/missing-table-id |
warning | Using string IDs instead of v.id("table") for document references |
security/missing-http-auth |
error | HTTP action endpoints without authentication checks |
security/conditional-function-export |
warning | Convex functions conditionally exported based on environment |
security/generic-mutation-args |
warning | Public mutations using v.any() in argument validators |
security/overly-broad-patch |
warning | ctx.db.patch with spread args that bypass validation |
security/http-missing-cors |
warning | HTTP routes without CORS headers |
Performance (12 rules)
| Rule ID | Severity | What it detects |
|---|---|---|
perf/unbounded-collect |
error | .collect() without .take(n) limit |
perf/filter-without-index |
warning | .filter() calls that scan entire tables |
perf/date-now-in-query |
error | Date.now() in query functions (breaks caching) |
perf/loop-run-mutation |
error | ctx.runMutation/ctx.runQuery inside loops (N+1) |
perf/sequential-run-calls |
warning | Multiple sequential ctx.run* calls in an action |
perf/unnecessary-run-action |
warning | ctx.runAction called from within an action |
perf/helper-vs-run |
warning | ctx.runQuery/ctx.runMutation inside a query or mutation |
perf/missing-index-on-foreign-key |
warning | v.id("table") field in schema without a corresponding index |
perf/action-from-client |
warning | Client calling actions directly instead of mutations |
perf/collect-then-filter |
warning | .collect() followed by JS .filter() instead of using DB query filters |
perf/large-document-write |
warning | Inserting documents with 20+ fields in a single write |
perf/no-pagination-for-list |
warning | Public query with .collect() returning unbounded results to client |
Correctness (15 rules)
| Rule ID | Severity | What it detects |
|---|---|---|
correctness/unwaited-promise |
error | ctx.db.insert, ctx.runMutation, etc. without await |
correctness/old-function-syntax |
warning | Legacy function registration syntax |
correctness/db-in-action |
error | Direct ctx.db.* calls inside actions |
correctness/deprecated-api |
warning | Usage of deprecated Convex APIs (v.bigint(), v.bytes()) |
correctness/wrong-runtime-import |
warning | Incompatible runtime imports (Node in edge, browser in server) |
correctness/direct-function-ref |
warning | Direct function refs passed to ctx.run* instead of api.*/internal.* |
correctness/missing-unique |
warning | .first() on indexed query where .unique() may be appropriate |
correctness/query-side-effect |
error | Side effects (ctx.db.insert/patch/delete) inside query functions |
correctness/mutation-in-query |
error | ctx.runMutation called from within a query function |
correctness/cron-uses-public-api |
warning | Cron jobs referencing public api.* instead of internal.* |
correctness/node-query-mutation |
warning | Queries/mutations using "use node" runtime unnecessarily |
correctness/scheduler-return-ignored |
info | ctx.scheduler.runAfter return value not captured |
correctness/non-deterministic-in-query |
warning | Math.random(), new Date(), crypto in query functions |
correctness/replace-vs-patch |
info | ctx.db.replace used where ctx.db.patch may be safer |
correctness/generated-code-modified |
warning | Generated files (_generated/) appear to be manually modified |
Schema (8 rules)
| Rule ID | Severity | What it detects |
|---|---|---|
schema/missing-schema |
warning | No schema.ts file found in convex/ directory |
schema/deep-nesting |
warning | Schema validators nested more than 3 levels deep |
schema/array-relationships |
warning | v.array(v.id(...)) patterns that may grow unbounded |
schema/redundant-index |
warning | Index that is a prefix of another index on the same table |
schema/too-many-indexes |
info | Table with 8+ indexes (limit is 32) |
schema/missing-search-index-filter |
info | Search index without filterFields |
schema/optional-field-no-default-handling |
warning | 5+ optional schema fields without undefined handling |
schema/missing-index-for-query |
warning | Query filters on a field with no matching index |
Architecture (8 rules)
| Rule ID | Severity | What it detects |
|---|---|---|
arch/large-handler |
warning | Handler functions exceeding 50 lines |
arch/monolithic-file |
warning | Files with more than 10 exported functions |
arch/duplicated-auth |
warning | 3+ functions with inline auth checks in the same file |
arch/action-without-scheduling |
info | Actions that could use ctx.scheduler instead of direct calls |
arch/no-convex-error |
info | throw new Error(...) instead of throw new ConvexError(...) |
arch/mixed-function-types |
info | File mixing public and internal function exports |
arch/no-helper-functions |
info | Multiple large handlers with no shared helper functions |
arch/deep-function-chain |
warning | Action with 5+ ctx.run* calls forming a deep chain |
Configuration (5 rules)
| Rule ID | Severity | What it detects |
|---|---|---|
config/missing-convex-json |
warning | No convex.json found in project root |
config/missing-auth-config |
error | Functions use ctx.auth but no auth.config.ts exists |
config/missing-generated-code |
warning | No _generated/ directory found |
config/outdated-node-version |
warning | Node version in config is outdated |
config/missing-tsconfig |
info | No tsconfig.json found in convex directory |
Client-Side (4 rules)
| Rule ID | Severity | What it detects |
|---|---|---|
client/mutation-in-render |
error | useMutation called in React render body (infinite loop) |
client/unhandled-loading-state |
warning | useQuery result used without checking for undefined loading state |
client/action-instead-of-mutation |
info | useAction used where useMutation may suffice |
client/missing-convex-provider |
info | Convex hooks used without ConvexProvider in component tree |
Scoring
The health score ranges from 0 to 100. Each finding deducts points based on its severity and category weight, with per-rule caps to prevent a single noisy rule from dominating the score.
| Score | Label | Meaning |
|---|---|---|
| 85 - 100 | Healthy | Few or no issues detected |
| 70 - 84 | Needs attention | Some issues worth addressing |
| 50 - 69 | Unhealthy | Significant problems found |
| 0 - 49 | Critical | Serious issues requiring immediate attention |
Configuration
Create a convex-doctor.toml in your project root to customize behavior:
# Disable specific rules
[rules]
"security/missing-return-validators" = "off"
"arch/monolithic-file" = "off"
# Ignore files by glob pattern
[ignore]
files = [
"convex/_generated/**",
"convex/testHelpers.ts",
]
# CI: exit with code 1 if score is below threshold
[ci]
fail_below = 70
CI integration
GitHub Actions
Add convex-doctor to your CI pipeline using the prebuilt binary (runs in ~2 seconds, no Rust toolchain needed):
name: Convex Health Check
on: [pull_request]
jobs:
convex-doctor:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install convex-doctor
run: |
curl -L https://github.com/nooesc/convex-doctor/releases/latest/download/convex-doctor-x86_64-linux -o convex-doctor
chmod +x convex-doctor
- name: Run convex-doctor
run: ./convex-doctor
By default, convex-doctor exits with code 0. To fail the build when the score drops below a threshold, add a convex-doctor.toml to your project root:
[ci]
fail_below = 70
convex-doctor will exit with code 1 when the score is below fail_below.
Diff mode (PR-only scanning)
Only lint files changed in the pull request — useful for large codebases:
- name: Run convex-doctor (changed files only)
run: ./convex-doctor --diff origin/main
Alternative: install via cargo
If you prefer using the Rust toolchain:
- uses: dtolnay/rust-toolchain@stable
- run: cargo install convex-doctor
- run: convex-doctor
Note: cargo install takes ~30s to compile. The prebuilt binary approach is faster.
License
MIT
Dependencies
~17MB
~303K SLoC