---
description: Convex backend conventions
globs: ["convex/**/*.ts","src/**/*.{ts,tsx}"]
alwaysApply: false
---

# Convex

- Schema in `convex/schema.ts` with `defineSchema`/`defineTable` and `v` validators. Add `.index("by_user", ["userId"])` for every field you filter on.
- Every function declares `args` (and ideally `returns`) validators: `export const list = query({ args: { channelId: v.id("channels") }, handler: async (ctx, args) => {...} })`.
- `query` for reads (reactive, deterministic), `mutation` for transactional writes, `action` for third-party APIs and side effects. Actions call DB via `ctx.runQuery`/`ctx.runMutation`, not `ctx.db`.
- Use `internalQuery`/`internalMutation`/`internalAction` for anything not called from the client; schedule follow-ups with `ctx.scheduler.runAfter`.
- Query with indexes: `ctx.db.query("messages").withIndex("by_channel", q => q.eq("channelId", id))`. Avoid `.filter()` on large tables, and use `.take(n)` or `.paginate()` instead of unbounded `.collect()`.
- Auth in every public function: `const identity = await ctx.auth.getUserIdentity(); if (!identity) throw new ConvexError("Unauthenticated");` then check ownership.
- Throw `ConvexError` with structured data for expected errors.
- Client: `useQuery(api.messages.list, { channelId })`, `useMutation(api.messages.send)`; pass `"skip"` to defer a query.
- Env vars set with `npx convex env set`, read via `process.env` in functions only.

## Done when
`npx convex dev --once` deploys to the dev deployment with no type errors, and `npx tsc --noEmit` passes.
