Troubleshooting FAQ
The documented gotchas, as questions. All of these are learned-the-hard-way rules from the agent playbook — none are hypothetical.
Why must I never delete .env.local or run convex dev --configure=new?
.env.local records which Convex cloud project the app belongs to. Deleting it, or running npx convex dev --configure=new on an app that already has one, mints a duplicate cloud project — you end up deploying to a fresh empty backend while your data lives in the old one. The deploy script guards against this; do not bypass the guard. To (re)initialize a deployment, use npx convex dev --once.
convex codegen fails or generates nothing — why?
convex codegen needs a configured deployment. Run npx convex dev --once first (it configures the project and writes .env.local), then codegen works. The standard loop after any backend change is: npx convex codegen && pnpm exec tsc --noEmit, fixed to zero errors.
TypeScript rejects my optional Convex field with undefined — what gives?
The apps compile with exactOptionalPropertyTypes, so a v.optional(...) field must be omitted, never passed as undefined. Build argument objects with conditional spreads:
await ctx.db.insert("records", {
userId,
title,
...(dueDate !== undefined ? { dueDate } : {}),
});
Why can't I use forEach(async ...) in a mutation?
forEach does not await its callback — the mutation returns before the writes finish, and Convex may commit or reject a half-applied batch. Always use for...of with await for batch db operations inside mutations.
What are the index naming rules?
Per-user tables always get index("by_user", ["userId"]) so queries scope to the authenticated user. Never name an index by_id — that name is reserved by Convex. Identity is always server-derived: call requireUserId(ctx) first in every handler.
How are dev ports assigned?
Each scaffolded app gets the next free port in the 52xx range (the scaffolder scans existing apps' dev scripts for --port <n> and picks the next). Override with --port <n> at scaffold time. The port is stamped into the dev script, so two apps never collide.
pnpm smoke fails immediately — what are the prerequisites?
Smoke boots the real app and drives a real browser, so it needs: pnpm install completed and pnpm exec playwright install chromium. Convex webapps also need a configured deployment (npx convex dev --once) to exercise the authed path. Supabase apps only smoke the marketing page + auth panel unless real project keys are configured. Screenshots land in /tmp/geenius-mvp-smoke.
Where does deploy state live?
Two places: apps/<slug>/.deploy.json remembers the chosen provider (and recorded project), so a bare pnpm deploy <slug> reuses it — the Convex backend deploys first, reusing the recorded project (never duplicating, preserving BETTER_AUTH_SECRET). The repo-root tmp-deploy-summary.txt holds the last deploy's summary output. Deploy errors are printed inline with the failing provider step.
My deployed page returns a 308 redirect when I curl it — is that broken?
No. Cloudflare Pages (the default provider) serves clean URLs: requests to /page.html get a 308 permanent redirect to /page. Follow redirects (curl -L) when checking deployed pages, and link to the clean, extensionless paths in your own content.
Why does my Supabase query builder not need await... but TypeScript complains when I wrap it?
Supabase query builders are thenables, not real Promises. useSupaQuery therefore types its fetcher as fn: () => PromiseLike<{ data: T | null; error: { message: string } | null }> — pass the builder directly, or an async function that awaits it. Remember the idiom is request/response, not realtime: call the returned refetch after your own mutations.