From 6494a61ed8801e7561d9e3fdfea116e3240fed9e Mon Sep 17 00:00:00 2001 From: -Puter <22245429+puterhimself@users.noreply.github.com> Date: Thu, 23 Jul 2026 00:19:53 +0530 Subject: [PATCH] remoge claude ew --- .claude/skills/flue | 1 - .../skills/convex-create-component/SKILL.md | 325 ------------- .../agents/openai.yaml | 14 - .../convex-create-component/assets/icon.svg | 3 - .../references/advanced-patterns.md | 140 ------ .../references/hybrid-components.md | 38 -- .../references/local-components.md | 39 -- .../references/packaged-components.md | 54 --- .../skills/convex-migration-helper/SKILL.md | 178 ------- .../agents/openai.yaml | 13 - .../convex-migration-helper/assets/icon.svg | 3 - .../references/migration-patterns.md | 243 ---------- .../references/migrations-component.md | 219 --------- .../skills/convex-performance-audit/SKILL.md | 185 ------- .../agents/openai.yaml | 14 - .../convex-performance-audit/assets/icon.svg | 3 - .../references/function-budget.md | 254 ---------- .../references/hot-path-rules.md | 411 ---------------- .../references/occ-conflicts.md | 137 ------ .../references/subscription-cost.md | 300 ------------ .../.claude/skills/convex-quickstart/SKILL.md | 451 ------------------ .../convex-quickstart/agents/openai.yaml | 14 - .../skills/convex-quickstart/assets/icon.svg | 4 - .../.claude/skills/convex-setup-auth/SKILL.md | 187 -------- .../convex-setup-auth/agents/openai.yaml | 14 - .../skills/convex-setup-auth/assets/icon.svg | 3 - .../convex-setup-auth/references/auth0.md | 156 ------ .../convex-setup-auth/references/clerk.md | 141 ------ .../references/convex-auth.md | 188 -------- .../references/workos-authkit.md | 147 ------ .../backend/.claude/skills/convex/SKILL.md | 53 -- 31 files changed, 3932 deletions(-) delete mode 120000 .claude/skills/flue delete mode 100644 packages/backend/.claude/skills/convex-create-component/SKILL.md delete mode 100644 packages/backend/.claude/skills/convex-create-component/agents/openai.yaml delete mode 100644 packages/backend/.claude/skills/convex-create-component/assets/icon.svg delete mode 100644 packages/backend/.claude/skills/convex-create-component/references/advanced-patterns.md delete mode 100644 packages/backend/.claude/skills/convex-create-component/references/hybrid-components.md delete mode 100644 packages/backend/.claude/skills/convex-create-component/references/local-components.md delete mode 100644 packages/backend/.claude/skills/convex-create-component/references/packaged-components.md delete mode 100644 packages/backend/.claude/skills/convex-migration-helper/SKILL.md delete mode 100644 packages/backend/.claude/skills/convex-migration-helper/agents/openai.yaml delete mode 100644 packages/backend/.claude/skills/convex-migration-helper/assets/icon.svg delete mode 100644 packages/backend/.claude/skills/convex-migration-helper/references/migration-patterns.md delete mode 100644 packages/backend/.claude/skills/convex-migration-helper/references/migrations-component.md delete mode 100644 packages/backend/.claude/skills/convex-performance-audit/SKILL.md delete mode 100644 packages/backend/.claude/skills/convex-performance-audit/agents/openai.yaml delete mode 100644 packages/backend/.claude/skills/convex-performance-audit/assets/icon.svg delete mode 100644 packages/backend/.claude/skills/convex-performance-audit/references/function-budget.md delete mode 100644 packages/backend/.claude/skills/convex-performance-audit/references/hot-path-rules.md delete mode 100644 packages/backend/.claude/skills/convex-performance-audit/references/occ-conflicts.md delete mode 100644 packages/backend/.claude/skills/convex-performance-audit/references/subscription-cost.md delete mode 100644 packages/backend/.claude/skills/convex-quickstart/SKILL.md delete mode 100644 packages/backend/.claude/skills/convex-quickstart/agents/openai.yaml delete mode 100644 packages/backend/.claude/skills/convex-quickstart/assets/icon.svg delete mode 100644 packages/backend/.claude/skills/convex-setup-auth/SKILL.md delete mode 100644 packages/backend/.claude/skills/convex-setup-auth/agents/openai.yaml delete mode 100644 packages/backend/.claude/skills/convex-setup-auth/assets/icon.svg delete mode 100644 packages/backend/.claude/skills/convex-setup-auth/references/auth0.md delete mode 100644 packages/backend/.claude/skills/convex-setup-auth/references/clerk.md delete mode 100644 packages/backend/.claude/skills/convex-setup-auth/references/convex-auth.md delete mode 100644 packages/backend/.claude/skills/convex-setup-auth/references/workos-authkit.md delete mode 100644 packages/backend/.claude/skills/convex/SKILL.md diff --git a/.claude/skills/flue b/.claude/skills/flue deleted file mode 120000 index 9065025..0000000 --- a/.claude/skills/flue +++ /dev/null @@ -1 +0,0 @@ -../../.agents/skills/flue \ No newline at end of file diff --git a/packages/backend/.claude/skills/convex-create-component/SKILL.md b/packages/backend/.claude/skills/convex-create-component/SKILL.md deleted file mode 100644 index 4e5785b..0000000 --- a/packages/backend/.claude/skills/convex-create-component/SKILL.md +++ /dev/null @@ -1,325 +0,0 @@ ---- -name: convex-create-component -description: - Builds reusable Convex components with isolated tables and app-facing APIs. - Use for new components, reusable backend modules, integrations, or component - boundary work. ---- - -# Convex Create Component - -Create reusable Convex components with clear boundaries and a small app-facing -API. - -## When to Use - -- Creating a new Convex component in an existing app -- Extracting reusable backend logic into a component -- Building a third-party integration that should own its own tables and - workflows -- Packaging Convex functionality for reuse across multiple apps - -## When Not to Use - -- One-off business logic that belongs in the main app -- Thin utilities that do not need Convex tables or functions -- App-level orchestration that should stay in `convex/` -- Cases where a normal TypeScript library is enough - -## Workflow - -1. Ask the user what they are building and what the end goal is. If the repo - already makes the answer obvious, say so and confirm before proceeding. -2. Choose the shape using the decision tree below and read the matching - reference file. -3. Decide whether a component is justified. Prefer normal app code or a regular - library if the feature does not need isolated tables, backend functions, or - reusable persistent state. -4. Make a short plan for: - - what tables the component owns - - what public functions it exposes - - what data must be passed in from the app (auth, env vars, parent IDs) - - what stays in the app as wrappers or HTTP mounts -5. Create the component structure with `convex.config.ts`, `schema.ts`, and - function files. -6. Implement functions using the component's own `./_generated/server` imports, - not the app's generated files. -7. Wire the component into the app with `app.use(...)`. If the app does not - already have `convex/convex.config.ts`, create it. -8. Call the component from the app through `components.` using - `ctx.runQuery`, `ctx.runMutation`, or `ctx.runAction`. -9. If React clients, HTTP callers, or public APIs need access, create wrapper - functions in the app instead of exposing component functions directly. -10. Run `npx convex dev` and fix codegen, type, or boundary issues before - finishing. - -## Choose the Shape - -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` | - -Read exactly one reference file before proceeding. - -## Default Approach - -Unless the user explicitly wants an npm package, default to a local component: - -- Put it under `convex/components//` -- Define it with `defineComponent(...)` in its own `convex.config.ts` -- Install it from the app's `convex/convex.config.ts` with `app.use(...)` -- Let `npx convex dev` generate the component's own `_generated/` files - -## Component Skeleton - -A minimal local component with a table and two functions, plus the app wiring. - -```ts -// convex/components/notifications/convex.config.ts -import { defineComponent } from "convex/server"; - -export default defineComponent("notifications"); -``` - -```ts -// convex/components/notifications/schema.ts -import { defineSchema, defineTable } from "convex/server"; -import { v } from "convex/values"; - -export default defineSchema({ - notifications: defineTable({ - userId: v.string(), - message: v.string(), - read: v.boolean(), - }).index("by_user_read", ["userId", "read"]), -}); -``` - -```ts -// convex/components/notifications/lib.ts -import { v } from "convex/values"; -import { mutation, query } from "./_generated/server.js"; - -export const send = mutation({ - args: { userId: v.string(), message: v.string() }, - returns: v.id("notifications"), - handler: async (ctx, args) => { - return await ctx.db.insert("notifications", { - userId: args.userId, - message: args.message, - read: false, - }); - }, -}); - -export const listUnread = query({ - args: { userId: v.string() }, - returns: v.array( - v.object({ - _id: v.id("notifications"), - _creationTime: v.number(), - userId: v.string(), - message: v.string(), - read: v.boolean(), - }), - ), - handler: async (ctx, args) => { - return await ctx.db - .query("notifications") - .withIndex("by_user_read", (q) => - q.eq("userId", args.userId).eq("read", false), - ) - .collect(); - }, -}); -``` - -```ts -// convex/convex.config.ts -import { defineApp } from "convex/server"; -import notifications from "./components/notifications/convex.config.js"; - -const app = defineApp(); -app.use(notifications); - -export default app; -``` - -```ts -// convex/notifications.ts (app-side wrapper) -import { v } from "convex/values"; -import { mutation, query } from "./_generated/server"; -import { components } from "./_generated/api"; -import { getAuthUserId } from "@convex-dev/auth/server"; - -export const sendNotification = mutation({ - args: { message: v.string() }, - returns: v.null(), - handler: async (ctx, args) => { - const userId = await getAuthUserId(ctx); - if (!userId) throw new Error("Not authenticated"); - - await ctx.runMutation(components.notifications.lib.send, { - userId, - message: args.message, - }); - return null; - }, -}); - -export const myUnread = query({ - args: {}, - handler: async (ctx) => { - const userId = await getAuthUserId(ctx); - if (!userId) throw new Error("Not authenticated"); - - return await ctx.runQuery(components.notifications.lib.listUnread, { - userId, - }); - }, -}); -``` - -Note the reference path shape: a function in -`convex/components/notifications/lib.ts` is called as -`components.notifications.lib.send` from the app. - -## Critical Rules - -- Keep authentication in the app, because `ctx.auth` is not available inside - components. -- Keep environment access in the app, because component functions cannot read - `process.env`. -- Pass parent app IDs across the boundary as strings, because `Id` types become - plain strings in the app-facing `ComponentApi`. -- Do not use `v.id("parentTable")` for app-owned tables inside component args or - schema, because the component has no access to the app's table namespace. -- Import `query`, `mutation`, and `action` from the component's own - `./_generated/server`, not the app's generated files. -- Do not expose component functions directly to clients. Create app wrappers - when client access is needed, because components are internal and need - auth/env wiring the app provides. -- If the component defines HTTP handlers, mount the routes in the app's - `convex/http.ts`, because components cannot register their own HTTP routes. -- If the component needs pagination, use `paginator` from `convex-helpers` - instead of built-in `.paginate()`, because `.paginate()` does not work across - the component boundary. -- Define indexes for queried fields instead of using Convex `.filter()` after a - database query. -- Add `args` and `returns` validators to all public component functions, because - the component boundary requires explicit type contracts. - -## Patterns - -### Authentication and environment access - -```ts -// Bad: component code cannot rely on app auth or env -const identity = await ctx.auth.getUserIdentity(); -const apiKey = process.env.OPENAI_API_KEY; -``` - -```ts -// Good: the app resolves auth and env, then passes explicit values -const userId = await getAuthUserId(ctx); -if (!userId) throw new Error("Not authenticated"); - -await ctx.runAction(components.translator.translate, { - userId, - apiKey: process.env.OPENAI_API_KEY, - text: args.text, -}); -``` - -### Client-facing API - -```ts -// Bad: assuming a component function is directly callable by clients -export const send = components.notifications.send; -``` - -```ts -// Good: re-export through an app mutation or query -export const sendNotification = mutation({ - args: { message: v.string() }, - returns: v.null(), - handler: async (ctx, args) => { - const userId = await getAuthUserId(ctx); - if (!userId) throw new Error("Not authenticated"); - - await ctx.runMutation(components.notifications.lib.send, { - userId, - message: args.message, - }); - return null; - }, -}); -``` - -### IDs across the boundary - -```ts -// Bad: parent app table IDs are not valid component validators -args: { - userId: v.id("users"), -} -``` - -```ts -// Good: treat parent-owned IDs as strings at the boundary -args: { - userId: v.string(), -} -``` - -### Advanced Patterns - -For additional patterns including function handles for callbacks, deriving -validators from schema, static configuration with a globals table, and -class-based client wrappers, see `references/advanced-patterns.md`. - -## Validation - -Try validation in this order: - -1. `npx convex codegen --component-dir convex/components/` -2. `npx convex codegen` -3. `npx convex dev` - -Important: - -- Fresh repos may fail these commands until `CONVEX_DEPLOYMENT` is configured. -- Until codegen runs, component-local `./_generated/*` imports and app-side - `components....` references will not typecheck. -- If validation blocks on Convex login or deployment setup, stop and ask the - user for that exact step instead of guessing. - -## Reference Files - -Read exactly one of these after the user confirms the goal: - -- `references/local-components.md` -- `references/packaged-components.md` -- `references/hybrid-components.md` - -Official docs: -[Authoring Components](https://docs.convex.dev/components/authoring) - -## Checklist - -- [ ] Asked the user what they want to build and confirmed the shape -- [ ] Read the matching reference file -- [ ] Confirmed a component is the right abstraction -- [ ] Planned tables, public API, boundaries, and app wrappers -- [ ] Component lives under `convex/components//` (or package layout if - publishing) -- [ ] Component imports from its own `./_generated/server` -- [ ] Auth, env access, and HTTP routes stay in the app -- [ ] Parent app IDs cross the boundary as `v.string()` -- [ ] Public functions have `args` and `returns` validators -- [ ] Ran `npx convex dev` and fixed codegen or type issues diff --git a/packages/backend/.claude/skills/convex-create-component/agents/openai.yaml b/packages/backend/.claude/skills/convex-create-component/agents/openai.yaml deleted file mode 100644 index bf58a4c..0000000 --- a/packages/backend/.claude/skills/convex-create-component/agents/openai.yaml +++ /dev/null @@ -1,14 +0,0 @@ -interface: - display_name: "Convex Create Component" - short_description: - "Design and build reusable Convex components with clear boundaries." - icon_small: "./assets/icon.svg" - icon_large: "./assets/icon.svg" - brand_color: "#14B8A6" - default_prompt: - "Help me create a Convex component for this feature. First check that a - component is actually justified, then design the tables, API surface, and - app-facing wrappers before implementing it." - -policy: - allow_implicit_invocation: true diff --git a/packages/backend/.claude/skills/convex-create-component/assets/icon.svg b/packages/backend/.claude/skills/convex-create-component/assets/icon.svg deleted file mode 100644 index 10f4c2c..0000000 --- a/packages/backend/.claude/skills/convex-create-component/assets/icon.svg +++ /dev/null @@ -1,3 +0,0 @@ - diff --git a/packages/backend/.claude/skills/convex-create-component/references/advanced-patterns.md b/packages/backend/.claude/skills/convex-create-component/references/advanced-patterns.md deleted file mode 100644 index 9e56080..0000000 --- a/packages/backend/.claude/skills/convex-create-component/references/advanced-patterns.md +++ /dev/null @@ -1,140 +0,0 @@ -# Advanced Component Patterns - -Additional patterns for Convex components that go beyond the basics covered in -the main skill file. - -## Function Handles for callbacks - -When the app needs to pass a callback function to the component, use function -handles. This is common for components that run app-defined logic on a schedule -or in a workflow. - -```ts -// App side: create a handle and pass it to the component -import { createFunctionHandle } from "convex/server"; - -export const startJob = mutation({ - handler: async (ctx) => { - const handle = await createFunctionHandle(internal.myModule.processItem); - await ctx.runMutation(components.workpool.enqueue, { - callback: handle, - }); - }, -}); -``` - -```ts -// Component side: accept and invoke the handle -import { v } from "convex/values"; -import type { FunctionHandle } from "convex/server"; -import { mutation } from "./_generated/server.js"; - -export const enqueue = mutation({ - args: { callback: v.string() }, - handler: async (ctx, args) => { - const handle = args.callback as FunctionHandle<"mutation">; - await ctx.scheduler.runAfter(0, handle, {}); - }, -}); -``` - -## Deriving validators from schema - -Instead of manually repeating field types in return validators, extend the -schema validator: - -```ts -import { v } from "convex/values"; -import schema from "./schema.js"; - -const notificationDoc = schema.tables.notifications.validator.extend({ - _id: v.id("notifications"), - _creationTime: v.number(), -}); - -export const getLatest = query({ - args: {}, - returns: v.nullable(notificationDoc), - handler: async (ctx) => { - return await ctx.db.query("notifications").order("desc").first(); - }, -}); -``` - -## Static configuration with a globals table - -A common pattern for component configuration is a single-document "globals" -table: - -```ts -// schema.ts -export default defineSchema({ - globals: defineTable({ - maxRetries: v.number(), - webhookUrl: v.optional(v.string()), - }), - // ... other tables -}); -``` - -```ts -// lib.ts -export const configure = mutation({ - args: { maxRetries: v.number(), webhookUrl: v.optional(v.string()) }, - returns: v.null(), - handler: async (ctx, args) => { - const existing = await ctx.db.query("globals").first(); - if (existing) { - await ctx.db.patch(existing._id, args); - } else { - await ctx.db.insert("globals", args); - } - return null; - }, -}); -``` - -## Class-based client wrappers - -For components with many functions or configuration options, a class-based -client provides a cleaner API. This pattern is common in published components. - -```ts -// src/client/index.ts -import type { GenericMutationCtx, GenericDataModel } from "convex/server"; -import type { ComponentApi } from "../component/_generated/component.js"; - -type MutationCtx = Pick, "runMutation">; - -export class Notifications { - constructor( - private component: ComponentApi, - private options?: { defaultChannel?: string }, - ) {} - - async send(ctx: MutationCtx, args: { userId: string; message: string }) { - return await ctx.runMutation(this.component.lib.send, { - ...args, - channel: this.options?.defaultChannel ?? "default", - }); - } -} -``` - -```ts -// App usage -import { Notifications } from "@convex-dev/notifications"; -import { components } from "./_generated/api"; - -const notifications = new Notifications(components.notifications, { - defaultChannel: "alerts", -}); - -export const send = mutation({ - args: { message: v.string() }, - handler: async (ctx, args) => { - const userId = await getAuthUserId(ctx); - await notifications.send(ctx, { userId, message: args.message }); - }, -}); -``` diff --git a/packages/backend/.claude/skills/convex-create-component/references/hybrid-components.md b/packages/backend/.claude/skills/convex-create-component/references/hybrid-components.md deleted file mode 100644 index 657a734..0000000 --- a/packages/backend/.claude/skills/convex-create-component/references/hybrid-components.md +++ /dev/null @@ -1,38 +0,0 @@ -# Hybrid Convex Components - -Read this file only when the user explicitly wants a hybrid setup. - -## What This Means - -A hybrid component combines a local Convex component with shared library code. - -This can help when: - -- the user wants a local install but also shared package logic -- the component needs extension points or override hooks -- some logic should live in normal TypeScript code outside the component - boundary - -## Default Advice - -Treat hybrid as an advanced option, not the default. - -Before choosing it, ask: - -- Why is a plain local component not enough? -- Why is a packaged component not enough? -- What exactly needs to stay overridable or shared? - -If the answer is vague, fall back to local or packaged. - -## Risks - -- More moving parts -- Harder upgrades and backwards compatibility -- Easier to blur the component boundary - -## Checklist - -- [ ] User explicitly needs hybrid behavior -- [ ] Local-only and packaged-only options were considered first -- [ ] The extension points are clearly defined before coding diff --git a/packages/backend/.claude/skills/convex-create-component/references/local-components.md b/packages/backend/.claude/skills/convex-create-component/references/local-components.md deleted file mode 100644 index 8d4af17..0000000 --- a/packages/backend/.claude/skills/convex-create-component/references/local-components.md +++ /dev/null @@ -1,39 +0,0 @@ -# Local Convex Components - -Read this file when the component should live inside the current app and does -not need to be published as an npm package. - -## When to Choose This - -- The user wants the simplest path -- The component only needs to work in this repo -- The goal is extracting app logic into a cleaner boundary - -## Default Layout - -Use this structure unless the repo already has a clear alternative pattern: - -```text -convex/ - convex.config.ts - components/ - / - convex.config.ts - schema.ts - .ts -``` - -## Workflow Notes - -- Define the component with `defineComponent("")` -- Install it from the app with `defineApp()` and `app.use(...)` -- Keep auth, env access, public API wrappers, and HTTP route mounting in the app -- Let the component own isolated tables and reusable backend workflows -- Add app wrappers if clients need to call into the component - -## Checklist - -- [ ] Component is inside `convex/components//` -- [ ] App installs it with `app.use(...)` -- [ ] Component owns only its own tables -- [ ] App wrappers handle client-facing calls when needed diff --git a/packages/backend/.claude/skills/convex-create-component/references/packaged-components.md b/packages/backend/.claude/skills/convex-create-component/references/packaged-components.md deleted file mode 100644 index 553cd3d..0000000 --- a/packages/backend/.claude/skills/convex-create-component/references/packaged-components.md +++ /dev/null @@ -1,54 +0,0 @@ -# Packaged Convex Components - -Read this file when the user wants a reusable npm package or a component shared -across multiple apps. - -## When to Choose This - -- The user wants to publish the component -- The user wants a stable reusable package boundary -- The component will be shared across multiple apps or teams - -## Default Approach - -- Prefer starting from `npx create-convex@latest --component` when possible -- Keep the official authoring docs as the source of truth for package layout and - exports -- Validate the bundled package through an example app, not just the source files - -## Build Flow - -When building a packaged component, make sure the bundled output exists before -the example app tries to consume it. - -Recommended order: - -1. `npx convex codegen --component-dir ./path/to/component` -2. Run the package build command -3. Run `npx convex dev --typecheck-components` in the example app - -Do not assume normal app codegen is enough for packaged component workflows. - -## Package Exports - -If publishing to npm, make sure the package exposes the entry points apps need: - -- package root for client helpers, types, or classes -- `./convex.config.js` for installing the component -- `./_generated/component.js` for the app-facing `ComponentApi` type -- `./test` for testing helpers when applicable - -## Testing - -- Use `convex-test` for component logic -- Register the component schema and modules with the test instance -- Test app-side wrapper code from an example app that installs the package -- Export a small helper from `./test` if consumers need easy test registration - -## Checklist - -- [ ] Packaging is actually required -- [ ] Build order avoids bundle and codegen races -- [ ] Package exports include install and typing entry points -- [ ] Example app exercises the packaged component -- [ ] Core behavior is covered by tests diff --git a/packages/backend/.claude/skills/convex-migration-helper/SKILL.md b/packages/backend/.claude/skills/convex-migration-helper/SKILL.md deleted file mode 100644 index 8ce9874..0000000 --- a/packages/backend/.claude/skills/convex-migration-helper/SKILL.md +++ /dev/null @@ -1,178 +0,0 @@ ---- -name: convex-migration-helper -description: - Plans Convex schema and data migrations with widen-migrate-narrow and - @convex-dev/migrations. Use for breaking schema changes, backfills, table - reshaping, or zero-downtime rollouts. ---- - -# Convex Migration Helper - -Safely migrate Convex schemas and data when making breaking changes. - -## When to Use - -- Adding new required fields to existing tables -- Changing field types or structure -- Splitting or merging tables -- Renaming or deleting fields -- Migrating from nested to relational data - -## When Not to Use - -- Greenfield schema with no existing data in production or dev -- Adding optional fields that do not need backfilling -- Adding new tables with no existing data to migrate -- Adding or removing indexes with no correctness concern -- Questions about Convex schema design without a migration need - -## Key Concepts - -### Schema Validation Drives the Workflow - -Convex will not let you deploy a schema that does not match the data at rest. -This is the fundamental constraint that shapes every migration: - -- You cannot add a required field if existing documents don't have it -- You cannot change a field's type if existing documents have the old type -- You cannot remove a field from the schema if existing documents still have it - -This means migrations follow a predictable pattern: **widen the schema, migrate -the data, narrow the schema**. - -### Online Migrations - -Convex migrations run online, meaning the app continues serving requests while -data is updated asynchronously in batches. During the migration window, your -code must handle both old and new data formats. - -### Prefer New Fields Over Changing Types - -When changing the shape of data, create a new field rather than modifying an -existing one. This makes the transition safer and easier to roll back. - -### Don't Delete Data - -Unless you are certain, prefer deprecating fields over deleting them. Mark the -field as `v.optional` and add a code comment explaining it is deprecated and why -it existed. - -## Safe Changes (No Migration Needed) - -### Adding Optional Field - -```typescript -// 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 - -```typescript -posts: defineTable({ - userId: v.id("users"), - title: v.string(), -}).index("by_user", ["userId"]); -``` - -### Adding Index - -```typescript -users: defineTable({ - name: v.string(), - email: v.string(), -}).index("by_email", ["email"]); -``` - -## Breaking Changes: The Deployment Workflow - -Every breaking migration follows the same multi-deploy pattern: - -**Deploy 1 - Widen the schema:** - -1. Update schema to allow both old and new formats (e.g., add optional new - field) -2. Update code to handle both formats when reading -3. Update code to write the new format for new documents -4. Deploy - -**Between deploys - Migrate data:** - -5. Run migration to backfill existing documents -6. Verify all documents are migrated - -**Deploy 2 - Narrow the schema:** - -7. Update schema to require the new format only -8. Remove code that handles the old format -9. Deploy - -## Using the Migrations Component - -For any non-trivial migration, use the -[`@convex-dev/migrations`](https://www.convex.dev/components/migrations) -component. It handles batching, cursor-based pagination, state tracking, resume -from failure, dry runs, and progress monitoring. - -See `references/migrations-component.md` for installation, setup, defining and -running migrations directly with `npx convex run migrations:myMigration`, dry -runs, status monitoring, and configuration options. - -## Common Migration Patterns - -See `references/migration-patterns.md` for complete patterns with code examples -covering: - -- Adding a required field -- Deleting a field -- Changing a field type -- Splitting nested data into a separate table -- Cleaning up orphaned documents -- Zero-downtime strategies (dual write, dual read) -- Small table shortcut (single internalMutation without the component) -- Verifying a migration is complete - -## Common Pitfalls - -1. **Making a field required before migrating data**: Convex rejects the deploy - because existing documents lack the field. Always widen the schema first. -2. **Using `.collect()` on large tables**: Hits transaction limits or causes - timeouts. Use the migrations component for proper batched pagination. - `.collect()` is only safe for tables you know are small. -3. **Not writing the new format before migrating**: Documents created during the - migration window will be missed, leaving unmigrated data after the migration - "completes." -4. **Skipping the dry run**: Use `dryRun: true` to validate migration logic - before committing changes to production data. Catches bugs before they touch - real documents. -5. **Deleting fields prematurely**: Prefer deprecating with `v.optional` and a - comment. Only delete after you are confident the data is no longer needed and - no code references it. -6. **Using crons for migration batches**: The migrations component handles - batching via recursive scheduling internally. Crons require manual cleanup - and an extra deploy to remove. - -## Migration Checklist - -- [ ] Identify the breaking change and plan the multi-deploy workflow -- [ ] Update schema to allow both old and new formats -- [ ] Update code to handle both formats when reading -- [ ] Update code to write the new format for new documents -- [ ] Deploy widened schema and updated code -- [ ] Define migration using the `@convex-dev/migrations` component -- [ ] Test with `npx convex run migrations:myMigration '{"dryRun": true}'` -- [ ] Run migration directly with `npx convex run migrations:myMigration` and - monitor status -- [ ] Verify all documents are migrated -- [ ] Update schema to require new format only -- [ ] Clean up code that handled old format -- [ ] Deploy final schema and code -- [ ] Remove migration code once confirmed stable diff --git a/packages/backend/.claude/skills/convex-migration-helper/agents/openai.yaml b/packages/backend/.claude/skills/convex-migration-helper/agents/openai.yaml deleted file mode 100644 index 5f027dd..0000000 --- a/packages/backend/.claude/skills/convex-migration-helper/agents/openai.yaml +++ /dev/null @@ -1,13 +0,0 @@ -interface: - display_name: "Convex Migration Helper" - short_description: "Plan and run safe Convex schema and data migrations." - icon_small: "./assets/icon.svg" - icon_large: "./assets/icon.svg" - brand_color: "#8B5CF6" - default_prompt: - "Help me plan and execute this Convex migration safely. Start by identifying - the schema change, the existing data shape, and the widen-migrate-narrow - path before making edits." - -policy: - allow_implicit_invocation: true diff --git a/packages/backend/.claude/skills/convex-migration-helper/assets/icon.svg b/packages/backend/.claude/skills/convex-migration-helper/assets/icon.svg deleted file mode 100644 index fba7241..0000000 --- a/packages/backend/.claude/skills/convex-migration-helper/assets/icon.svg +++ /dev/null @@ -1,3 +0,0 @@ - diff --git a/packages/backend/.claude/skills/convex-migration-helper/references/migration-patterns.md b/packages/backend/.claude/skills/convex-migration-helper/references/migration-patterns.md deleted file mode 100644 index 7201f7e..0000000 --- a/packages/backend/.claude/skills/convex-migration-helper/references/migration-patterns.md +++ /dev/null @@ -1,243 +0,0 @@ -# Migration Patterns Reference - -Common migration patterns, zero-downtime strategies, and verification techniques -for Convex schema and data migrations. - -## Adding a Required Field - -```typescript -// Deploy 1: Schema allows both states -users: defineTable({ - name: v.string(), - role: v.optional(v.union(v.literal("user"), v.literal("admin"))), -}).index("by_role", ["role"]); - -// Migration: backfill the field -export const addDefaultRole = migrations.define({ - table: "users", - migrateOne: async (ctx, user) => { - if (user.role === undefined) { - await ctx.db.patch(user._id, { role: "user" }); - } - }, -}); - -// Deploy 2: After migration completes, make it required -users: defineTable({ - name: v.string(), - role: v.union(v.literal("user"), v.literal("admin")), -}); -``` - -## Deleting a Field - -Mark the field optional first, migrate data to remove it, then remove from -schema: - -```typescript -// Deploy 1: Make optional -// isPro: v.boolean() --> isPro: v.optional(v.boolean()) - -// Migration -export const removeIsPro = migrations.define({ - table: "teams", - migrateOne: async (ctx, team) => { - if (team.isPro !== undefined) { - await ctx.db.patch(team._id, { isPro: undefined }); - } - }, -}); - -// Deploy 2: Remove isPro from schema entirely -``` - -## Changing a Field Type - -Prefer creating a new field. You can combine adding and deleting in one -migration: - -```typescript -// Deploy 1: Add new field, keep old field optional -// isPro: v.boolean() --> isPro: v.optional(v.boolean()), plan: v.optional(...) - -// Migration: convert old field to new field -export const convertToEnum = migrations.define({ - table: "teams", - migrateOne: async (ctx, team) => { - if (team.plan === undefined) { - await ctx.db.patch(team._id, { - plan: team.isPro ? "pro" : "basic", - isPro: undefined, - }); - } - }, -}); - -// Deploy 2: Remove isPro from schema, make plan required -``` - -## Splitting Nested Data Into a Separate Table - -```typescript -export const extractPreferences = migrations.define({ - table: "users", - migrateOne: async (ctx, user) => { - if (user.preferences === undefined) return; - - const existing = await ctx.db - .query("userPreferences") - .withIndex("by_user", (q) => q.eq("userId", user._id)) - .first(); - - if (!existing) { - await ctx.db.insert("userPreferences", { - userId: user._id, - ...user.preferences, - }); - } - - await ctx.db.patch(user._id, { preferences: undefined }); - }, -}); -``` - -Make sure your code is already writing to the new `userPreferences` table for -new users before running this migration, so you don't miss documents created -during the migration window. - -## Cleaning Up Orphaned Documents - -```typescript -export const deleteOrphanedEmbeddings = migrations.define({ - table: "embeddings", - migrateOne: async (ctx, doc) => { - const chunk = await ctx.db - .query("chunks") - .withIndex("by_embedding", (q) => q.eq("embeddingId", doc._id)) - .first(); - - if (!chunk) { - await ctx.db.delete(doc._id); - } - }, -}); -``` - -## Zero-Downtime Strategies - -During the migration window, your app must handle both old and new data formats. -There are two main strategies. - -### Dual Write (Preferred) - -Write to both old and new structures. Read from the old structure until -migration is complete. - -1. Deploy code that writes both formats, reads old format -2. Run migration on existing data -3. Deploy code that reads new format, still writes both -4. Deploy code that only reads and writes new format - -This is preferred because you can safely roll back at any point, the old format -is always up to date. - -```typescript -// Bad: only writing to new structure before migration is done -export const createTeam = mutation({ - args: { name: v.string(), isPro: v.boolean() }, - handler: async (ctx, args) => { - await ctx.db.insert("teams", { - name: args.name, - plan: args.isPro ? "pro" : "basic", - }); - }, -}); - -// Good: writing to both structures during migration -export const createTeam = mutation({ - args: { name: v.string(), isPro: v.boolean() }, - handler: async (ctx, args) => { - const plan = args.isPro ? "pro" : "basic"; - await ctx.db.insert("teams", { - name: args.name, - isPro: args.isPro, - plan, - }); - }, -}); -``` - -### Dual Read - -Read both formats. Write only the new format. - -1. Deploy code that reads both formats (preferring new), writes only new format -2. Run migration on existing data -3. Deploy code that reads and writes only new format - -This avoids duplicating writes, which is useful when having two copies of data -could cause inconsistencies. The downside is that rolling back to before step 1 -is harder, since new documents only have the new format. - -```typescript -// Good: reading both formats, preferring new -function getTeamPlan(team: Doc<"teams">): "basic" | "pro" { - if (team.plan !== undefined) return team.plan; - return team.isPro ? "pro" : "basic"; -} -``` - -## Small Table Shortcut - -For small tables (a few thousand documents at most), you can migrate in a single -`internalMutation` without the component: - -```typescript -import { internalMutation } from "./_generated/server"; - -export const backfillSmallTable = internalMutation({ - handler: async (ctx) => { - const docs = await ctx.db.query("smallConfig").collect(); - for (const doc of docs) { - if (doc.newField === undefined) { - await ctx.db.patch(doc._id, { newField: "default" }); - } - } - }, -}); -``` - -```bash -npx convex run migrations:backfillSmallTable -``` - -Only use `.collect()` when you are certain the table is small. For anything -larger, use the migrations component. - -## Verifying a Migration - -Query to check remaining unmigrated documents: - -```typescript -import { query } from "./_generated/server"; - -export const verifyMigration = query({ - handler: async (ctx) => { - const remaining = await ctx.db - .query("users") - .withIndex("by_role", (q) => q.eq("role", undefined)) - .take(10); - - return { - complete: remaining.length === 0, - sampleRemaining: remaining.map((u) => u._id), - }; - }, -}); -``` - -Or use the component's built-in status monitoring: - -```bash -npx convex run --component migrations lib:getStatus --watch -``` diff --git a/packages/backend/.claude/skills/convex-migration-helper/references/migrations-component.md b/packages/backend/.claude/skills/convex-migration-helper/references/migrations-component.md deleted file mode 100644 index f4daa65..0000000 --- a/packages/backend/.claude/skills/convex-migration-helper/references/migrations-component.md +++ /dev/null @@ -1,219 +0,0 @@ -# Migrations Component Reference - -Complete guide to the -[`@convex-dev/migrations`](https://www.convex.dev/components/migrations) -component for batched, resumable Convex data migrations. - -## Installation - -```bash -npm install @convex-dev/migrations -``` - -## Setup - -```typescript -// convex/convex.config.ts -import { defineApp } from "convex/server"; -import migrations from "@convex-dev/migrations/convex.config.js"; - -const app = defineApp(); -app.use(migrations); -export default app; -``` - -```typescript -// convex/migrations.ts -import { Migrations } from "@convex-dev/migrations"; -import { components } from "./_generated/api.js"; -import { DataModel } from "./_generated/dataModel.js"; - -export const migrations = new Migrations(components.migrations); -``` - -The `DataModel` type parameter is optional but provides type safety for -migration definitions. - -## Define a Migration - -The `migrateOne` function processes a single document. The component handles -batching and pagination automatically. - -```typescript -// convex/migrations.ts -export const addDefaultRole = migrations.define({ - table: "users", - migrateOne: async (ctx, user) => { - if (user.role === undefined) { - await ctx.db.patch(user._id, { role: "user" }); - } - }, -}); -``` - -Shorthand: if you return an object, it is applied as a patch automatically. - -```typescript -export const clearDeprecatedField = migrations.define({ - table: "users", - migrateOne: () => ({ legacyField: undefined }), -}); -``` - -## Run a Migration - -From the CLI: - -```bash -npx convex run migrations:addDefaultRole - -# Pass --prod to run in production. -npx convex run migrations:addDefaultRole --prod -``` - -The migration exported by `migrations.define` is directly callable from the CLI -or dashboard. You do not need a separate one-off runner for normal single -migrations. - -If you want a general-purpose runner that accepts a migration name, define one: - -```typescript -export const run = migrations.runner(); -``` - -Then call it with the full function name: - -```bash -npx convex run migrations:run '{"fn": "migrations:addDefaultRole"}' -``` - -Programmatically from another Convex function: - -```typescript -await migrations.runOne(ctx, internal.migrations.addDefaultRole); -``` - -## Run Multiple Migrations in Order - -For a short ad hoc series, pass `next` when starting the first migration: - -```bash -npx convex run migrations:addDefaultRole '{"next":["migrations:clearDeprecatedField","migrations:normalizeEmails"]}' -``` - -For a reusable series, define a runner: - -```typescript -export const runAll = migrations.runner([ - internal.migrations.addDefaultRole, - internal.migrations.clearDeprecatedField, - internal.migrations.normalizeEmails, -]); -``` - -```bash -npx convex run migrations:runAll -``` - -If one fails, it stops and will not continue to the next. Call it again to retry -from where it left off. Completed migrations are skipped automatically. - -Programmatically from another Convex function: - -```typescript -await migrations.runSerially(ctx, [ - internal.migrations.addDefaultRole, - internal.migrations.clearDeprecatedField, - internal.migrations.normalizeEmails, -]); -``` - -## Dry Run - -Test a migration before committing changes: - -```bash -npx convex run migrations:addDefaultRole '{"dryRun": true}' -``` - -This runs one batch and then rolls back, so you can see what it would do without -changing any data. - -## Restart a Migration - -Pass `reset: true` to restart a migration from the beginning: - -```bash -npx convex run migrations:addDefaultRole '{"reset": true}' -``` - -If you specify `next` or run a defined series, `reset: true` resets the cursor -for all migrations in the group. - -## Check Migration Status - -```bash -npx convex run --component migrations lib:getStatus --watch -``` - -## Cancel a Running Migration - -```bash -npx convex run --component migrations lib:cancel '{"name": "migrations:addDefaultRole"}' -``` - -Or programmatically: - -```typescript -await migrations.cancel(ctx, internal.migrations.addDefaultRole); -``` - -## Run Migrations on Deploy - -Chain migration execution after deploying: - -```bash -npx convex deploy --cmd 'npm run build' && npx convex run migrations:runAll --prod -``` - -## Configuration Options - -### Custom Batch Size - -If documents are large or the table has heavy write traffic, reduce the batch -size to avoid transaction limits or OCC conflicts: - -```typescript -export const migrateHeavyTable = migrations.define({ - table: "largeDocuments", - batchSize: 10, - migrateOne: async (ctx, doc) => { - // migration logic - }, -}); -``` - -### Migrate a Subset Using an Index - -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", "")), - migrateOne: () => ({ name: "" }), -}); -``` - -### Parallelize Within a Batch - -By default each document in a batch is processed serially. Enable parallel -processing if your migration logic does not depend on ordering: - -```typescript -export const clearField = migrations.define({ - table: "myTable", - parallelize: true, - migrateOne: () => ({ optionalField: undefined }), -}); -``` diff --git a/packages/backend/.claude/skills/convex-performance-audit/SKILL.md b/packages/backend/.claude/skills/convex-performance-audit/SKILL.md deleted file mode 100644 index 998af1d..0000000 --- a/packages/backend/.claude/skills/convex-performance-audit/SKILL.md +++ /dev/null @@ -1,185 +0,0 @@ ---- -name: convex-performance-audit -description: - Audits Convex performance for reads, subscriptions, write contention, and - function limits. Use for slow features, insights findings, OCC conflicts, or - read amplification. ---- - -# Convex Performance Audit - -Diagnose and fix performance problems in Convex applications, one problem class -at a time. - -## When to Use - -- A Convex page or feature feels slow or expensive -- `npx convex insights --details` reports high bytes read, documents read, or - OCC conflicts -- Low-freshness read paths are using reactivity where point-in-time reads would - do -- OCC conflict errors or excessive mutation retries -- High subscription count or slow UI updates -- Functions approaching execution or transaction limits -- The same performance pattern needs fixing across sibling functions - -## When Not to Use - -- Initial Convex setup, auth setup, or component extraction -- Pure schema migrations with no performance goal -- One-off micro-optimizations without a user-visible or deployment-visible - problem - -## Guardrails - -- Prefer simpler code when scale is small, traffic is modest, or the available - signals are weak -- Do not recommend digest tables, document splitting, fetch-strategy changes, or - migration-heavy rollouts unless there is a measured signal, a clearly - unbounded path, or a known hot read/write path -- In Convex, a simple scan on a small table is often acceptable. Do not invent - structural work just because a pattern is not ideal at large scale - -## First Step: Gather Signals - -Start with the strongest signal available: - -1. If deployment Health insights are already available from the user or the - current context, treat them as a first-class source of performance signals. -2. If CLI insights are available, run `npx convex insights --details`. Use - `--prod`, `--preview-name`, or `--deployment-name` when needed. - - If the local repo's Convex CLI is too old to support `insights`, try - `npx -y convex@latest insights --details` before giving up. -3. If the repo already uses `convex-doctor`, you may treat its findings as - hints. Do not require it, and do not treat it as the source of truth. -4. If runtime signals are unavailable, audit from code anyway, but keep the - guardrails above in mind. Lack of insights is not proof of health, but it is - also not proof that a large refactor is warranted. - -## Signal Routing - -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` | - -Multiple problem classes can overlap. Read the most relevant reference first, -then check the others if symptoms remain. - -## Escalate Larger Fixes - -If the likely fix is invasive, cross-cutting, or migration-heavy, stop and -present options before editing. - -Examples: - -- introducing digest or summary tables across multiple flows -- splitting documents to isolate frequently-updated fields -- reworking pagination or fetch strategy across several screens -- switching to a new index or denormalized field that needs migration-safe - rollout - -When correctness depends on handling old and new states during a rollout, -consult the `convex-migration-helper` skill for the migration workflow. - -## Workflow - -### 1. Scope the problem - -Pick one concrete user flow from the actual project. Look at the codebase, -client pages, and API surface to find the flow that matches the symptom. - -Write down: - -- entrypoint functions -- client callsites using `useQuery`, `usePaginatedQuery`, or `useMutation` -- tables read -- tables written -- whether the path is high-read, high-write, or both - -### 2. Trace the full read and write set - -For each function in the path: - -1. Trace every `ctx.db.get()` and `ctx.db.query()` -2. Trace every `ctx.db.patch()`, `ctx.db.replace()`, and `ctx.db.insert()` -3. Note foreign-key lookups, JS-side filtering, and full-document reads -4. Identify all sibling functions touching the same tables -5. Identify reactive stats, aggregates, or widgets rendered on the same page - -In Convex, every extra read increases transaction work, and every write can -invalidate reactive subscribers. Treat read amplification and invalidation -amplification as first-class problems. - -### 3. Apply fixes from the relevant reference - -Read the reference file matching your problem class. Each reference includes -specific patterns, code examples, and a recommended fix order. - -Do not stop at the single function named by an insight. Trace sibling readers -and writers touching the same tables. - -### 4. Fix sibling functions together - -When one function touching a table has a performance bug, audit sibling -functions for the same pattern. - -After finding one problem, inspect both sibling readers and sibling writers for -the same table family, including companion digest or summary tables. - -Examples: - -- If one list query switches from full docs to a digest table, inspect the other - list queries for that 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. - -### 5. Verify before finishing - -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. 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 - -- `references/hot-path-rules.md` - Read amplification, invalidation, - denormalization, indexes, digest tables -- `references/occ-conflicts.md` - Write contention, OCC resolution, hot document - splitting -- `references/subscription-cost.md` - Reactive query cost, subscription - granularity, point-in-time reads -- `references/function-budget.md` - Execution limits, transaction size, large - documents, payload size - -Also check the official -[Convex Best Practices](https://docs.convex.dev/understanding/best-practices/) -page for additional patterns covering argument validation, access control, and -code organization that may surface during the audit. - -## Checklist - -- [ ] Gathered signals from insights, dashboard, or code audit -- [ ] Identified the problem class and read the matching reference -- [ ] Scoped one concrete user flow or function path -- [ ] Traced every read and write in that path -- [ ] Identified sibling functions touching the same tables -- [ ] Applied fixes from the reference, following the recommended fix order -- [ ] Fixed sibling functions consistently -- [ ] Verified behavior and confirmed no regressions diff --git a/packages/backend/.claude/skills/convex-performance-audit/agents/openai.yaml b/packages/backend/.claude/skills/convex-performance-audit/agents/openai.yaml deleted file mode 100644 index 0a1d788..0000000 --- a/packages/backend/.claude/skills/convex-performance-audit/agents/openai.yaml +++ /dev/null @@ -1,14 +0,0 @@ -interface: - display_name: "Convex Performance Audit" - short_description: - "Audit slow Convex reads, subscriptions, OCC conflicts, and limits." - icon_small: "./assets/icon.svg" - icon_large: "./assets/icon.svg" - brand_color: "#EF4444" - default_prompt: - "Audit this Convex app for performance issues. Start with the strongest - signal available, identify the problem class, and suggest the smallest - high-impact fix before proposing bigger structural changes." - -policy: - allow_implicit_invocation: true diff --git a/packages/backend/.claude/skills/convex-performance-audit/assets/icon.svg b/packages/backend/.claude/skills/convex-performance-audit/assets/icon.svg deleted file mode 100644 index 7ab9e09..0000000 --- a/packages/backend/.claude/skills/convex-performance-audit/assets/icon.svg +++ /dev/null @@ -1,3 +0,0 @@ - diff --git a/packages/backend/.claude/skills/convex-performance-audit/references/function-budget.md b/packages/backend/.claude/skills/convex-performance-audit/references/function-budget.md deleted file mode 100644 index 3e8dfef..0000000 --- a/packages/backend/.claude/skills/convex-performance-audit/references/function-budget.md +++ /dev/null @@ -1,254 +0,0 @@ -# Function Budget - -Use these rules when functions are hitting execution limits, transaction size -errors, or returning excessively large payloads to the client. - -## Core Principle - -Convex functions run inside transactions with budgets for time, reads, and -writes. Staying well within these limits is not just about avoiding errors, it -reduces latency and contention. - -## Limits to Know - -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 | -| 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 | - -## Symptoms - -- "Function execution took too long" errors -- "Transaction too large" or read/write set size errors -- Slow queries that read many documents -- Client receiving large payloads that slow down page load -- `npx convex insights --details` showing high bytes read - -## Common Causes - -### Unbounded collection - -A query that calls `.collect()` on a table without a reasonable limit. As the -table grows, the query reads more and more documents. - -### Large document reads on hot paths - -Reading documents with large fields (rich text, embedded media references, long -arrays) when only a small subset of the data is needed for the current view. - -### Mutation doing too much work - -A single mutation that updates hundreds of documents, backfills data, or -rebuilds derived state in one transaction. - -### Returning too much data to the client - -A query returning full documents when the client only needs a few fields. - -## Fix Order - -### 1. Bound your reads - -Never `.collect()` without a limit on a table that can grow unbounded. - -```ts -// Bad: unbounded read, breaks as the table grows -const messages = await ctx.db.query("messages").collect(); -``` - -```ts -// Good: paginate or limit -const messages = await ctx.db - .query("messages") - .withIndex("by_channel", (q) => q.eq("channelId", channelId)) - .order("desc") - .take(50); -``` - -### 2. Read smaller shapes - -If the list page only needs title, author, and date, do not read full documents -with rich content fields. - -Use digest or summary tables for hot list pages. See `hot-path-rules.md` for the -digest table pattern. - -### 3. Break large mutations into batches - -If a mutation needs to update hundreds of documents, split it into a -self-scheduling chain. - -```ts -// Bad: one mutation updating every row -export const backfillAll = internalMutation({ - handler: async (ctx) => { - const docs = await ctx.db.query("items").collect(); - for (const doc of docs) { - await ctx.db.patch(doc._id, { newField: computeValue(doc) }); - } - }, -}); -``` - -```ts -// Good: cursor-based batch processing -export const backfillBatch = internalMutation({ - args: { cursor: v.optional(v.string()), batchSize: v.optional(v.number()) }, - handler: async (ctx, args) => { - const batchSize = args.batchSize ?? 100; - const result = await ctx.db - .query("items") - .paginate({ cursor: args.cursor ?? null, numItems: batchSize }); - - for (const doc of result.page) { - if (doc.newField === undefined) { - await ctx.db.patch(doc._id, { newField: computeValue(doc) }); - } - } - - if (!result.isDone) { - await ctx.scheduler.runAfter(0, internal.items.backfillBatch, { - cursor: result.continueCursor, - batchSize, - }); - } - }, -}); -``` - -### 4. Move heavy work to actions - -Queries and mutations run inside Convex's transactional runtime with strict -budgets. If you need to do CPU-intensive computation, call external APIs, or -process large files, use an action instead. - -Actions run outside the transaction and can call mutations to write results -back. - -```ts -// Bad: heavy computation inside a mutation -export const processUpload = mutation({ - handler: async (ctx, args) => { - const result = expensiveComputation(args.data); - await ctx.db.insert("results", result); - }, -}); -``` - -```ts -// Good: action for heavy work, mutation for the write -export const processUpload = action({ - handler: async (ctx, args) => { - const result = expensiveComputation(args.data); - await ctx.runMutation(internal.results.store, { result }); - }, -}); -``` - -### 5. Trim return values - -Only return what the client needs. If a query fetches full documents but the -component only renders a few fields, map the results before returning. - -```ts -// Bad: returns full documents including large content fields -export const list = query({ - handler: async (ctx) => { - return await ctx.db.query("articles").take(20); - }, -}); -``` - -```ts -// Good: project to only the fields the client needs -export const list = query({ - handler: async (ctx) => { - const articles = await ctx.db.query("articles").take(20); - return articles.map((a) => ({ - _id: a._id, - title: a.title, - author: a.author, - createdAt: a._creationTime, - })); - }, -}); -``` - -### 6. Replace `ctx.runQuery` and `ctx.runMutation` with helper functions - -Inside queries and mutations, `ctx.runQuery` and `ctx.runMutation` have overhead -compared to calling a plain TypeScript helper function. They run in the same -transaction but pay extra per-call cost. - -```ts -// Bad: unnecessary overhead from ctx.runQuery inside a mutation -export const createProject = mutation({ - handler: async (ctx, args) => { - const user = await ctx.runQuery(api.users.getCurrentUser); - await ctx.db.insert("projects", { ...args, ownerId: user._id }); - }, -}); -``` - -```ts -// Good: plain helper function, no extra overhead -export const createProject = mutation({ - handler: async (ctx, args) => { - const user = await getCurrentUser(ctx); - await ctx.db.insert("projects", { ...args, ownerId: user._id }); - }, -}); -``` - -Exception: components require `ctx.runQuery`/`ctx.runMutation`. Use them there, -but prefer helpers everywhere else. - -### 7. Avoid unnecessary `runAction` calls - -`runAction` from within an action creates a separate function invocation with -its own memory and CPU budget. The parent action just sits idle waiting. Replace -with a plain TypeScript function call unless you need a different runtime (e.g. -calling Node.js code from the Convex runtime). - -```ts -// Bad: runAction overhead for no reason -export const processItems = action({ - handler: async (ctx, args) => { - for (const item of args.items) { - await ctx.runAction(internal.items.processOne, { item }); - } - }, -}); -``` - -```ts -// Good: plain function call -export const processItems = action({ - handler: async (ctx, args) => { - for (const item of args.items) { - await processOneItem(ctx, { item }); - } - }, -}); -``` - -## Verification - -1. No function execution or transaction size errors -2. `npx convex insights --details` shows reduced bytes read -3. Large mutations are batched and self-scheduling -4. Client payloads are reasonably sized for the UI they serve -5. `ctx.runQuery`/`ctx.runMutation` in queries and mutations replaced with - helpers where possible -6. Sibling functions with similar patterns were checked diff --git a/packages/backend/.claude/skills/convex-performance-audit/references/hot-path-rules.md b/packages/backend/.claude/skills/convex-performance-audit/references/hot-path-rules.md deleted file mode 100644 index 142231d..0000000 --- a/packages/backend/.claude/skills/convex-performance-audit/references/hot-path-rules.md +++ /dev/null @@ -1,411 +0,0 @@ -# Hot Path Rules - -Use these rules when the top-level workflow points to read amplification, -denormalization, index rollout, reactive query cost, or invalidation-heavy -writes. - -## Contents - -- Core Principle -- Consistency Rule -- 1. Push Filters To Storage (indexes, migration rule, redundant indexes) -- 2. Minimize Data Sources (denormalization, fallback rule) -- 3. Minimize Row Size (digest tables) -- 4. Skip No-Op Writes -- 5. Match Consistency To Read Patterns (high-read/low-write, - high-read/high-write) -- Convex-Specific Notes (reactive queries, point-in-time reads, triggers, - aggregates, backfills) -- Verification - -## Core Principle - -Every byte read or written multiplies with concurrency. - -Think: - -`cost x calls_per_second x 86400` - -In Convex, every write can also fan out into reactive invalidation, replication -work, and downstream sync. - -## Consistency Rule - -If you fix a hot-path pattern for one function, audit sibling functions touching -the same tables for the same pattern. - -Do this especially for: - -- multiple list queries over the same table -- multiple writers to the same table -- public browse and search queries over the same records -- helper functions reused by more than one endpoint - -## 1. Push Filters To Storage - -Both JavaScript `.filter()` and the Convex query `.filter()` method after a DB -scan mean you already paid for the read. The Convex `.filter()` method has the -same performance as filtering in JS, it does not push the predicate to the -storage layer. Only `.withIndex()` and `.withSearchIndex()` actually reduce the -documents scanned. - -Prefer: - -- `withIndex(...)` -- `.withSearchIndex(...)` for text search -- narrower tables -- summary tables - -before accepting a scan-plus-filter pattern. - -```ts -// Bad: scans then filters in JavaScript -export const listOpen = query({ - args: {}, - handler: async (ctx) => { - const tasks = await ctx.db.query("tasks").collect(); - return tasks.filter((task) => task.status === "open"); - }, -}); -``` - -```ts -// Also bad: Convex .filter() does not push to storage either -export const listOpen = query({ - args: {}, - handler: async (ctx) => { - return await ctx.db - .query("tasks") - .filter((q) => q.eq(q.field("status"), "open")) - .collect(); - }, -}); -``` - -```ts -// Good: use an index so storage does the filtering -export const listOpen = query({ - args: {}, - handler: async (ctx) => { - return await ctx.db - .query("tasks") - .withIndex("by_status", (q) => q.eq("status", "open")) - .collect(); - }, -}); -``` - -### Migration rule for indexes - -New indexes on partially backfilled fields can create correctness bugs during -rollout. - -Important Convex detail: - -`undefined !== false` - -If an older document is missing a field entirely, it will not match a compound -index entry that expects `false`. - -Do not trust old comments saying a field is "not backfilled" or "already -backfilled". Verify. - -If correctness depends on handling old and new states during rollout, do not -improvise a partial-backfill workaround in the hot path. Use a migration-safe -rollout and consult the `convex-migration-helper` skill. - -```ts -// Bad: optional booleans can miss older rows where the field is undefined -const projects = await ctx.db - .query("projects") - .withIndex("by_archived_and_updated", (q) => q.eq("isArchived", false)) - .order("desc") - .take(20); -``` - -```ts -// Good: switch hot-path reads only after the rollout is migration-safe -// See the migration helper skill for dual-read / backfill / cutover patterns. -``` - -### Check for redundant indexes - -Indexes like `by_foo` and `by_foo_and_bar` are usually redundant. You only need -`by_foo_and_bar`, since you can query it with just the `foo` condition and omit -`bar`. Extra indexes add storage cost and write overhead on every insert, patch, -and delete. - -```ts -// 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"]); -``` - -```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"], -); -``` - -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. - -## 2. Minimize Data Sources - -Trace every read. - -If a function resolves a foreign key for a tiny display field and a denormalized -copy already exists, prefer the denormalized field on the hot path. - -### When to denormalize - -Denormalize when all of these are true: - -- the path is hot -- the joined document is much larger than the field you need -- many readers are paying that join cost repeatedly - -Useful mental model: - -`join_cost = rows_per_page x foreign_doc_size x pages_per_second` - -Small-table joins are often fine. Large-document joins for tiny fields on hot -list pages are usually not. - -### Fallback rule - -Denormalized data is an optimization. Live data is the correctness path. - -Rules: - -- If the denormalized field is missing or null, fall back to the live read -- Do not show placeholders instead of falling back -- In lookup maps, only include fully populated entries - -```ts -// Bad: missing denormalized data becomes a placeholder and blocks correctness -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; -``` - -Bad lookup map pattern: - -```ts -const ownersById = { - [project.ownerId]: { ownerName: null }, -}; -``` - -That blocks fallback because the map says "I have data" when it does not. - -Good lookup map pattern: - -```ts -const ownersById = - project.ownerName !== undefined && project.ownerName !== null - ? { [project.ownerId]: { ownerName: project.ownerName } } - : {}; -``` - -### No denormalized copy yet - -Prefer adding fields to an existing summary, companion, or digest table instead -of bloating the primary hot-path table. - -If introducing the new field or table requires a staged rollout, backfill, or -old/new-shape handling, use the migration helper skill for the rollout plan. - -Rollout order: - -1. Update schema -2. Update write path -3. Backfill -4. Switch read path - -## 3. Minimize Row Size - -Hot list pages should read the smallest document shape that still answers the -UI. - -Prefer summary or digest tables over full source tables when: - -- the list page only needs a subset of fields -- source documents are large -- the query is high volume - -An 800 byte summary row is materially cheaper than a 3 KB full document on a hot -page. - -Digest tables are a tradeoff, not a default: - -- Worth it when the path is clearly hot, the source rows are much larger than - the UI needs, or many readers are repeatedly paying the same join and payload - cost -- Probably not worth it when an indexed read on the source table is already - cheap enough, the table is still small, or the extra write and migration - complexity would dominate the benefit - -```ts -// Bad: list page reads source docs, then joins owner data per row -const projects = await ctx.db - .query("projects") - .withIndex("by_public", (q) => q.eq("isPublic", true)) - .collect(); -``` - -```ts -// Good: list page reads the smaller digest shape first -const projects = await ctx.db - .query("projectDigests") - .withIndex("by_public_and_updated", (q) => q.eq("isPublic", true)) - .order("desc") - .take(20); -``` - -## 4. Isolate Frequently-Updated Fields - -Convex already no-ops unchanged writes. The invalidation problem here is real -writes hitting documents that many queries subscribe to. - -Move high-churn fields like `lastSeen`, counters, presence, or ephemeral status -off widely-read documents when most readers do not need them. - -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: 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: 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 - -Choose read strategy based on traffic shape. - -### High-read, low-write - -Examples: - -- public browse pages -- search results -- landing pages -- directory listings - -Prefer: - -- point-in-time reads where appropriate -- explicit refresh -- local state for pagination -- caching where appropriate - -Do not treat subscriptions as automatically wrong here. Prefer point-in-time -reads only when the product does not need live freshness and the reactive cost -is material. See `subscription-cost.md` for detailed patterns. - -### High-read, high-write - -Examples: - -- collaborative editors -- live dashboards -- presence-heavy views - -Reactive queries may be worth the ongoing cost. - -## Convex-Specific Notes - -### Reactive queries - -Every `ctx.db.get()` and `ctx.db.query()` contributes to the invalidation set -for the query. - -On the client: - -- `useQuery` creates a live subscription -- `usePaginatedQuery` creates a live subscription per page - -For low-freshness flows, consider a point-in-time read instead of a live -subscription only when the product does not need updates pushed automatically. - -### Point-in-time reads - -Framework helpers, server-rendered fetches, or one-shot client reads can avoid -ongoing subscription cost when live updates are not useful. - -Use them for: - -- aggregate snapshots -- reports -- low-churn listings -- pages where explicit refresh is fine - -### Triggers and fan-out - -Triggers fire on every write, including writes that did not materially change -the document. - -When a write exists only to keep derived state in sync: - -- diff before patching -- move expensive non-blocking work to `ctx.scheduler.runAfter` when appropriate - -### Aggregates - -Reactive global counts invalidate frequently on busy tables. - -Prefer: - -- one-shot aggregate fetches -- periodic recomputation -- precomputed summary rows - -for global stats that do not need live updates every second. - -### Backfills - -For larger backfills, use cursor-based, self-scheduling `internalMutation` jobs -or the migrations component. - -Deploy code that can handle both states before running the backfill. - -During the gap: - -- writes should populate the new shape -- reads should fall back safely - -## Verification - -Before closing the audit, confirm: - -1. Same results as before, no dropped records -2. The removed table or lookup is no longer in the hot-path read set -3. Tests or validation cover fallback behavior -4. Migration safety is preserved while fields or indexes are unbackfilled -5. Sibling functions were fixed consistently diff --git a/packages/backend/.claude/skills/convex-performance-audit/references/occ-conflicts.md b/packages/backend/.claude/skills/convex-performance-audit/references/occ-conflicts.md deleted file mode 100644 index e7d0954..0000000 --- a/packages/backend/.claude/skills/convex-performance-audit/references/occ-conflicts.md +++ /dev/null @@ -1,137 +0,0 @@ -# OCC Conflict Resolution - -Use these rules when insights, logs, or dashboard health show OCC (Optimistic -Concurrency Control) conflicts, mutation retries, or write contention on hot -tables. - -## Core Principle - -Convex uses optimistic concurrency control. When two transactions read or write -overlapping data, one succeeds and the other retries automatically. High -contention means wasted work and increased latency. - -## Symptoms - -- OCC conflict errors in deployment logs or health page -- Mutations retrying multiple times before succeeding -- User-visible latency spikes on write-heavy pages -- `npx convex insights --details` showing high conflict rates - -## Common Causes - -### Hot documents - -Multiple mutations writing to the same document concurrently. Classic examples: -a global counter, a shared settings row, or a "last updated" timestamp on a -parent record. - -### Broad read sets causing false conflicts - -A query that scans a large table range creates a broad read set. If any write -touches that range, the query's transaction conflicts even if the specific -document the query cared about was not modified. - -### Fan-out from triggers or cascading writes - -A single user action triggers multiple mutations that all touch related -documents. Each mutation competes with the others. - -Database triggers (e.g. from `convex-helpers`) run inside the same transaction -as the mutation that caused them. If a trigger does heavy work, reads extra -tables, or writes to many documents, it extends the transaction's read/write set -and increases the window for conflicts. Keep trigger logic minimal, or move -expensive derived work to a scheduled function. - -### Write-then-read chains - -A mutation writes a document, then a reactive query re-reads it, then another -mutation writes it again. Under load, these chains stack up. - -## Fix Order - -### 1. Reduce read set size - -Narrower reads mean fewer false conflicts. - -```ts -// Bad: broad scan creates a wide conflict surface -const allTasks = await ctx.db.query("tasks").collect(); -const mine = allTasks.filter((t) => t.ownerId === userId); -``` - -```ts -// Good: indexed query touches only relevant documents -const mine = await ctx.db - .query("tasks") - .withIndex("by_owner", (q) => q.eq("ownerId", userId)) - .collect(); -``` - -### 2. Split hot documents - -When many writers target the same document, split the contention point. - -```ts -// Bad: every vote increments the same counter document -const counter = await ctx.db.get(pollCounterId); -await ctx.db.patch(pollCounterId, { count: counter!.count + 1 }); -``` - -```ts -// Good: shard the counter across multiple documents, aggregate on read -const shardIndex = Math.floor(Math.random() * SHARD_COUNT); -const shardId = shardIds[shardIndex]; -const shard = await ctx.db.get(shardId); -await ctx.db.patch(shardId, { count: shard!.count + 1 }); -``` - -Aggregate the shards in a query or scheduled job when you need the total. - -### 3. Move non-critical work to scheduled functions - -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: 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, -}); -``` - -```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. - -Do not introduce artificial locks or queues unless the above steps have been -tried first. - -## Related: Invalidation Scope - -Splitting hot documents also reduces subscription invalidation, not just OCC -contention. If a document is written frequently and read by many queries, those -queries re-run on every write even when the fields they care about have not -changed. See `subscription-cost.md` section 4 ("Isolate frequently-updated -fields") for that pattern. - -## Verification - -1. OCC conflict rate has dropped in insights or dashboard -2. Mutation latency is lower and more consistent -3. No data correctness regressions from splitting or scheduling changes -4. Sibling writers to the same hot documents were fixed consistently diff --git a/packages/backend/.claude/skills/convex-performance-audit/references/subscription-cost.md b/packages/backend/.claude/skills/convex-performance-audit/references/subscription-cost.md deleted file mode 100644 index 78f05b0..0000000 --- a/packages/backend/.claude/skills/convex-performance-audit/references/subscription-cost.md +++ /dev/null @@ -1,300 +0,0 @@ -# Subscription Cost - -Use these rules when the problem is too many reactive subscriptions, queries -invalidating too frequently, or React components re-rendering excessively due to -Convex state changes. - -## Core Principle - -Every `useQuery` and `usePaginatedQuery` call creates a live subscription. The -server tracks the query's read set and re-executes the query whenever any -document in that read set changes. Subscription cost scales with: - -`subscriptions x invalidation_frequency x query_cost` - -Subscriptions are not inherently bad. Convex reactivity is often the right -default. The goal is to reduce unnecessary invalidation work, not to eliminate -subscriptions on principle. - -## Symptoms - -- Dashboard shows high active subscription count -- UI feels sluggish or laggy despite fast individual queries -- React profiling shows frequent re-renders from Convex state -- Pages with many components each running their own `useQuery` -- Paginated lists where every loaded page stays subscribed - -## Common Causes - -### Reactive queries on low-freshness flows - -Some user flows are read-heavy and do not need live updates every time the -underlying data changes. In those cases, ongoing subscriptions may cost more -than they are worth. - -### Overly broad queries - -A query that returns a large result set invalidates whenever any document in -that set changes. The broader the query, the more frequent the invalidation. - -### Too many subscriptions per page - -A page with 20 list items, each running its own `useQuery` to fetch related -data, creates 20+ subscriptions per visitor. - -### Paginated queries keeping all pages live - -`usePaginatedQuery` with `loadMore` keeps every loaded page subscribed. On a -page where a user has scrolled through 10 pages, all 10 stay reactive. - -### Frequently-updated fields on widely-read documents - -A document that many queries touch gets a frequently-updated field (like -`lastSeen`, `lastActiveAt`, or a counter). Every write to that field invalidates -every subscription that reads the document, even if those subscriptions never -use the field. This is different from OCC conflicts (see `occ-conflicts.md`), -which are write-vs-write contention. This is write-vs-subscription: the write -succeeds fine, but it forces hundreds of queries to re-run for no reason. - -## Fix Order - -### 1. Use point-in-time reads when live updates are not valuable - -Keep `useQuery` and `usePaginatedQuery` by default when the product benefits -from fresh live data. - -Consider a point-in-time read instead when all of these are true: - -- the flow is high-read -- the underlying data changes less often than users need to see -- explicit refresh, periodic refresh, or a fresh read on navigation is - acceptable - -Possible implementations depend on environment: - -- a server-rendered fetch -- a framework helper like `fetchQuery` -- a point-in-time client read such as `ConvexHttpClient.query()` - -```ts -// Reactive by default when fresh live data matters -function TeamPresence() { - const presence = useQuery(api.teams.livePresence, { teamId }); - return ; -} -``` - -```ts -// Point-in-time read when explicit refresh is acceptable -import { ConvexHttpClient } from "convex/browser"; - -const client = new ConvexHttpClient(import.meta.env.VITE_CONVEX_URL); - -function SnapshotView() { - const [items, setItems] = useState([]); - - useEffect(() => { - client.query(api.items.snapshot).then(setItems); - }, []); - - return ; -} -``` - -Good candidates for point-in-time reads: - -- aggregate snapshots -- reports -- low-churn listings -- flows where explicit refresh is already acceptable - -Keep reactive for: - -- collaborative editing -- live dashboards -- presence-heavy views -- any surface where users expect fresh changes to appear automatically - -### 2. Batch related data into fewer queries - -Instead of N components each fetching their own related data, fetch it in a -single query. - -```ts -// Bad: each card fetches its own author -function ProjectCard({ project }: { project: Project }) { - const author = useQuery(api.users.get, { id: project.authorId }); - return ; -} -``` - -```ts -// Good: parent query returns projects with author names included -function ProjectList() { - const projects = useQuery(api.projects.listWithAuthors); - return projects?.map((p) => ( - - )); -} -``` - -This can use denormalized fields or server-side joins in the query handler. -Either way, it is one subscription instead of N. - -This is not automatically better. If the combined query becomes much broader and -invalidates much more often, several narrower subscriptions may be the better -tradeoff. Optimize for total invalidation cost, not raw subscription count. - -### 3. Use skip to avoid unnecessary subscriptions - -The `"skip"` value prevents a subscription from being created when the arguments -are not ready. - -```ts -// Bad: subscribes with undefined args, wastes a subscription slot -const profile = useQuery(api.users.getProfile, { userId: selectedId! }); -``` - -```ts -// Good: skip when there is nothing to fetch -const profile = useQuery( - api.users.getProfile, - selectedId ? { userId: selectedId } : "skip", -); -``` - -### 4. Isolate frequently-updated fields into separate documents - -If a document is widely read but has a field that changes often, move that field -to a separate document. Queries that do not need the field will no longer be -invalidated by its writes. - -```ts -// Bad: lastSeen lives on the user doc, every heartbeat invalidates -// every query that reads this user -const users = defineTable({ - name: v.string(), - email: v.string(), - lastSeen: v.number(), -}); -``` - -```ts -// Good: lastSeen lives in a separate heartbeat doc -const users = defineTable({ - name: v.string(), - email: v.string(), - heartbeatId: v.id("heartbeats"), -}); - -const heartbeats = defineTable({ - lastSeen: v.number(), -}); -``` - -Queries that only need `name` and `email` no longer re-run on every heartbeat. -Queries that actually need online status fetch the heartbeat document -explicitly. - -For an even further optimization, if you only need a coarse online/offline -boolean rather than the exact `lastSeen` timestamp, add a separate presence -document with an `isOnline` flag. Update it immediately when a user comes -online, and use a cron to batch-mark users offline when their heartbeat goes -stale. This way the presence query only invalidates when online status actually -changes, not on every heartbeat. - -### 5. Use the aggregate component for counts and sums - -Reactive global counts (`SELECT COUNT(*)` equivalent) invalidate on every insert -or delete to the table. The -[`@convex-dev/aggregate`](https://www.npmjs.com/package/@convex-dev/aggregate) -component maintains denormalized COUNT, SUM, and MAX values efficiently so you -do not need a reactive query scanning the full table. - -Use it for leaderboards, totals, "X items" badges, or any stat that would -otherwise require scanning many rows reactively. - -If the aggregate component is not appropriate, prefer point-in-time reads for -global stats, or precomputed summary rows updated by a cron or trigger, over -reactive queries that scan large tables. - -### 6. Narrow query read sets - -Queries that return less data and touch fewer documents invalidate less often. - -```ts -// Bad: returns all fields, invalidates on any field change -export const list = query({ - handler: async (ctx) => { - return await ctx.db.query("projects").collect(); - }, -}); -``` - -```ts -// Good: use a digest table with only the fields the list needs -export const listDigests = query({ - handler: async (ctx) => { - return await ctx.db.query("projectDigests").collect(); - }, -}); -``` - -Writes to fields not in the digest table do not invalidate the digest query. - -### 7. Remove `Date.now()` from queries - -Using `Date.now()` inside a query defeats Convex's query cache. The cache is -invalidated frequently to avoid showing stale time-dependent results, which -increases database work even when the underlying data has not changed. - -```ts -// Bad: Date.now() defeats query caching and causes frequent re-evaluation -const releasedPosts = await ctx.db - .query("posts") - .withIndex("by_released_at", (q) => q.lte("releasedAt", Date.now())) - .take(100); -``` - -```ts -// Good: use a boolean field updated by a scheduled function -const releasedPosts = await ctx.db - .query("posts") - .withIndex("by_is_released", (q) => q.eq("isReleased", true)) - .take(100); -``` - -If the query must compare against a time value, pass it as an explicit argument -from the client and round it to a coarse interval (e.g. the most recent minute) -so requests within that window share the same cache entry. - -### 8. Consider pagination strategy - -For long lists where users scroll through many pages: - -- If the data does not need live updates, use point-in-time fetching with manual - "load more" -- If it does need live updates, accept the subscription cost but limit the - number of loaded pages -- Consider whether older pages can be unloaded as the user scrolls forward - -### 9. Separate backend cost from UI churn - -If the main problem is loading flash or UI churn when query arguments change, -stabilizing the reactive UI behavior may be better than replacing reactivity -altogether. - -Treat this as a UX problem first when: - -- the underlying query is already reasonably cheap -- the complaint is flicker, loading flashes, or re-render churn -- live updates are still desirable once fresh data arrives - -## Verification - -1. Subscription count in dashboard is lower for the affected pages -2. UI responsiveness has improved -3. React profiling shows fewer unnecessary re-renders -4. Surfaces that do not need live updates are not paying for persistent - subscriptions unnecessarily -5. Sibling pages with similar patterns were updated consistently diff --git a/packages/backend/.claude/skills/convex-quickstart/SKILL.md b/packages/backend/.claude/skills/convex-quickstart/SKILL.md deleted file mode 100644 index 09400d3..0000000 --- a/packages/backend/.claude/skills/convex-quickstart/SKILL.md +++ /dev/null @@ -1,451 +0,0 @@ ---- -name: convex-quickstart -description: - Creates or adds Convex to an app. Use for new Convex projects, npm create - convex@latest, frontend setup, env vars, or the first npx convex dev run. ---- - -# Convex Quickstart - -Set up a working Convex project as fast as possible. - -## When to Use - -- Starting a brand new project with Convex -- Adding Convex to an existing React, Next.js, Vue, Svelte, or other app -- Scaffolding a Convex app for prototyping - -## When Not to Use - -- The project already has Convex installed and `convex/` exists - just start - building -- You only need to add auth to an existing Convex app - use the - `convex-setup-auth` skill - -## Workflow - -1. Determine the starting point: new project or existing app -2. If new project, pick a template and scaffold with `npm create convex@latest` -3. If existing app, install `convex` and wire up the provider -4. Run `npx convex dev --once` to provision a local anonymous deployment, push - the current `convex/` code, typecheck it, and regenerate types — all in one - shot, exiting cleanly. The output tells the agent whether the schema and - functions are valid. -5. Ask the user (or, for cloud agents, start in the background) `npm run dev` — - Convex templates wire the watcher and the frontend into a single command. If - the project has no combined dev script, use `npx convex dev` for the watcher - and run the frontend separately. -6. Verify the setup works - -## Path 1: New Project (Recommended) - -Use the official scaffolding tool. It creates a complete project with the -frontend framework, Convex backend, and all config wired together. - -### 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 | - -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. - -You can also use any GitHub repo as a template: - -```bash -npm create convex@latest my-app -- -t owner/repo -npm create convex@latest my-app -- -t owner/repo#branch -``` - -### Scaffold the project - -Always pass the project name and template flag to avoid interactive prompts: - -```bash -npm create convex@latest my-app -- -t react-vite-shadcn -cd my-app -npm install -``` - -The scaffolding tool creates files but does not run `npm install`, so you must -run it yourself. - -To scaffold in the current directory (if it is empty): - -```bash -npm create convex@latest . -- -t react-vite-shadcn -npm install -``` - -### Provision the deployment and push code - -Run this yourself — it is a one-shot command that exits cleanly: - -```bash -npx convex dev --once -``` - -In a non-TTY environment (which is true for almost every agent run), this: - -- Provisions an _anonymous_ local Convex backend bound to `127.0.0.1`. No - browser login, no team/project prompts. -- Writes `CONVEX_DEPLOYMENT` and the framework's `*_CONVEX_URL` variables to - `.env.local`. -- Generates `convex/_generated/`. -- Pushes the current `convex/` code to the deployment, **typechecks it**, and - **validates the schema**. The agent reads this output to find out if the code - it just wrote is broken. - -To be explicit (recommended), set `CONVEX_AGENT_MODE=anonymous` so the behavior -does not depend on TTY detection: - -```bash -CONVEX_AGENT_MODE=anonymous npx convex dev --once -``` - -The deployment lives under `~/.convex/` and persists across runs. Re-running -`convex dev --once` after editing `convex/` files is the agent's main feedback -loop while the user-launched `npm run dev` is not in use. - -If the template's `package.json` defines a `predev` script (Convex Auth -templates and similar do), `npm run predev` runs `convex init` plus any one-time -setup (e.g. minting auth keys). Use it _in addition to_ `convex dev --once` when -present — `predev` handles the one-time setup, `convex dev --once` pushes and -validates the code. - -### Start the dev loop - -In most Convex templates, `npm run dev` runs both the Convex watcher and the -frontend dev server together (typically `convex dev --start 'vite --open'` or -the Next.js equivalent). That is what the user should run. - -```bash -npm run dev -``` - -If the project does not have a combined `dev` script — e.g. the `bare` template, -or an existing app where you haven't wired the frontend dev server into Convex's -`--start` flag — the user can run the Convex watcher directly: - -```bash -npx convex dev -``` - -`npx convex dev` is the same long-running watcher `npm run dev` invokes under -the hood; it just doesn't start the frontend. Use it when there is no frontend, -or when the user prefers to run the frontend in a separate terminal. - -Either way, the agent should not invoke the watcher in the foreground because it -does not exit. Two options: - -- **Local development (user is at the keyboard):** ask the user to run - `npm run dev` (or `npx convex dev`) in a terminal. The deployment provisioned - by `convex dev --once` above is already selected, so the watcher picks up - immediately with no prompts. -- **Cloud or headless agents:** start `npm run dev` (or `npx convex dev`) in the - background. - -Vite apps serve on `http://localhost:5173`, Next.js on `http://localhost:3000`. - -### What you get - -After scaffolding, the project structure looks like: - -``` -my-app/ - convex/ # Backend functions and schema - _generated/ # Auto-generated types (check this into git) - schema.ts # Database schema (if template includes one) - src/ # Frontend code (or app/ for Next.js) - package.json - .env.local # CONVEX_URL / VITE_CONVEX_URL / NEXT_PUBLIC_CONVEX_URL -``` - -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) -- Auth provider configured (for auth templates) - -Proceed to adding schema, functions, and UI. - -## Path 2: Add Convex to an Existing App - -Use this when the user already has a frontend project and wants to add Convex as -the backend. - -### Install - -```bash -npm install convex -``` - -### Provision and push - -Run `npx convex dev --once` yourself to provision a local anonymous deployment, -write `.env.local`, generate types, push the current `convex/` code, and -typecheck it. This is one-shot and exits: - -```bash -npx convex dev --once -``` - -The output tells you whether the schema and functions are valid — use it as your -feedback loop while iterating. - -Then ask the user to start the watcher (or, for cloud/headless agents, start it -in the background). You have two options: - -- **Wire Convex into `npm run dev`** — change the existing app's `dev` script to - `convex dev --start ''`. That's the standard pattern - Convex templates use; the user then runs a single `npm run dev` to start both. -- **Run them separately** — leave `npm run dev` for the frontend and tell the - user to run `npx convex dev` in a second terminal for the Convex watcher. - -See "Start the dev loop" above for why the agent should not run the watcher in -the foreground. - -### Wire up the provider - -The Convex client must wrap the app at the root. The setup varies by framework. - -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, - ); - return ...; -} - -// Good: created once at module scope -const convex = new ConvexReactClient(import.meta.env.VITE_CONVEX_URL as string); -function App() { - return ...; -} -``` - -#### React (Vite) - -```tsx -// src/main.tsx -import { StrictMode } from "react"; -import { createRoot } from "react-dom/client"; -import { ConvexProvider, ConvexReactClient } from "convex/react"; -import App from "./App"; - -const convex = new ConvexReactClient(import.meta.env.VITE_CONVEX_URL as string); - -createRoot(document.getElementById("root")!).render( - - - - - , -); -``` - -#### Next.js (App Router) - -```tsx -// app/ConvexClientProvider.tsx -"use client"; - -import { ConvexProvider, ConvexReactClient } from "convex/react"; -import { ReactNode } from "react"; - -const convex = new ConvexReactClient(process.env.NEXT_PUBLIC_CONVEX_URL!); - -export function ConvexClientProvider({ children }: { children: ReactNode }) { - return {children}; -} -``` - -```tsx -// app/layout.tsx -import { ConvexClientProvider } from "./ConvexClientProvider"; - -export default function RootLayout({ - children, -}: { - children: React.ReactNode; -}) { - return ( - - - {children} - - - ); -} -``` - -#### Other frameworks - -For Vue, Svelte, React Native, TanStack Start, Remix, and others, follow the -matching quickstart guide: - -- [Vue](https://docs.convex.dev/quickstart/vue) -- [Svelte](https://docs.convex.dev/quickstart/svelte) -- [React Native](https://docs.convex.dev/quickstart/react-native) -- [TanStack Start](https://docs.convex.dev/quickstart/tanstack-start) -- [Remix](https://docs.convex.dev/quickstart/remix) -- [Node.js (no frontend)](https://docs.convex.dev/quickstart/nodejs) - -### Environment variables - -The env var name depends on the framework: - -| 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. - -## Agent Mode - -`CONVEX_AGENT_MODE=anonymous` forces an unauthenticated local backend. It is -already the implicit default for any non-TTY run of `npx convex init` or -`npx convex dev`, but set it explicitly so the behavior does not depend on TTY -detection: - -```bash -CONVEX_AGENT_MODE=anonymous npx convex dev --once -``` - -Use it for: - -- Any AI coding agent (local or cloud). -- CI-like setup scripts. -- Cases where the user is logged in but you do not want to touch their personal - dev deployment. - -The resulting backend runs on `127.0.0.1` and is not associated with any team or -project until the user later claims it via `npx convex login` and the -`npx convex deployment` commands. - -## Verify the Setup - -After setup, confirm everything is working: - -1. `npx convex dev --once` exited without errors (deployment provisioned, code - pushed, schema validated, typecheck clean) -2. The `convex/_generated/` directory exists and has `api.ts` and `server.ts` -3. `.env.local` contains a `CONVEX_DEPLOYMENT` value and the framework's - `*_CONVEX_URL` variable -4. (If applicable) `npm run dev` (or `npx convex dev` for the watcher alone) is - running without errors in another terminal or in the background - -## Writing Your First Function - -Once the project is set up, create a schema and a query to verify the full loop -works. - -`convex/schema.ts`: - -```ts -import { defineSchema, defineTable } from "convex/server"; -import { v } from "convex/values"; - -export default defineSchema({ - tasks: defineTable({ - text: v.string(), - completed: v.boolean(), - }), -}); -``` - -`convex/tasks.ts`: - -```ts -import { query, mutation } from "./_generated/server"; -import { v } from "convex/values"; - -export const list = query({ - args: {}, - handler: async (ctx) => { - return await ctx.db.query("tasks").collect(); - }, -}); - -export const create = mutation({ - args: { text: v.string() }, - handler: async (ctx, args) => { - await ctx.db.insert("tasks", { text: args.text, completed: false }); - }, -}); -``` - -Use in a React component (adjust the import path based on your file location -relative to `convex/`): - -```tsx -import { useQuery, useMutation } from "convex/react"; -import { api } from "../convex/_generated/api"; - -function Tasks() { - const tasks = useQuery(api.tasks.list); - const create = useMutation(api.tasks.create); - - return ( -
- - {tasks?.map((t) => ( -
{t.text}
- ))} -
- ); -} -``` - -## Development vs Production - -Always use `npx convex dev` during development. It runs against your personal -dev deployment and syncs code on save. - -When ready to ship, deploy to production: - -```bash -npx convex deploy -``` - -This pushes to the production deployment, which is separate from dev. Do not use -`deploy` during development. - -## Next Steps - -- Add authentication: use the `convex-setup-auth` skill -- Design your schema: see - [Schema docs](https://docs.convex.dev/database/schemas) -- Build components: use the `convex-create-component` skill -- Plan a migration: use the `convex-migration-helper` skill -- Add file storage: see - [File Storage docs](https://docs.convex.dev/file-storage) -- Set up cron jobs: see [Scheduling docs](https://docs.convex.dev/scheduling) - -## Checklist - -- [ ] Determined starting point: new project or existing app -- [ ] If new project: scaffolded with `npm create convex@latest` using - appropriate template -- [ ] If existing app: installed `convex` and wired up the provider -- [ ] Agent ran `npx convex dev --once`: deployment provisioned, code pushed, - typecheck clean -- [ ] `npm run dev` (or `npx convex dev` for the watcher alone) is running — - user-launched terminal, or background for cloud agents -- [ ] `convex/_generated/` directory exists with types -- [ ] `.env.local` has the deployment URL -- [ ] Verified a basic query/mutation round-trip works diff --git a/packages/backend/.claude/skills/convex-quickstart/agents/openai.yaml b/packages/backend/.claude/skills/convex-quickstart/agents/openai.yaml deleted file mode 100644 index 0347a50..0000000 --- a/packages/backend/.claude/skills/convex-quickstart/agents/openai.yaml +++ /dev/null @@ -1,14 +0,0 @@ -interface: - display_name: "Convex Quickstart" - short_description: - "Start a new Convex app or add Convex to an existing frontend." - icon_small: "./assets/icon.svg" - icon_large: "./assets/icon.svg" - brand_color: "#F97316" - default_prompt: - "Set up Convex for this project as fast as possible. First decide whether - this is a new app or an existing app, then scaffold or integrate Convex and - verify the setup works." - -policy: - allow_implicit_invocation: true diff --git a/packages/backend/.claude/skills/convex-quickstart/assets/icon.svg b/packages/backend/.claude/skills/convex-quickstart/assets/icon.svg deleted file mode 100644 index d83a73f..0000000 --- a/packages/backend/.claude/skills/convex-quickstart/assets/icon.svg +++ /dev/null @@ -1,4 +0,0 @@ - diff --git a/packages/backend/.claude/skills/convex-setup-auth/SKILL.md b/packages/backend/.claude/skills/convex-setup-auth/SKILL.md deleted file mode 100644 index 410198f..0000000 --- a/packages/backend/.claude/skills/convex-setup-auth/SKILL.md +++ /dev/null @@ -1,187 +0,0 @@ ---- -name: convex-setup-auth -description: - Sets up Convex auth, identity mapping, and access control. Use for login, auth - providers, users tables, protected functions, or roles in a Convex app. ---- - -# Convex Authentication Setup - -Implement secure authentication in Convex with user management and access -control. - -## When to Use - -- Setting up authentication for the first time -- Implementing user management (users table, identity mapping) -- Creating authentication helper functions -- Setting up auth providers (Convex Auth, Clerk, WorkOS AuthKit, Auth0, custom - JWT) - -## When Not to Use - -- Auth for a non-Convex backend -- Pure OAuth/OIDC documentation without a Convex implementation -- Debugging unrelated bugs that happen to surface near auth code -- The auth provider is already fully configured and the user only needs a - one-line fix - -## First Step: Choose the Auth Provider - -Convex supports multiple authentication approaches. Do not assume a provider. - -Before writing setup code: - -1. Ask the user which auth solution they want, unless the repository already - makes it obvious -2. If the repo already uses a provider, continue with that provider unless the - user wants to switch -3. If the user has not chosen a provider and the repo does not make it obvious, - ask before proceeding - -Common options: - -- [Convex Auth](https://docs.convex.dev/auth/convex-auth) - good default when - the user wants auth handled directly in Convex -- [Clerk](https://docs.convex.dev/auth/clerk) - use when the app already uses - Clerk or the user wants Clerk's hosted auth features -- [WorkOS AuthKit](https://docs.convex.dev/auth/authkit/) - use when the app - already uses WorkOS or the user wants AuthKit specifically -- [Auth0](https://docs.convex.dev/auth/auth0) - use when the app already uses - Auth0 -- Custom JWT provider - use when integrating an existing auth system not covered - above - -Look for signals in the repo before asking: - -- Dependencies such as `@clerk/*`, `@workos-inc/*`, `@auth0/*`, or Convex Auth - packages -- Existing files such as `convex/auth.config.ts`, auth middleware, provider - wrappers, or login components -- Environment variables that clearly point at a provider - -## After Choosing a Provider - -Read the provider's official guide and the matching local reference file: - -- Convex Auth: [official docs](https://docs.convex.dev/auth/convex-auth), then - `references/convex-auth.md` -- Clerk: [official docs](https://docs.convex.dev/auth/clerk), then - `references/clerk.md` -- WorkOS AuthKit: [official docs](https://docs.convex.dev/auth/authkit/), then - `references/workos-authkit.md` -- Auth0: [official docs](https://docs.convex.dev/auth/auth0), then - `references/auth0.md` - -The local reference files contain the concrete workflow, expected files and env -vars, gotchas, and validation checks. - -Use those sources for: - -- package installation -- client provider wiring -- environment variables -- `convex/auth.config.ts` setup -- login and logout UI patterns -- framework-specific setup for React, Vite, or Next.js - -For shared auth behavior, use the official Convex docs as the source of truth: - -- [Auth in Functions](https://docs.convex.dev/auth/functions-auth) for - `ctx.auth.getUserIdentity()` -- [Storing Users in the Convex Database](https://docs.convex.dev/auth/database-auth) - for optional app-level user storage -- [Authentication](https://docs.convex.dev/auth) for general auth and - authorization guidance -- [Convex Auth Authorization](https://labs.convex.dev/auth/authz) when the - provider is Convex Auth - -Prefer official docs over recalled steps, because provider CLIs and Convex Auth -internals change between versions. Inventing setup from memory risks outdated -patterns. For third-party providers, only add app-level user storage if the app -actually needs user documents in Convex. Not every app needs a `users` table. -For Convex Auth, follow the Convex Auth docs and built-in auth tables rather -than adding a parallel `users` table plus `storeUser` flow, because Convex Auth -already manages user records internally. After running provider initialization -commands, verify generated files and complete the post-init wiring steps the -provider reference calls out. Initialization commands rarely finish the entire -integration. - -## Core Pattern: Protecting Backend Functions - -The most common auth task is checking identity in Convex functions. - -```ts -// Bad: trusting a client-provided userId -export const getMyProfile = query({ - args: { userId: v.id("users") }, - handler: async (ctx, args) => { - return await ctx.db.get(args.userId); - }, -}); -``` - -```ts -// Good: verifying identity server-side -export const getMyProfile = query({ - args: {}, - handler: async (ctx) => { - const identity = await ctx.auth.getUserIdentity(); - if (!identity) throw new Error("Not authenticated"); - - return await ctx.db - .query("users") - .withIndex("by_tokenIdentifier", (q) => - q.eq("tokenIdentifier", identity.tokenIdentifier), - ) - .unique(); - }, -}); -``` - -## Workflow - -1. Determine the provider, either by asking the user or inferring from the repo -2. Ask whether the user wants local-only setup or production-ready setup now -3. Read the matching provider reference file -4. Follow the official provider docs for current setup details -5. Follow the official Convex docs for shared backend auth behavior, user - storage, and authorization patterns -6. Only add app-level user storage if the docs and app requirements call for it -7. Add authorization checks for ownership, roles, or team access only where the - app needs them -8. Verify login state, protected queries, environment variables, and production - configuration if requested - -If the flow blocks on interactive provider or deployment setup, ask the user -explicitly for the exact human step needed, then continue after they complete -it. For UI-facing auth flows, offer to validate the real sign-up or sign-in flow -after setup is done. If the environment has browser automation tools, you can -use them. If it does not, give the user a short manual validation checklist -instead. - -## Reference Files - -### Provider References - -- `references/convex-auth.md` -- `references/clerk.md` -- `references/workos-authkit.md` -- `references/auth0.md` - -## Checklist - -- [ ] Chosen the correct auth provider before writing setup code -- [ ] Read the relevant provider reference file -- [ ] Asked whether the user wants local-only setup or production-ready setup -- [ ] Used the official provider docs for provider-specific wiring -- [ ] Used the official Convex docs for shared auth behavior and authorization - patterns -- [ ] Only added app-level user storage if the app actually needs it -- [ ] Did not invent a cross-provider `users` table or `storeUser` flow for - Convex Auth -- [ ] Added authentication checks in protected backend functions -- [ ] Added authorization checks where the app actually needs them -- [ ] Clear error messages ("Not authenticated", "Unauthorized") -- [ ] Client auth provider configured for the chosen provider -- [ ] If requested, production auth setup is covered too diff --git a/packages/backend/.claude/skills/convex-setup-auth/agents/openai.yaml b/packages/backend/.claude/skills/convex-setup-auth/agents/openai.yaml deleted file mode 100644 index f7e207c..0000000 --- a/packages/backend/.claude/skills/convex-setup-auth/agents/openai.yaml +++ /dev/null @@ -1,14 +0,0 @@ -interface: - display_name: "Convex Setup Auth" - short_description: - "Set up Convex auth, user identity mapping, and access control." - icon_small: "./assets/icon.svg" - icon_large: "./assets/icon.svg" - brand_color: "#2563EB" - default_prompt: - "Set up authentication for this Convex app. Figure out the provider first, - then wire up the user model, identity mapping, and access control with the - smallest solid implementation." - -policy: - allow_implicit_invocation: true diff --git a/packages/backend/.claude/skills/convex-setup-auth/assets/icon.svg b/packages/backend/.claude/skills/convex-setup-auth/assets/icon.svg deleted file mode 100644 index 4917dbb..0000000 --- a/packages/backend/.claude/skills/convex-setup-auth/assets/icon.svg +++ /dev/null @@ -1,3 +0,0 @@ - diff --git a/packages/backend/.claude/skills/convex-setup-auth/references/auth0.md b/packages/backend/.claude/skills/convex-setup-auth/references/auth0.md deleted file mode 100644 index c1e08a5..0000000 --- a/packages/backend/.claude/skills/convex-setup-auth/references/auth0.md +++ /dev/null @@ -1,156 +0,0 @@ -# Auth0 - -Official docs: - -- https://docs.convex.dev/auth/auth0 -- https://auth0.github.io/auth0-cli/ -- https://auth0.github.io/auth0-cli/auth0_apps_create.html - -Use this when the app already uses Auth0 or the user wants Auth0 specifically. - -## Workflow - -1. Confirm the user wants Auth0 -2. Determine the app framework and whether Auth0 is already partly set up -3. Ask whether the user wants local-only setup or production-ready setup now -4. Read the official Convex and Auth0 guides before making changes -5. Ask whether they want the fastest setup path by installing the Auth0 CLI -6. If they agree, install the Auth0 CLI and do as much of the Auth0 app setup as - possible through the CLI -7. If they do not want the CLI path, use the Auth0 dashboard path instead -8. Complete the relevant Auth0 frontend quickstart if the app does not already - have Auth0 wired up -9. Configure `convex/auth.config.ts` with the Auth0 domain and client ID -10. Set environment variables for local and production environments -11. Wrap the app with `Auth0Provider` and `ConvexProviderWithAuth0` -12. Gate Convex-backed UI with Convex auth state -13. Try to verify Convex reports the user as authenticated after Auth0 login -14. If the refresh-token path fails, stop improvising and send the user back to - the official docs -15. If the user wants production-ready setup, make sure the production Auth0 - tenant and env vars are also covered - -## What To Do - -- Read the official Convex and Auth0 guide before writing setup code -- Prefer the Auth0 CLI path for mechanical setup if the user is willing to - install it, but do not present it as a fully validated end-to-end path yet -- Ask the user directly: "The fastest path is to install the Auth0 CLI so I can - do more of this for you. If you want, I can install it and then only ask you - to log in when needed. Would you like me to do that?" -- Make sure the app has already completed the relevant Auth0 quickstart for its - frontend -- Use the official examples for `Auth0Provider` and `ConvexProviderWithAuth0` -- If the Auth0 login or refresh flow starts failing in a way that is not clearly - explained by the docs, say that plainly and fall back to the official docs - instead of pretending the flow is validated - -## Key Setup Areas - -- install the Auth0 SDK for the app's framework -- configure `convex/auth.config.ts` with the Auth0 domain and client ID -- set environment variables for local and production environments -- wrap the app with `Auth0Provider` and `ConvexProviderWithAuth0` -- use Convex auth state when gating Convex-backed UI - -## Files and Env Vars To Expect - -- `convex/auth.config.ts` -- frontend app entry or provider wrapper -- Auth0 CLI install docs: `https://auth0.github.io/auth0-cli/` -- Auth0 environment variables commonly include: - - `AUTH0_DOMAIN` - - `AUTH0_CLIENT_ID` - - `VITE_AUTH0_DOMAIN` - - `VITE_AUTH0_CLIENT_ID` - -## Concrete Steps - -1. Start by reading `https://docs.convex.dev/auth/auth0` and the relevant Auth0 - quickstart for the app's framework -2. Ask whether the user wants the Auth0 CLI path -3. If yes, install Auth0 CLI and have the user authenticate it with - `auth0 login` -4. Use `auth0 apps create` with SPA settings, callback URL, logout URL, and web - origins if creating a new app -5. If not using the CLI path, complete the relevant Auth0 frontend quickstart - and create the Auth0 app in the dashboard -6. Get the Auth0 domain and client ID from the CLI output or the Auth0 dashboard -7. Install the Auth0 SDK for the app's framework -8. Create or update `convex/auth.config.ts` with the Auth0 domain and client ID -9. Set frontend and backend environment variables -10. Wrap the app in `Auth0Provider` -11. Replace plain `ConvexProvider` wiring with `ConvexProviderWithAuth0` -12. Run the normal Convex dev or deploy flow after backend config changes -13. Try the official provider config shown in the Convex docs -14. If login works but Convex auth or token refresh fails in a way you cannot - clearly resolve, stop and tell the user to follow the official docs manually - for now -15. Only claim success if the user can sign in and Convex recognizes the - authenticated session -16. If the user wants production-ready setup, configure the production Auth0 - tenant values and production environment variables too - -## Gotchas - -- The Convex docs assume the Auth0 side is already set up, so do not skip the - Auth0 quickstart if the app is starting from scratch -- The Auth0 CLI is often the fastest path for a fresh setup, but it still - requires the user to authenticate the CLI to their Auth0 tenant -- If the user agrees to install the Auth0 CLI, do the mechanical setup yourself - instead of bouncing them through the dashboard -- If login succeeds but Convex still reports unauthenticated, double-check - `convex/auth.config.ts` and whether the backend config was synced -- We were able to automate Auth0 app creation and Convex config wiring, but we - did not fully validate the refresh-token path end to end -- In validation, the documented `useRefreshTokens={true}` and - `cacheLocation="localstorage"` setup hit refresh-token failures, so do not - present that path as settled -- If you hit Auth0 errors like `Unknown or invalid refresh token`, do not keep - inventing fixes indefinitely, send the user back to the official docs and - explain that this path is still under investigation -- Keep dev and prod tenants separate if the project uses different Auth0 - environments -- Do not confuse "Auth0 login works" with "Convex can validate the Auth0 token". - Both need to work. -- If the repo already uses Auth0, preserve existing redirect and tenant - configuration unless the user asked to change it. -- Do not assume the local Auth0 tenant settings match production. Verify the - production domain, client ID, and callback URLs separately. -- For local dev, make sure the Auth0 app settings match the app's real local - port for callback URLs, logout URLs, and web origins - -## Production - -- Ask whether the user wants dev-only setup or production-ready setup -- If the answer is production-ready, make sure the production Auth0 tenant - values, callback URLs, and Convex deployment config are all covered -- Verify production environment variables and redirect settings before calling - the task complete -- Do not silently write a notes file into the repo by default. If the user wants - rollout or handoff docs, create one explicitly. - -## Validation - -- Verify the user can complete the Auth0 login flow -- Verify Convex-authenticated UI renders only after Convex auth state is ready -- Verify protected Convex queries succeed after login -- Verify `ctx.auth.getUserIdentity()` is non-null in protected backend functions -- Verify the Auth0 app settings match the real local callback and logout URLs - during development -- If the Auth0 refresh-token path fails, mark the setup as not fully validated - and direct the user to the official docs instead of claiming the skill - completed successfully -- If production-ready setup was requested, verify the production Auth0 - configuration is also covered - -## Checklist - -- [ ] Confirm the user wants Auth0 -- [ ] Ask whether the user wants local-only setup or production-ready setup -- [ ] Complete the relevant Auth0 frontend setup -- [ ] Configure `convex/auth.config.ts` -- [ ] Set environment variables -- [ ] Verify Convex authenticated state after login, or explicitly tell the user - this path is still under investigation and send them to the official docs -- [ ] If requested, configure the production deployment too diff --git a/packages/backend/.claude/skills/convex-setup-auth/references/clerk.md b/packages/backend/.claude/skills/convex-setup-auth/references/clerk.md deleted file mode 100644 index 20f37dd..0000000 --- a/packages/backend/.claude/skills/convex-setup-auth/references/clerk.md +++ /dev/null @@ -1,141 +0,0 @@ -# Clerk - -Official docs: - -- https://docs.convex.dev/auth/clerk -- https://clerk.com/docs/guides/development/integrations/databases/convex - -Use this when the app already uses Clerk or the user wants Clerk's hosted auth -features. - -## Workflow - -1. Confirm the user wants Clerk -2. Make sure the user has a Clerk account and a Clerk application -3. Determine the app framework: - - React - - Next.js - - TanStack Start -4. Ask whether the user wants local-only setup or production-ready setup now -5. Gather the Clerk keys and the Clerk Frontend API URL -6. Follow the correct framework section in the official docs -7. Complete the backend and client wiring -8. Verify Convex reports the user as authenticated after login -9. If the user wants production-ready setup, make sure the production Clerk - config is also covered - -## What To Do - -- Read the official Convex and Clerk guide before writing setup code -- If the user does not already have Clerk set up, send them to - `https://dashboard.clerk.com/sign-up` to create an account and - `https://dashboard.clerk.com/apps/new` to create an application -- Send the user to `https://dashboard.clerk.com/apps/setup/convex` if the Convex - integration is not already active -- Match the guide to the app's framework, usually React, Next.js, or TanStack - Start -- Use the official examples for `ConvexProviderWithClerk`, `ClerkProvider`, and - `useAuth` - -## Key Setup Areas - -- install the Clerk SDK for the framework in use -- configure `convex/auth.config.ts` with the Clerk issuer domain -- set the required Clerk environment variables -- wrap the app with `ClerkProvider` and `ConvexProviderWithClerk` -- use Convex auth-aware UI patterns such as `Authenticated`, `Unauthenticated`, - and `AuthLoading` - -## Files and Env Vars To Expect - -- `convex/auth.config.ts` -- React or Vite client entry such as `src/main.tsx` -- Next.js client wrapper for Convex if using App Router -- Clerk account sign-up page: `https://dashboard.clerk.com/sign-up` -- Clerk app creation page: `https://dashboard.clerk.com/apps/new` -- Clerk Convex integration page: `https://dashboard.clerk.com/apps/setup/convex` -- Clerk API keys page: `https://dashboard.clerk.com/last-active?path=api-keys` -- Clerk environment variables: - - `CLERK_JWT_ISSUER_DOMAIN` for Convex backend validation in the Convex docs - - `CLERK_FRONTEND_API_URL` in the Clerk docs - - `VITE_CLERK_PUBLISHABLE_KEY` for Vite apps - - `NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY` for Next.js apps - - `CLERK_SECRET_KEY` for Next.js server-side Clerk setup where required - -`CLERK_JWT_ISSUER_DOMAIN` and `CLERK_FRONTEND_API_URL` refer to the same Clerk -Frontend API URL value. Do not treat them as two different URLs. - -## Concrete Steps - -1. If needed, create a Clerk account at `https://dashboard.clerk.com/sign-up` -2. If needed, create a Clerk application at - `https://dashboard.clerk.com/apps/new` -3. Open `https://dashboard.clerk.com/last-active?path=api-keys` and copy the - publishable key, plus the secret key for Next.js where needed -4. Open `https://dashboard.clerk.com/apps/setup/convex` -5. Activate the Convex integration in Clerk if it is not already active -6. Copy the Clerk Frontend API URL shown there -7. Install the Clerk package for the app's framework -8. Create or update `convex/auth.config.ts` so Convex validates Clerk tokens -9. Set the publishable key in the frontend environment -10. Set the issuer domain or Frontend API URL so Convex can validate the JWT -11. Replace plain `ConvexProvider` wiring with `ConvexProviderWithClerk` -12. Wrap the app in `ClerkProvider` -13. Use Convex auth helpers for authenticated rendering -14. Run the normal Convex dev or deploy flow after updating backend auth config -15. If the user wants production-ready setup, configure the production Clerk - values and production issuer domain too - -## Gotchas - -- Prefer `useConvexAuth()` over raw Clerk auth state when deciding whether - Convex-authenticated UI can render -- For Next.js, keep server and client boundaries in mind when creating the - Convex provider wrapper -- After changing `convex/auth.config.ts`, run the normal Convex dev or deploy - flow so the backend picks up the new config -- Do not stop at "Clerk login works". The important check is that Convex also - sees the session and can authenticate requests. -- If the repo already uses Clerk, preserve its existing auth flow unless the - user asked to change it. -- Do not assume the same Clerk values work for both dev and production. Check - the production issuer domain and publishable key separately. -- The Convex setup page is where you get the Clerk Frontend API URL for Convex. - Keep using the Clerk API keys page for the publishable key and the secret key. -- If Convex says no auth provider matched the token, first confirm the Clerk - Convex integration was activated at - `https://dashboard.clerk.com/apps/setup/convex` -- After activating the Clerk Convex integration, sign out completely and sign - back in before retesting. An old Clerk session can keep using a token that - Convex rejects. - -## Production - -- Ask whether the user wants dev-only setup or production-ready setup -- If the answer is production-ready, make sure production Clerk keys and issuer - configuration are included -- Verify production redirect URLs and any production Clerk domain values before - calling the task complete -- Do not silently write a notes file into the repo by default. If the user wants - rollout or handoff docs, create one explicitly. - -## Validation - -- Verify the user can sign in with Clerk -- If the Clerk integration was just activated, verify after a full Clerk - sign-out and fresh sign-in -- Verify `useConvexAuth()` reaches the authenticated state after Clerk login -- Verify protected Convex queries run successfully inside authenticated UI -- Verify `ctx.auth.getUserIdentity()` is non-null in protected backend functions -- If production-ready setup was requested, verify the production Clerk - configuration is also covered - -## Checklist - -- [ ] Confirm the user wants Clerk -- [ ] Ask whether the user wants local-only setup or production-ready setup -- [ ] Follow the correct framework section in the official guide -- [ ] Set Clerk environment variables -- [ ] Configure `convex/auth.config.ts` -- [ ] Verify Convex authenticated state after login -- [ ] If requested, configure the production deployment too diff --git a/packages/backend/.claude/skills/convex-setup-auth/references/convex-auth.md b/packages/backend/.claude/skills/convex-setup-auth/references/convex-auth.md deleted file mode 100644 index 480672e..0000000 --- a/packages/backend/.claude/skills/convex-setup-auth/references/convex-auth.md +++ /dev/null @@ -1,188 +0,0 @@ -# Convex Auth - -Official docs: https://docs.convex.dev/auth/convex-auth Setup guide: -https://labs.convex.dev/auth/setup - -Use this when the user wants auth handled directly in Convex rather than through -a third-party provider. - -## Workflow - -1. Confirm the user wants Convex Auth specifically -2. Determine which sign-in methods the app needs: - - magic links or OTPs - - OAuth providers - - passwords and password reset -3. Ask whether the user wants local-only setup or production-ready setup now -4. Read the Convex Auth setup guide before writing code -5. Make sure the project has a configured Convex deployment: - - run `npx convex dev` first if `CONVEX_DEPLOYMENT` is not set - - if CLI configuration requires interactive human input, stop and ask the - user to complete that step before continuing -6. Install the auth packages: - - `npm install @convex-dev/auth @auth/core@0.37.0` -7. Run the initialization command: - - `npx @convex-dev/auth` -8. Confirm the initializer created: - - `convex/auth.config.ts` - - `convex/auth.ts` - - `convex/http.ts` -9. Add the required `authTables` to `convex/schema.ts` -10. Replace plain `ConvexProvider` wiring with `ConvexAuthProvider` -11. Configure at least one auth method in `convex/auth.ts` -12. Run `npx convex dev --once` or the normal dev flow to push the updated - schema and generated code -13. Verify the client can sign in successfully -14. Verify Convex receives authenticated identity in backend functions -15. If the user wants production-ready setup, make sure the same auth setup is - configured for the production deployment as well -16. Only add a `users` table and `storeUser` flow if the app needs app-level - user records inside Convex - -## What This Reference Is For - -- choosing Convex Auth as the default provider for a new Convex app -- understanding whether the app wants magic links, OTPs, OAuth, or passwords -- keeping the setup provider-specific while using the official Convex Auth docs - for identity and authorization behavior - -## What To Do - -- Read the Convex Auth setup guide before writing setup code -- Follow the setup flow from the docs rather than recreating it from memory -- If the app is new, consider starting from the official starter flow instead of - hand-wiring everything -- Treat `npx @convex-dev/auth` as a required initialization step for existing - apps, not an optional extra - -## Concrete Steps - -1. Install `@convex-dev/auth` and `@auth/core@0.37.0` -2. Run `npx convex dev` if the project does not already have a configured - deployment -3. If `npx convex dev` blocks on interactive setup, ask the user explicitly to - finish configuring the Convex deployment -4. Run `npx @convex-dev/auth` -5. Confirm the generated auth setup is present before continuing: - - `convex/auth.config.ts` - - `convex/auth.ts` - - `convex/http.ts` -6. Add `authTables` to `convex/schema.ts` -7. Replace `ConvexProvider` with `ConvexAuthProvider` in the app entry -8. Configure the selected auth methods in `convex/auth.ts` -9. Run `npx convex dev --once` or the normal dev flow so the updated schema and - auth files are pushed -10. Verify login locally -11. If the user wants production-ready setup, repeat the required auth - configuration against the production deployment - -## Expected Files and Decisions - -- `convex/schema.ts` -- frontend app entry such as `src/main.tsx` or the framework-equivalent provider - file -- generated Convex Auth setup produced by `npx @convex-dev/auth` -- an existing configured Convex deployment, or the ability to create one with - `npx convex dev` -- `convex/auth.ts` starts with `providers: []` until the app configures actual - sign-in methods - -- Decide whether the user is creating a new app or adding auth to an existing - app -- For a new app, prefer the official starter flow instead of rebuilding setup by - hand -- Decide which auth methods the app needs: - - magic links or OTPs - - OAuth providers - - passwords -- Decide whether the user wants local-only setup or production-ready setup now -- Decide whether the app actually needs a `users` table inside Convex, or - whether provider identity alone is enough - -## Gotchas - -- Do not assume a specific sign-in method. Ask which methods the app needs - before wiring UI and backend behavior. -- `npx @convex-dev/auth` is important because it initializes the auth setup, - including the key material. Do not skip it when adding Convex Auth to an - existing project. -- `npx @convex-dev/auth` will fail if the project does not already have a - configured `CONVEX_DEPLOYMENT`. -- `npx convex dev` may require interactive setup for deployment creation or - project selection. If that happens, ask the user explicitly for that human - step instead of guessing. -- `npx @convex-dev/auth` does not finish the whole integration by itself. You - still need to add `authTables`, swap in `ConvexAuthProvider`, and configure at - least one auth method. -- A project can still build even if `convex/auth.ts` still has `providers: []`, - so do not treat a successful build as proof that sign-in is fully configured. -- Convex Auth does not mean every app needs a `users` table. If the app only - needs authentication gates, `ctx.auth.getUserIdentity()` may be enough. -- If the app is greenfield, starting from the official starter flow is usually - better than partially recreating it by hand. -- Do not stop at local dev setup if the user expects production-ready auth. The - production deployment needs the auth setup too. -- Keep provider-specific setup and Convex Auth authorization behavior in the - official docs instead of inventing shared patterns from memory. - -## Production - -- Ask whether the user wants dev-only setup or production-ready setup -- If the answer is production-ready, make sure the auth configuration is applied - to the production deployment, not just the dev deployment -- Verify production-specific redirect URLs, auth method configuration, and - deployment settings before calling the task complete -- Do not silently write a notes file into the repo by default. If the user wants - rollout or handoff docs, create one explicitly. - -## Human Handoff - -If `npx convex dev` or deployment setup requires human input: - -- stop and explain exactly what the user needs to do -- say why that step is required -- resume the auth setup immediately after the user confirms it is done - -## Validation - -- Verify the user can complete a sign-in flow -- Offer to validate sign up, sign out, and sign back in with the configured auth - method -- If browser automation is available in the environment, you can do this - directly -- If browser automation is not available, give the user a short manual - validation checklist instead -- Verify `ctx.auth.getUserIdentity()` returns an identity in protected backend - functions -- Verify protected UI only renders after Convex-authenticated state is ready -- Verify environment variables and redirect settings match the current app - environment -- Verify `convex/auth.ts` no longer has an empty `providers: []` configuration - once the app is meant to support real sign-in -- Run `npx convex dev --once` or the normal dev flow after setup changes and - confirm Convex codegen and push succeed -- If production-ready setup was requested, verify the production deployment is - also configured correctly - -## Checklist - -- [ ] Confirm the user wants Convex Auth specifically -- [ ] Ask whether the user wants local-only setup or production-ready setup -- [ ] Ensure a Convex deployment is configured before running auth - initialization -- [ ] Install `@convex-dev/auth` and `@auth/core@0.37.0` -- [ ] Run `npx convex dev` first if needed -- [ ] Run `npx @convex-dev/auth` -- [ ] Confirm `convex/auth.config.ts`, `convex/auth.ts`, and `convex/http.ts` - were created -- [ ] Follow the setup guide for package install and wiring -- [ ] Add `authTables` to `convex/schema.ts` -- [ ] Replace `ConvexProvider` with `ConvexAuthProvider` -- [ ] Configure at least one auth method in `convex/auth.ts` -- [ ] Run `npx convex dev --once` or the normal dev flow after setup changes -- [ ] Confirm which sign-in methods the app needs -- [ ] Verify the client can sign in and the backend receives authenticated - identity -- [ ] Offer end-to-end validation of sign up, sign out, and sign back in -- [ ] If requested, configure the production deployment too -- [ ] Only add extra `users` table sync if the app needs app-level user records diff --git a/packages/backend/.claude/skills/convex-setup-auth/references/workos-authkit.md b/packages/backend/.claude/skills/convex-setup-auth/references/workos-authkit.md deleted file mode 100644 index 7c031b3..0000000 --- a/packages/backend/.claude/skills/convex-setup-auth/references/workos-authkit.md +++ /dev/null @@ -1,147 +0,0 @@ -# WorkOS AuthKit - -Official docs: - -- https://docs.convex.dev/auth/authkit/ -- https://docs.convex.dev/auth/authkit/add-to-app -- https://docs.convex.dev/auth/authkit/auto-provision - -Use this when the app already uses WorkOS or the user wants AuthKit -specifically. - -## Workflow - -1. Confirm the user wants WorkOS AuthKit -2. Determine whether they want: - - a Convex-managed WorkOS team - - an existing WorkOS team -3. Ask whether the user wants local-only setup or production-ready setup now -4. Read the official Convex and WorkOS AuthKit guide -5. Create or update `convex.json` for the app's framework and real local port -6. Follow the correct branch of the setup flow based on that choice -7. Configure the required WorkOS environment variables -8. Configure `convex/auth.config.ts` for WorkOS-issued JWTs -9. Wire the client provider and callback flow -10. Verify authenticated requests reach Convex -11. If the user wants production-ready setup, make sure the production WorkOS - configuration is covered too -12. Only add `storeUser` or a `users` table if the app needs first-class user - rows inside Convex - -## What To Do - -- Read the official Convex and WorkOS AuthKit guide before writing setup code -- Determine whether the user wants a Convex-managed WorkOS team or an existing - WorkOS team -- Treat `convex.json` as a first-class part of the AuthKit setup, not an - optional extra -- Follow the current setup flow from the docs instead of relying on older - examples - -## Key Setup Areas - -- package installation for the app's framework -- `convex.json` with the `authKit` section for dev, and preview or prod if - needed -- environment variables such as `WORKOS_CLIENT_ID`, `WORKOS_API_KEY`, and - redirect configuration -- `convex/auth.config.ts` wiring for WorkOS-issued JWTs -- client provider setup and token flow into Convex -- login callback and redirect configuration - -## Files and Env Vars To Expect - -- `convex.json` -- `convex/auth.config.ts` -- frontend auth provider wiring -- callback or redirect route setup where the framework requires it -- WorkOS environment variables commonly include: - - `WORKOS_CLIENT_ID` - - `WORKOS_API_KEY` - - `WORKOS_COOKIE_PASSWORD` - - `VITE_WORKOS_CLIENT_ID` - - `VITE_WORKOS_REDIRECT_URI` - - `NEXT_PUBLIC_WORKOS_REDIRECT_URI` - -For a managed WorkOS team, `convex dev` can provision the AuthKit environment -and write local env vars such as `VITE_WORKOS_CLIENT_ID` and -`VITE_WORKOS_REDIRECT_URI` into `.env.local` for Vite apps. - -## Concrete Steps - -1. Choose Convex-managed or existing WorkOS team -2. Create or update `convex.json` with the `authKit` section for the framework - in use -3. Make sure the dev `redirectUris`, `appHomepageUrl`, `corsOrigins`, and local - redirect env vars match the app's actual local port -4. For a managed WorkOS team, run `npx convex dev` and follow the interactive - onboarding flow -5. For an existing WorkOS team, get `WORKOS_CLIENT_ID` and `WORKOS_API_KEY` from - the WorkOS dashboard and set them with `npx convex env set` -6. Create or update `convex/auth.config.ts` for WorkOS JWT validation -7. Run the normal Convex dev or deploy flow so backend config is synced -8. Wire the WorkOS client provider in the app -9. Configure callback and redirect handling -10. Verify the user can sign in and return to the app -11. Verify Convex sees the authenticated user after login -12. If the user wants production-ready setup, configure the production client - ID, API key, redirect URI, and deployment settings too - -## Gotchas - -- The docs split setup between Convex-managed and existing WorkOS teams, so ask - which path the user wants if it is not obvious -- Keep dev and prod WorkOS configuration separate where the docs call for - different client IDs or API keys -- Only add `storeUser` or a `users` table if the app needs first-class user rows - inside Convex -- Do not mix dev and prod WorkOS credentials or redirect URIs -- If the repo already contains WorkOS setup, preserve the current tenant model - unless the user wants to change it -- For managed WorkOS setup, `convex dev` is interactive the first time. In - non-interactive terminals, stop and ask the user to complete the onboarding - prompts. -- `convex.json` is not optional for the managed AuthKit flow. It drives redirect - URI, homepage URL, CORS configuration, and local env var generation. -- If the frontend starts on a different port than the one in `convex.json`, the - hosted WorkOS sign-in flow will point to the wrong callback URL. Update - `convex.json`, update the local redirect env var, and run `npx convex dev` - again. -- Vite can fall off `5173` if other apps are already running. Do not assume the - default port still matches the generated AuthKit config. -- A successful WorkOS sign-in should redirect back to the local callback route - and then reach a Convex-authenticated state. Do not stop at "the hosted WorkOS - page loaded." - -## Production - -- Ask whether the user wants dev-only setup or production-ready setup -- If the answer is production-ready, make sure the production WorkOS client ID, - API key, redirect URI, and Convex deployment config are all covered -- Verify the production redirect and callback settings before calling the task - complete -- Do not silently write a notes file into the repo by default. If the user wants - rollout or handoff docs, create one explicitly. - -## Validation - -- Verify the user can complete the login flow and return to the app -- Verify the callback URL matches the real frontend port in local dev -- Verify Convex receives authenticated requests after login -- Verify `convex.json` matches the framework and chosen WorkOS setup path -- Verify `convex/auth.config.ts` matches the chosen WorkOS setup path -- Verify environment variables differ correctly between local and production - where needed -- If production-ready setup was requested, verify the production WorkOS - configuration is also covered - -## Checklist - -- [ ] Confirm the user wants WorkOS AuthKit -- [ ] Ask whether the user wants local-only setup or production-ready setup -- [ ] Choose Convex-managed or existing WorkOS team -- [ ] Create or update `convex.json` -- [ ] Configure WorkOS environment variables -- [ ] Configure `convex/auth.config.ts` -- [ ] Verify authenticated requests reach Convex after login -- [ ] If requested, configure the production deployment too diff --git a/packages/backend/.claude/skills/convex/SKILL.md b/packages/backend/.claude/skills/convex/SKILL.md deleted file mode 100644 index 08c67f1..0000000 --- a/packages/backend/.claude/skills/convex/SKILL.md +++ /dev/null @@ -1,53 +0,0 @@ ---- -name: convex -description: - Routes general Convex requests to the right project skill. Use when the user - asks which Convex skill to use or gives an underspecified Convex app task. ---- - -# 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