remoge claude ew
This commit is contained in:
@@ -1 +0,0 @@
|
|||||||
../../.agents/skills/flue
|
|
||||||
@@ -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.<name>` 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/<componentName>/`
|
|
||||||
- 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/<name>`
|
|
||||||
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.<name>...` 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/<name>/` (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
|
|
||||||
@@ -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
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" aria-hidden="true" data-slot="icon">
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" d="m21 7.5-2.25-1.313M21 7.5v2.25m0-2.25-2.25 1.313M3 7.5l2.25-1.313M3 7.5l2.25 1.313M3 7.5v2.25m9 3 2.25-1.313M12 12.75l-2.25-1.313M12 12.75V15m0 6.75 2.25-1.313M12 21.75V19.5m0 2.25-2.25-1.313m0-16.875L12 2.25l2.25 1.313M21 14.25v2.25l-2.25 1.313m-13.5 0L3 16.5v-2.25"/>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 485 B |
@@ -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<GenericMutationCtx<GenericDataModel>, "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 });
|
|
||||||
},
|
|
||||||
});
|
|
||||||
```
|
|
||||||
@@ -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
|
|
||||||
@@ -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/
|
|
||||||
<name>/
|
|
||||||
convex.config.ts
|
|
||||||
schema.ts
|
|
||||||
<feature>.ts
|
|
||||||
```
|
|
||||||
|
|
||||||
## Workflow Notes
|
|
||||||
|
|
||||||
- Define the component with `defineComponent("<name>")`
|
|
||||||
- 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/<name>/`
|
|
||||||
- [ ] App installs it with `app.use(...)`
|
|
||||||
- [ ] Component owns only its own tables
|
|
||||||
- [ ] App wrappers handle client-facing calls when needed
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" aria-hidden="true" data-slot="icon">
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" d="M16.023 9.348h4.992v-.001M2.985 19.644v-4.992m0 0h4.992m-4.993 0 3.181 3.183a8.25 8.25 0 0 0 13.803-3.7M4.031 9.865a8.25 8.25 0 0 1 13.803-3.7l3.181 3.182m0-4.991v4.99"/>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 386 B |
@@ -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
|
|
||||||
```
|
|
||||||
@@ -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<DataModel>(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: "<unknown>" }),
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
### 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 }),
|
|
||||||
});
|
|
||||||
```
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" aria-hidden="true" data-slot="icon">
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" d="M8.25 3v1.5M4.5 8.25H3m18 0h-1.5M4.5 12H3m18 0h-1.5m-15 3.75H3m18 0h-1.5M8.25 19.5V21M12 3v1.5m0 15V21m3.75-18v1.5m0 15V21m-9-1.5h10.5a2.25 2.25 0 0 0 2.25-2.25V6.75a2.25 2.25 0 0 0-2.25-2.25H6.75A2.25 2.25 0 0 0 4.5 6.75v10.5a2.25 2.25 0 0 0 2.25 2.25Zm.75-12h9v9h-9v-9Z"/>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 490 B |
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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 <PresenceList users={presence} />;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
```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<Item[]>([]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
client.query(api.items.snapshot).then(setItems);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return <ItemGrid items={items} />;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
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 <Card title={project.name} author={author?.name} />;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
```ts
|
|
||||||
// Good: parent query returns projects with author names included
|
|
||||||
function ProjectList() {
|
|
||||||
const projects = useQuery(api.projects.listWithAuthors);
|
|
||||||
return projects?.map((p) => (
|
|
||||||
<Card key={p._id} title={p.name} author={p.authorName} />
|
|
||||||
));
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
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
|
|
||||||
@@ -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 '<existing dev command>'`. 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 <ConvexProvider client={convex}>...</ConvexProvider>;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Good: created once at module scope
|
|
||||||
const convex = new ConvexReactClient(import.meta.env.VITE_CONVEX_URL as string);
|
|
||||||
function App() {
|
|
||||||
return <ConvexProvider client={convex}>...</ConvexProvider>;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 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(
|
|
||||||
<StrictMode>
|
|
||||||
<ConvexProvider client={convex}>
|
|
||||||
<App />
|
|
||||||
</ConvexProvider>
|
|
||||||
</StrictMode>,
|
|
||||||
);
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 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 <ConvexProvider client={convex}>{children}</ConvexProvider>;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
```tsx
|
|
||||||
// app/layout.tsx
|
|
||||||
import { ConvexClientProvider } from "./ConvexClientProvider";
|
|
||||||
|
|
||||||
export default function RootLayout({
|
|
||||||
children,
|
|
||||||
}: {
|
|
||||||
children: React.ReactNode;
|
|
||||||
}) {
|
|
||||||
return (
|
|
||||||
<html lang="en">
|
|
||||||
<body>
|
|
||||||
<ConvexClientProvider>{children}</ConvexClientProvider>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 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 (
|
|
||||||
<div>
|
|
||||||
<button onClick={() => create({ text: "New task" })}>Add</button>
|
|
||||||
{tasks?.map((t) => (
|
|
||||||
<div key={t._id}>{t.text}</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## 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
|
|
||||||
@@ -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
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" aria-hidden="true" data-slot="icon">
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" d="M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z"/>
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" d="M15.91 11.672a.375.375 0 0 1 0 .656l-5.603 3.113a.375.375 0 0 1-.557-.328V8.887c0-.286.307-.466.557-.327l5.603 3.112Z"/>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 435 B |
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" aria-hidden="true" data-slot="icon">
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" d="M16.5 10.5V6.75a4.5 4.5 0 1 0-9 0v3.75m-.75 11.25h10.5a2.25 2.25 0 0 0 2.25-2.25v-6.75a2.25 2.25 0 0 0-2.25-2.25H6.75a2.25 2.25 0 0 0-2.25 2.25v6.75a2.25 2.25 0 0 0 2.25 2.25Z"/>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 394 B |
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
Reference in New Issue
Block a user