From ed36c74925691dda52a8b382566e04d366e590b4 Mon Sep 17 00:00:00 2001 From: Ben Allfree Date: Thu, 23 Apr 2026 14:26:54 -0700 Subject: [PATCH] feat: add Convex routing skill and enhance existing skills with formatting improvements --- .../skills/convex-create-component/SKILL.md | 22 ++++---- .../skills/convex-migration-helper/SKILL.md | 9 ++-- .../references/migration-patterns.md | 4 +- .../references/migrations-component.md | 3 +- .../skills/convex-performance-audit/SKILL.md | 18 +++---- .../references/function-budget.md | 20 ++++---- .../references/hot-path-rules.md | 50 +++++++++---------- .../references/occ-conflicts.md | 46 +++++++---------- .agents/skills/convex-quickstart/SKILL.md | 44 +++++++++------- .agents/skills/convex-setup-auth/SKILL.md | 2 +- .agents/skills/convex/SKILL.md | 47 +++++++++++++++++ 11 files changed, 155 insertions(+), 110 deletions(-) create mode 100644 .agents/skills/convex/SKILL.md diff --git a/.agents/skills/convex-create-component/SKILL.md b/.agents/skills/convex-create-component/SKILL.md index a79c18e..22af601 100644 --- a/.agents/skills/convex-create-component/SKILL.md +++ b/.agents/skills/convex-create-component/SKILL.md @@ -42,12 +42,12 @@ Create reusable Convex components with clear boundaries and a small app-facing A Ask the user, then pick one path: -| Goal | Shape | Reference | -|------|-------|-----------| -| Component for this app only | Local | `references/local-components.md` | -| Publish or share across apps | Packaged | `references/packaged-components.md` | -| User explicitly needs local + shared library code | Hybrid | `references/hybrid-components.md` | -| Not sure | Default to local | `references/local-components.md` | +| Goal | Shape | Reference | +| ------------------------------------------------- | ---------------- | ----------------------------------- | +| Component for this app only | Local | `references/local-components.md` | +| Publish or share across apps | Packaged | `references/packaged-components.md` | +| User explicitly needs local + shared library code | Hybrid | `references/hybrid-components.md` | +| Not sure | Default to local | `references/local-components.md` | Read exactly one reference file before proceeding. @@ -111,7 +111,7 @@ export const listUnread = query({ userId: v.string(), message: v.string(), read: v.boolean(), - }) + }), ), handler: async (ctx, args) => { return await ctx.db @@ -234,12 +234,16 @@ export const sendNotification = mutation({ ```ts // Bad: parent app table IDs are not valid component validators -args: { userId: v.id("users") } +args: { + userId: v.id("users"); +} ``` ```ts // Good: treat parent-owned IDs as strings at the boundary -args: { userId: v.string() } +args: { + userId: v.string(); +} ``` ### Advanced Patterns diff --git a/.agents/skills/convex-migration-helper/SKILL.md b/.agents/skills/convex-migration-helper/SKILL.md index 97f64c1..db36c62 100644 --- a/.agents/skills/convex-migration-helper/SKILL.md +++ b/.agents/skills/convex-migration-helper/SKILL.md @@ -55,13 +55,13 @@ Unless you are certain, prefer deprecating fields over deleting them. Mark the f // Before users: defineTable({ name: v.string(), -}) +}); // After - safe, new field is optional users: defineTable({ name: v.string(), bio: v.optional(v.string()), -}) +}); ``` ### Adding New Table @@ -70,7 +70,7 @@ users: defineTable({ posts: defineTable({ userId: v.id("users"), title: v.string(), -}).index("by_user", ["userId"]) +}).index("by_user", ["userId"]); ``` ### Adding Index @@ -79,8 +79,7 @@ posts: defineTable({ users: defineTable({ name: v.string(), email: v.string(), -}) - .index("by_email", ["email"]) +}).index("by_email", ["email"]); ``` ## Breaking Changes: The Deployment Workflow diff --git a/.agents/skills/convex-migration-helper/references/migration-patterns.md b/.agents/skills/convex-migration-helper/references/migration-patterns.md index 219583e..53b4946 100644 --- a/.agents/skills/convex-migration-helper/references/migration-patterns.md +++ b/.agents/skills/convex-migration-helper/references/migration-patterns.md @@ -9,7 +9,7 @@ Common migration patterns, zero-downtime strategies, and verification techniques users: defineTable({ name: v.string(), role: v.optional(v.union(v.literal("user"), v.literal("admin"))), -}) +}); // Migration: backfill the field export const addDefaultRole = migrations.define({ @@ -25,7 +25,7 @@ export const addDefaultRole = migrations.define({ users: defineTable({ name: v.string(), role: v.union(v.literal("user"), v.literal("admin")), -}) +}); ``` ## Deleting a Field diff --git a/.agents/skills/convex-migration-helper/references/migrations-component.md b/.agents/skills/convex-migration-helper/references/migrations-component.md index c80522f..95ec292 100644 --- a/.agents/skills/convex-migration-helper/references/migrations-component.md +++ b/.agents/skills/convex-migration-helper/references/migrations-component.md @@ -151,8 +151,7 @@ Process only matching documents instead of the full table: ```typescript export const fixEmptyNames = migrations.define({ table: "users", - customRange: (query) => - query.withIndex("by_name", (q) => q.eq("name", "")), + customRange: (query) => query.withIndex("by_name", (q) => q.eq("name", "")), migrateOne: () => ({ name: "" }), }); ``` diff --git a/.agents/skills/convex-performance-audit/SKILL.md b/.agents/skills/convex-performance-audit/SKILL.md index 9d92b33..382951c 100644 --- a/.agents/skills/convex-performance-audit/SKILL.md +++ b/.agents/skills/convex-performance-audit/SKILL.md @@ -43,13 +43,13 @@ Start with the strongest signal available: After gathering signals, identify the problem class and read the matching reference file. -| Signal | Reference | -|---|---| -| High bytes or documents read, JS filtering, unnecessary joins | `references/hot-path-rules.md` | -| OCC conflict errors, write contention, mutation retries | `references/occ-conflicts.md` | -| High subscription count, slow UI updates, excessive re-renders | `references/subscription-cost.md` | -| Function timeouts, transaction size errors, large payloads | `references/function-budget.md` | -| General "it's slow" with no specific signal | Start with `references/hot-path-rules.md` | +| Signal | Reference | +| -------------------------------------------------------------- | ----------------------------------------- | +| High bytes or documents read, JS filtering, unnecessary joins | `references/hot-path-rules.md` | +| OCC conflict errors, write contention, mutation retries | `references/occ-conflicts.md` | +| High subscription count, slow UI updates, excessive re-renders | `references/subscription-cost.md` | +| Function timeouts, transaction size errors, large payloads | `references/function-budget.md` | +| General "it's slow" with no specific signal | Start with `references/hot-path-rules.md` | Multiple problem classes can overlap. Read the most relevant reference first, then check the others if symptoms remain. @@ -107,7 +107,7 @@ After finding one problem, inspect both sibling readers and sibling writers for Examples: - If one list query switches from full docs to a digest table, inspect the other list queries for that table -- If one mutation needs no-op write protection, inspect the other writers to the same table +- If one mutation isolates a frequently-updated field or splits a hot document, inspect the other writers to the same table - If one read path needs a migration-safe rollout for an unbackfilled field, inspect sibling reads for the same rollout risk Do not leave one path fixed and another path on the old pattern unless there is a clear product reason. @@ -119,7 +119,7 @@ Confirm all of these: 1. Results are the same as before, no dropped records 2. Eliminated reads or writes are no longer in the path where expected 3. Fallback behavior works when denormalized or indexed fields are missing -4. New writes avoid unnecessary invalidation when data is unchanged +4. Frequently-updated fields are isolated from widely-read documents where needed 5. Every relevant sibling reader and writer was inspected, not just the original function ## Reference Files diff --git a/.agents/skills/convex-performance-audit/references/function-budget.md b/.agents/skills/convex-performance-audit/references/function-budget.md index c71d14c..d4d4aa5 100644 --- a/.agents/skills/convex-performance-audit/references/function-budget.md +++ b/.agents/skills/convex-performance-audit/references/function-budget.md @@ -10,17 +10,17 @@ Convex functions run inside transactions with budgets for time, reads, and write These are the current values from the [Convex limits docs](https://docs.convex.dev/production/state/limits). Check that page for the latest numbers. -| Resource | Limit | -|---|---| -| Query/mutation execution time | 1 second (user code only, excludes DB operations) | -| Action execution time | 10 minutes | -| Data read per transaction | 16 MiB | -| Data written per transaction | 16 MiB | +| Resource | Limit | +| --------------------------------- | ----------------------------------------------------- | +| Query/mutation execution time | 1 second (user code only, excludes DB operations) | +| Action execution time | 10 minutes | +| Data read per transaction | 16 MiB | +| Data written per transaction | 16 MiB | | Documents scanned per transaction | 32,000 (includes documents filtered out by `.filter`) | -| Index ranges read per transaction | 4,096 (each `db.get` and `db.query` call) | -| Documents written per transaction | 16,000 | -| Individual document size | 1 MiB | -| Function return value size | 16 MiB | +| Index ranges read per transaction | 4,096 (each `db.get` and `db.query` call) | +| Documents written per transaction | 16,000 | +| Individual document size | 1 MiB | +| Function return value size | 16 MiB | ## Symptoms diff --git a/.agents/skills/convex-performance-audit/references/hot-path-rules.md b/.agents/skills/convex-performance-audit/references/hot-path-rules.md index e3e44b1..e003e05 100644 --- a/.agents/skills/convex-performance-audit/references/hot-path-rules.md +++ b/.agents/skills/convex-performance-audit/references/hot-path-rules.md @@ -121,13 +121,15 @@ Indexes like `by_foo` and `by_foo_and_bar` are usually redundant. You only need // Bad: two indexes where one would do defineTable({ team: v.id("teams"), user: v.id("users") }) .index("by_team", ["team"]) - .index("by_team_and_user", ["team", "user"]) + .index("by_team_and_user", ["team", "user"]); ``` ```ts // Good: single compound index serves both query patterns -defineTable({ team: v.id("teams"), user: v.id("users") }) - .index("by_team_and_user", ["team", "user"]) +defineTable({ team: v.id("teams"), user: v.id("users") }).index( + "by_team_and_user", + ["team", "user"], +); ``` Exception: `.index("by_foo", ["foo"])` is really an index on `foo` + `_creationTime`, while `.index("by_foo_and_bar", ["foo", "bar"])` is on `foo` + `bar` + `_creationTime`. If you need results sorted by `foo` then `_creationTime`, you need the single-field index because the compound one would sort by `bar` first. @@ -170,9 +172,7 @@ const ownerName = project.ownerName ?? "Unknown owner"; ```ts // Good: denormalized data is an optimization, not the only source of truth const ownerName = - project.ownerName ?? - (await ctx.db.get(project.ownerId))?.name ?? - null; + project.ownerName ?? (await ctx.db.get(project.ownerId))?.name ?? null; ``` Bad lookup map pattern: @@ -241,35 +241,33 @@ const projects = await ctx.db .take(20); ``` -## 4. Skip No-Op Writes +## 4. Isolate Frequently-Updated Fields -No-op writes still cost work in Convex: +Convex already no-ops unchanged writes. The invalidation problem here is real writes hitting documents that many queries subscribe to. -- invalidation -- replication -- trigger execution -- downstream sync +Move high-churn fields like `lastSeen`, counters, presence, or ephemeral status off widely-read documents when most readers do not need them. -Before `patch` or `replace`, compare against the existing document and skip the write if nothing changed. - -Apply this across sibling writers too. One careful writer does not help much if three other mutations still patch unconditionally. +Apply this across sibling writers too. Splitting one write path does not help much if three other mutations still update the same widely-read document. ```ts -// Bad: patching unchanged values still triggers invalidation and downstream work -await ctx.db.patch(settings._id, { - theme: args.theme, - locale: args.locale, +// Bad: every presence heartbeat invalidates subscribers to the whole profile +await ctx.db.patch(user._id, { + name: args.name, + avatarUrl: args.avatarUrl, + lastSeen: Date.now(), }); ``` ```ts -// Good: only write when something actually changed -if (settings.theme !== args.theme || settings.locale !== args.locale) { - await ctx.db.patch(settings._id, { - theme: args.theme, - locale: args.locale, - }); -} +// Good: keep profile reads stable, move heartbeat updates to a separate document +await ctx.db.patch(user._id, { + name: args.name, + avatarUrl: args.avatarUrl, +}); + +await ctx.db.patch(presence._id, { + lastSeen: Date.now(), +}); ``` ## 5. Match Consistency To Read Patterns diff --git a/.agents/skills/convex-performance-audit/references/occ-conflicts.md b/.agents/skills/convex-performance-audit/references/occ-conflicts.md index a96d046..1da4380 100644 --- a/.agents/skills/convex-performance-audit/references/occ-conflicts.md +++ b/.agents/skills/convex-performance-audit/references/occ-conflicts.md @@ -73,42 +73,30 @@ await ctx.db.patch(shardId, { count: shard!.count + 1 }); Aggregate the shards in a query or scheduled job when you need the total. -### 3. Skip no-op writes +### 3. Move non-critical work to scheduled functions -Writes that do not change data still participate in conflict detection and trigger invalidation. +If a mutation does primary work plus secondary bookkeeping (analytics, non-critical notifications, cache warming), the bookkeeping extends the transaction's lifetime and read/write set. ```ts -// Bad: patches even when nothing changed -await ctx.db.patch(doc._id, { status: args.status }); -``` - -```ts -// Good: only write when the value actually differs -if (doc.status !== args.status) { - await ctx.db.patch(doc._id, { status: args.status }); -} -``` - -### 4. Move non-critical work to scheduled functions - -If a mutation does primary work plus secondary bookkeeping (analytics, notifications, cache warming), the bookkeeping extends the transaction's lifetime and read/write set. - -```ts -// Bad: analytics update in the same transaction as the user action -await ctx.db.patch(userId, { lastActiveAt: Date.now() }); -await ctx.db.insert("analytics", { event: "action", userId, ts: Date.now() }); -``` - -```ts -// Good: schedule the bookkeeping so the primary transaction is smaller -await ctx.db.patch(userId, { lastActiveAt: Date.now() }); -await ctx.scheduler.runAfter(0, internal.analytics.recordEvent, { - event: "action", +// Bad: canonical write and derived work happen in the same transaction +await ctx.db.patch(userId, { name: args.name }); +await ctx.db.insert("userUpdateAnalytics", { userId, + kind: "name_changed", + name: args.name, }); ``` -### 5. Combine competing writes +```ts +// Good: keep the primary write small, defer the analytics work +await ctx.db.patch(userId, { name: args.name }); +await ctx.scheduler.runAfter(0, internal.users.recordNameChangeAnalytics, { + userId, + name: args.name, +}); +``` + +### 4. Combine competing writes If two mutations must update the same document atomically, consider whether they can be combined into a single mutation call from the client, reducing round trips and conflict windows. diff --git a/.agents/skills/convex-quickstart/SKILL.md b/.agents/skills/convex-quickstart/SKILL.md index 792bba3..5bff17b 100644 --- a/.agents/skills/convex-quickstart/SKILL.md +++ b/.agents/skills/convex-quickstart/SKILL.md @@ -32,15 +32,15 @@ Use the official scaffolding tool. It creates a complete project with the fronte ### Pick a template -| Template | Stack | -|----------|-------| -| `react-vite-shadcn` | React + Vite + Tailwind + shadcn/ui | -| `nextjs-shadcn` | Next.js App Router + Tailwind + shadcn/ui | -| `react-vite-clerk-shadcn` | React + Vite + Clerk auth + shadcn/ui | -| `nextjs-clerk` | Next.js + Clerk auth | -| `nextjs-convexauth-shadcn` | Next.js + Convex Auth + shadcn/ui | -| `nextjs-lucia-shadcn` | Next.js + Lucia auth + shadcn/ui | -| `bare` | Convex backend only, no frontend | +| Template | Stack | +| -------------------------- | ----------------------------------------- | +| `react-vite-shadcn` | React + Vite + Tailwind + shadcn/ui | +| `nextjs-shadcn` | Next.js App Router + Tailwind + shadcn/ui | +| `react-vite-clerk-shadcn` | React + Vite + Clerk auth + shadcn/ui | +| `nextjs-clerk` | Next.js + Clerk auth | +| `nextjs-convexauth-shadcn` | Next.js + Convex Auth + shadcn/ui | +| `nextjs-lucia-shadcn` | Next.js + Lucia auth + shadcn/ui | +| `bare` | Convex backend only, no frontend | If the user has not specified a preference, default to `react-vite-shadcn` for simple apps or `nextjs-shadcn` for apps that need SSR or API routes. @@ -77,6 +77,7 @@ npm install **Ask the user to run this themselves:** Tell the user to run `npx convex dev` in their terminal. On first run it will prompt them to log in or develop anonymously. Once running, it will: + - Create a Convex project and dev deployment - Write the deployment URL to `.env.local` - Create the `convex/` directory with generated types @@ -111,6 +112,7 @@ my-app/ ``` The template already has: + - `ConvexProvider` wired into the app root - Correct env var names for the framework - Tailwind and shadcn/ui ready (for shadcn templates) @@ -141,7 +143,9 @@ Create the `ConvexReactClient` at module scope, not inside a component: ```tsx // Bad: re-creates the client on every render function App() { - const convex = new ConvexReactClient(import.meta.env.VITE_CONVEX_URL as string); + const convex = new ConvexReactClient( + import.meta.env.VITE_CONVEX_URL as string, + ); return ...; } @@ -192,7 +196,11 @@ export function ConvexClientProvider({ children }: { children: ReactNode }) { // app/layout.tsx import { ConvexClientProvider } from "./ConvexClientProvider"; -export default function RootLayout({ children }: { children: React.ReactNode }) { +export default function RootLayout({ + children, +}: { + children: React.ReactNode; +}) { return ( @@ -218,11 +226,11 @@ For Vue, Svelte, React Native, TanStack Start, Remix, and others, follow the mat The env var name depends on the framework: -| Framework | Variable | -|-----------|----------| -| Vite | `VITE_CONVEX_URL` | -| Next.js | `NEXT_PUBLIC_CONVEX_URL` | -| Remix | `CONVEX_URL` | +| Framework | Variable | +| ------------ | ------------------------ | +| Vite | `VITE_CONVEX_URL` | +| Next.js | `NEXT_PUBLIC_CONVEX_URL` | +| Remix | `CONVEX_URL` | | React Native | `EXPO_PUBLIC_CONVEX_URL` | `npx convex dev` writes the correct variable to `.env.local` automatically. @@ -299,7 +307,9 @@ function Tasks() { return (
- {tasks?.map((t) =>
{t.text}
)} + {tasks?.map((t) => ( +
{t.text}
+ ))}
); } diff --git a/.agents/skills/convex-setup-auth/SKILL.md b/.agents/skills/convex-setup-auth/SKILL.md index 0fa00e2..0d1d9dd 100644 --- a/.agents/skills/convex-setup-auth/SKILL.md +++ b/.agents/skills/convex-setup-auth/SKILL.md @@ -102,7 +102,7 @@ export const getMyProfile = query({ return await ctx.db .query("users") .withIndex("by_tokenIdentifier", (q) => - q.eq("tokenIdentifier", identity.tokenIdentifier) + q.eq("tokenIdentifier", identity.tokenIdentifier), ) .unique(); }, diff --git a/.agents/skills/convex/SKILL.md b/.agents/skills/convex/SKILL.md new file mode 100644 index 0000000..d467827 --- /dev/null +++ b/.agents/skills/convex/SKILL.md @@ -0,0 +1,47 @@ +--- +name: convex +description: Routing skill for Convex work in this repo. Use when the user explicitly invokes the `convex` skill, asks which Convex workflow or skill to use, or says they are working on a Convex app without naming a specific task yet. Do not prefer this skill when the request is clearly about setting up Convex, authentication, components, migrations, or performance. +--- + +# Convex + +Use this as the routing skill for Convex work in this repo. + +If a more specific Convex skill clearly matches the request, use that instead. + +## Start Here + +If the project does not already have Convex AI guidance installed, or the existing guidance looks stale, strongly recommend installing it first. + +Preferred: + +```bash +npx convex ai-files install +``` + +This installs or refreshes the managed Convex AI files. It is the recommended starting point for getting the official Convex guidelines in place and following the current Convex AI setup described in the docs: + +- [Convex AI docs](https://docs.convex.dev/ai) + +Simple fallback: + +- [convex_rules.txt](https://convex.link/convex_rules.txt) + +Prefer `npx convex ai-files install` over copying rules by hand when possible. + +## Route to the Right Skill + +After that, use the most specific Convex skill for the task: + +- New project or adding Convex to an app: `convex-quickstart` +- Authentication setup: `convex-setup-auth` +- Building a reusable Convex component: `convex-create-component` +- Planning or running a migration: `convex-migration-helper` +- Investigating performance issues: `convex-performance-audit` + +If one of those clearly matches the user's goal, switch to it instead of staying in this skill. + +## When Not to Use + +- The user has already named a more specific Convex workflow +- Another Convex skill obviously fits the request better