convex-doctor
Convex doctor workflow
This skill codifies the full convex-doctor remediation workflow used in this codebase (score 42 to 100 across 17 passes). Follow it whenever running convex-doctor or fixing its findings.
What is convex-doctor
convex-doctor is a static analysis tool for Convex backends. It scores your codebase 0 to 100 across five categories: security, correctness, performance, schema, and architecture.
Run it with:
npx convex-doctor@latest
Configuration
This project has a convex-doctor.toml at the repo root with intentional suppressions. Always check it before working on findings.
Current suppressions and rationale
| Rule | Level | Rationale |
|---|---|---|
correctness/generated-code-modified |
off | Working tree is always dirty after codegen |
schema/optional-field-no-default-handling |
off | 94 optional fields by design for markdown frontmatter |
correctness/missing-unique |
off | Remaining .first() calls are intentional ordered picks |
schema/deep-nesting |
off | 4-level validators needed for chat attachments |
schema/array-relationships |
off | Flagged on function args, not table columns |
perf/missing-index-on-foreign-key |
off | Remaining FK is inside nested array (not indexable) |
arch/duplicated-auth |
off | Auth awareness is intentional per public handler |
arch/monolithic-file |
off | Files organized by domain |
arch/large-handler |
off | Email templates, sync, and search are inherently multi-step |
Ignored files
convex/_generated/**(generated code)convex/authComponent.ts(thin auth component forwarders)
Fix priority order
When convex-doctor reports findings, fix them in this order:
-
Security errors (highest priority)
- Add auth to HTTP actions and public endpoints
- Convert
api.*server-to-server calls tointernal.* - Move public actions to mutation-scheduled internal actions
-
Correctness errors
- Remove
Date.now()from queries (breaks caching and reactivity) - Convert
.first()to.unique()only where the index enforces uniqueness - Fix
collect then filterpatterns with indexed queries
- Remove
-
Performance warnings
- Replace unbounded
.collect()with.take(n)or pagination - Batch sequential
ctx.run*calls into single internal queries - Eliminate N+1 patterns in HTTP and RSS endpoints
- Replace unbounded
-
Schema warnings
- Add missing indexes for foreign keys where query patterns exist
- Rename indexes to
by_fieldsnake_case convention - Remove redundant indexes (prefixes of compound indexes)
-
Architecture warnings
- Extract helper functions from large handlers
- Split provider modules from orchestration logic
- Replace
throw new Error(...)withConvexErrorin user-facing handlers
Common fix patterns
Convert public action to queued job
Instead of calling a public action from the browser, create a job table and mutation-scheduled internal action:
- Add a job table to
convex/schema.tswith status, result, and error fields - Create a public mutation that inserts a pending job and schedules the internal action
- Create a public query that returns job status for the UI
- Convert the action to
internalActionthat updates the job record on completion or failure - Update the frontend to call the mutation and poll the query
This pattern was used for: AI image generation, AI chat responses, URL imports.
Convert api.* to internal.*
When a Convex function calls another Convex function on the server side:
- Create an
internal*version if only a public version exists - Replace
api.module.fnwithinternal.module.fnin the caller - If the function needs both public and internal access, keep both and have the public version call the internal one
Batch sequential ctx.run* calls
When an action makes multiple ctx.runQuery calls for independent data:
- Create a single internal query that returns all needed data in one object
- Replace the sequential calls with one
ctx.runQueryto the batched query - This reduces transaction overhead and eliminates the
sequential-run-callswarning
Remove Date.now() from queries
Queries must be deterministic. Replace Date.now() with a timestamp argument:
- Add a
now: v.number()argument to the query - Pass
Date.now()from the frontend or from the action/mutation that calls the query - For reactive subscriptions, round the timestamp (e.g., 60-second intervals) to keep reactivity stable
Auth component helper conversion
When components.auth.public.* triggers direct-function-ref warnings:
- Create helper functions in
convex/authComponent.tsthat call the component API - Import helpers directly instead of using
ctx.runQuery(internal.authComponent.*) - Add
convex/authComponent.tsto the[ignore]section ofconvex-doctor.toml
Verification checklist
After every fix pass:
-
npx convex codegenpasses -
npx tsc --noEmitpasses (ornpx convex codegencovers this) -
npm run buildsucceeds -
npx convex-doctor@latestshows improved score or fewer findings - Existing functionality still works (AI chat, search, dashboard, RSS, stats)
Score history
| Pass | Score | Errors | Warnings | Key changes |
|---|---|---|---|---|
| Initial | 42/100 | 73 | 243 | Baseline |
| 1 (remediation) | ~55 | ~50 | ~221 | Security: auth on HTTP, api to internal |
| 2 | ~60 | ~40 | ~200 | AI action flow, HTTP hardening |
| 3 | 68/100 | - | - | collect-then-filter, auth signals |
| 10 | 80/100 | 1 | 68 | Import URL queued job, unique lookups |
| 15 | 91/100 | 0 | 43 | Newsletter batching, auth forwarders, toml config |
| 16 | 92/100 | 0 | 39 | Semantic search batching, auth helpers |
| 17 | 100/100 | 0 | 0 | Stats helpers, contact helpers, final toml tuning |
When to suppress vs fix
Fix it when:
- The finding points to a real bug or security gap
- The fix is low risk and improves code quality
- The pattern can be changed without affecting product behavior
Suppress it when:
- The finding is a tool false positive (e.g., component function refs)
- The pattern is intentional by design (e.g., per-handler auth checks)
- The fix would add more complexity than the warning is worth
- Generated code triggers the finding
Always document suppressions with rationale in convex-doctor.toml.
Related PRDs
All remediation PRDs are in prds/convex-doctor/:
convex-doctor-remediation.md(initial plan, 5 phases)convex-doctor-second-pass.mdthroughconvex-doctor-seventeenth-pass.md
Related files
convex-doctor.toml(suppression config)convex/schema.ts(indexes and table definitions)convex/authComponent.ts(auth component forwarders)convex/importJobs.ts(queued job pattern example)convex/aiImageJobs.ts(queued job pattern example)convex/semanticSearchJobs.ts(queued job pattern example)