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)
1---2name: convex-doctor3description: Run convex-doctor static analysis, interpret findings, and fix issues across security, performance, correctness, schema, and architecture categories. Use when running convex-doctor, fixing convex-doctor warnings or errors, improving the convex-doctor score, or when asked about Convex code quality, static analysis, or linting Convex functions.4---56# Convex doctor workflow78This 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.910## What is convex-doctor1112[convex-doctor](https://github.com/nooesc/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.1314Run it with:1516```bash17npx convex-doctor@latest18```1920## Configuration2122This project has a `convex-doctor.toml` at the repo root with intentional suppressions. Always check it before working on findings.2324### Current suppressions and rationale2526| Rule | Level | Rationale |27|------|-------|-----------|28| `correctness/generated-code-modified` | off | Working tree is always dirty after codegen |29| `schema/optional-field-no-default-handling` | off | 94 optional fields by design for markdown frontmatter |30| `correctness/missing-unique` | off | Remaining `.first()` calls are intentional ordered picks |31| `schema/deep-nesting` | off | 4-level validators needed for chat attachments |32| `schema/array-relationships` | off | Flagged on function args, not table columns |33| `perf/missing-index-on-foreign-key` | off | Remaining FK is inside nested array (not indexable) |34| `arch/duplicated-auth` | off | Auth awareness is intentional per public handler |35| `arch/monolithic-file` | off | Files organized by domain |36| `arch/large-handler` | off | Email templates, sync, and search are inherently multi-step |3738### Ignored files3940- `convex/_generated/**` (generated code)41- `convex/authComponent.ts` (thin auth component forwarders)4243## Fix priority order4445When convex-doctor reports findings, fix them in this order:46471. **Security errors** (highest priority)48 - Add auth to HTTP actions and public endpoints49 - Convert `api.*` server-to-server calls to `internal.*`50 - Move public actions to mutation-scheduled internal actions51522. **Correctness errors**53 - Remove `Date.now()` from queries (breaks caching and reactivity)54 - Convert `.first()` to `.unique()` only where the index enforces uniqueness55 - Fix `collect then filter` patterns with indexed queries56573. **Performance warnings**58 - Replace unbounded `.collect()` with `.take(n)` or pagination59 - Batch sequential `ctx.run*` calls into single internal queries60 - Eliminate N+1 patterns in HTTP and RSS endpoints61624. **Schema warnings**63 - Add missing indexes for foreign keys where query patterns exist64 - Rename indexes to `by_field` snake_case convention65 - Remove redundant indexes (prefixes of compound indexes)66675. **Architecture warnings**68 - Extract helper functions from large handlers69 - Split provider modules from orchestration logic70 - Replace `throw new Error(...)` with `ConvexError` in user-facing handlers7172## Common fix patterns7374### Convert public action to queued job7576Instead of calling a public action from the browser, create a job table and mutation-scheduled internal action:77781. Add a job table to `convex/schema.ts` with status, result, and error fields792. Create a public mutation that inserts a pending job and schedules the internal action803. Create a public query that returns job status for the UI814. Convert the action to `internalAction` that updates the job record on completion or failure825. Update the frontend to call the mutation and poll the query8384This pattern was used for: AI image generation, AI chat responses, URL imports.8586### Convert api.* to internal.*8788When a Convex function calls another Convex function on the server side:89901. Create an `internal*` version if only a public version exists912. Replace `api.module.fn` with `internal.module.fn` in the caller923. If the function needs both public and internal access, keep both and have the public version call the internal one9394### Batch sequential ctx.run* calls9596When an action makes multiple `ctx.runQuery` calls for independent data:97981. Create a single internal query that returns all needed data in one object992. Replace the sequential calls with one `ctx.runQuery` to the batched query1003. This reduces transaction overhead and eliminates the `sequential-run-calls` warning101102### Remove Date.now() from queries103104Queries must be deterministic. Replace `Date.now()` with a timestamp argument:1051061. Add a `now: v.number()` argument to the query1072. Pass `Date.now()` from the frontend or from the action/mutation that calls the query1083. For reactive subscriptions, round the timestamp (e.g., 60-second intervals) to keep reactivity stable109110### Auth component helper conversion111112When `components.auth.public.*` triggers `direct-function-ref` warnings:1131141. Create helper functions in `convex/authComponent.ts` that call the component API1152. Import helpers directly instead of using `ctx.runQuery(internal.authComponent.*)`1163. Add `convex/authComponent.ts` to the `[ignore]` section of `convex-doctor.toml`117118## Verification checklist119120After every fix pass:121122- [ ] `npx convex codegen` passes123- [ ] `npx tsc --noEmit` passes (or `npx convex codegen` covers this)124- [ ] `npm run build` succeeds125- [ ] `npx convex-doctor@latest` shows improved score or fewer findings126- [ ] Existing functionality still works (AI chat, search, dashboard, RSS, stats)127128## Score history129130| Pass | Score | Errors | Warnings | Key changes |131|------|-------|--------|----------|-------------|132| Initial | 42/100 | 73 | 243 | Baseline |133| 1 (remediation) | ~55 | ~50 | ~221 | Security: auth on HTTP, api to internal |134| 2 | ~60 | ~40 | ~200 | AI action flow, HTTP hardening |135| 3 | 68/100 | - | - | collect-then-filter, auth signals |136| 10 | 80/100 | 1 | 68 | Import URL queued job, unique lookups |137| 15 | 91/100 | 0 | 43 | Newsletter batching, auth forwarders, toml config |138| 16 | 92/100 | 0 | 39 | Semantic search batching, auth helpers |139| 17 | 100/100 | 0 | 0 | Stats helpers, contact helpers, final toml tuning |140141## When to suppress vs fix142143**Fix it** when:144- The finding points to a real bug or security gap145- The fix is low risk and improves code quality146- The pattern can be changed without affecting product behavior147148**Suppress it** when:149- The finding is a tool false positive (e.g., component function refs)150- The pattern is intentional by design (e.g., per-handler auth checks)151- The fix would add more complexity than the warning is worth152- Generated code triggers the finding153154Always document suppressions with rationale in `convex-doctor.toml`.155156## Related PRDs157158All remediation PRDs are in `prds/convex-doctor/`:159160- `convex-doctor-remediation.md` (initial plan, 5 phases)161- `convex-doctor-second-pass.md` through `convex-doctor-seventeenth-pass.md`162163## Related files164165- `convex-doctor.toml` (suppression config)166- `convex/schema.ts` (indexes and table definitions)167- `convex/authComponent.ts` (auth component forwarders)168- `convex/importJobs.ts` (queued job pattern example)169- `convex/aiImageJobs.ts` (queued job pattern example)170- `convex/semanticSearchJobs.ts` (queued job pattern example)