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 to internal.*
- 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 filter patterns with indexed queries
-
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
-
Schema warnings
- Add missing indexes for foreign keys where query patterns exist
- Rename indexes to
by_field snake_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(...) with ConvexError in 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.ts with 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
internalAction that 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.fn with internal.module.fn in 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.runQuery to the batched query
- This reduces transaction overhead and eliminates the
sequential-run-calls warning
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.ts that call the component API
- Import helpers directly instead of using
ctx.runQuery(internal.authComponent.*)
- Add
convex/authComponent.ts to the [ignore] section of convex-doctor.toml
Verification checklist
After every fix pass:
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.md through convex-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)