opencastle 0.33.9 → 0.34.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/init.d.ts.map +1 -1
- package/dist/cli/init.js +39 -17
- package/dist/cli/init.js.map +1 -1
- package/dist/cli/stack-config.d.ts.map +1 -1
- package/dist/cli/stack-config.js +5 -0
- package/dist/cli/stack-config.js.map +1 -1
- package/dist/cli/types.d.ts +1 -1
- package/dist/cli/types.d.ts.map +1 -1
- package/dist/orchestrator/plugins/cloudflare/config.d.ts +3 -0
- package/dist/orchestrator/plugins/cloudflare/config.d.ts.map +1 -0
- package/dist/orchestrator/plugins/cloudflare/config.js +23 -0
- package/dist/orchestrator/plugins/cloudflare/config.js.map +1 -0
- package/dist/orchestrator/plugins/coolify/config.d.ts +3 -0
- package/dist/orchestrator/plugins/coolify/config.d.ts.map +1 -0
- package/dist/orchestrator/plugins/coolify/config.js +28 -0
- package/dist/orchestrator/plugins/coolify/config.js.map +1 -0
- package/dist/orchestrator/plugins/drizzle/config.d.ts +3 -0
- package/dist/orchestrator/plugins/drizzle/config.d.ts.map +1 -0
- package/dist/orchestrator/plugins/drizzle/config.js +15 -0
- package/dist/orchestrator/plugins/drizzle/config.js.map +1 -0
- package/dist/orchestrator/plugins/expo/config.d.ts +3 -0
- package/dist/orchestrator/plugins/expo/config.d.ts.map +1 -0
- package/dist/orchestrator/plugins/expo/config.js +23 -0
- package/dist/orchestrator/plugins/expo/config.js.map +1 -0
- package/dist/orchestrator/plugins/index.d.ts.map +1 -1
- package/dist/orchestrator/plugins/index.js +12 -0
- package/dist/orchestrator/plugins/index.js.map +1 -1
- package/dist/orchestrator/plugins/sentry/config.d.ts +3 -0
- package/dist/orchestrator/plugins/sentry/config.d.ts.map +1 -0
- package/dist/orchestrator/plugins/sentry/config.js +28 -0
- package/dist/orchestrator/plugins/sentry/config.js.map +1 -0
- package/dist/orchestrator/plugins/stripe/config.d.ts +3 -0
- package/dist/orchestrator/plugins/stripe/config.d.ts.map +1 -0
- package/dist/orchestrator/plugins/stripe/config.js +42 -0
- package/dist/orchestrator/plugins/stripe/config.js.map +1 -0
- package/dist/orchestrator/plugins/types.d.ts +1 -1
- package/dist/orchestrator/plugins/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/cli/init.ts +43 -22
- package/src/cli/stack-config.ts +5 -0
- package/src/cli/types.ts +1 -1
- package/src/dashboard/dist/data/convoys/demo-api-v2.json +3 -3
- package/src/dashboard/dist/data/convoys/demo-auth-revamp.json +4 -4
- package/src/dashboard/dist/data/convoys/demo-dashboard-ui.json +12 -12
- package/src/dashboard/dist/data/convoys/demo-data-pipeline.json +3 -3
- package/src/dashboard/dist/data/convoys/demo-deploy-ci.json +1 -1
- package/src/dashboard/dist/data/convoys/demo-docs-update.json +3 -3
- package/src/dashboard/dist/data/convoys/demo-perf-opt.json +4 -4
- package/src/dashboard/node_modules/.vite/deps/_metadata.json +6 -6
- package/src/dashboard/public/data/convoys/demo-api-v2.json +3 -3
- package/src/dashboard/public/data/convoys/demo-auth-revamp.json +4 -4
- package/src/dashboard/public/data/convoys/demo-dashboard-ui.json +12 -12
- package/src/dashboard/public/data/convoys/demo-data-pipeline.json +3 -3
- package/src/dashboard/public/data/convoys/demo-deploy-ci.json +1 -1
- package/src/dashboard/public/data/convoys/demo-docs-update.json +3 -3
- package/src/dashboard/public/data/convoys/demo-perf-opt.json +4 -4
- package/src/orchestrator/customizations/agents/skill-matrix.json +24 -4
- package/src/orchestrator/customizations/agents/skill-matrix.md +5 -0
- package/src/orchestrator/plugins/cloudflare/SKILL.md +111 -0
- package/src/orchestrator/plugins/cloudflare/config.ts +24 -0
- package/src/orchestrator/plugins/cloudflare/references/deployment.md +147 -0
- package/src/orchestrator/plugins/cloudflare/references/storage.md +118 -0
- package/src/orchestrator/plugins/cloudflare/references/workers.md +135 -0
- package/src/orchestrator/plugins/convex/SKILL.md +62 -20
- package/src/orchestrator/plugins/convex/references/auth-auth0.md +116 -0
- package/src/orchestrator/plugins/convex/references/auth-clerk.md +113 -0
- package/src/orchestrator/plugins/convex/references/auth-convex-auth.md +143 -0
- package/src/orchestrator/plugins/convex/references/auth-setup.md +87 -0
- package/src/orchestrator/plugins/convex/references/auth-workos.md +114 -0
- package/src/orchestrator/plugins/convex/references/components-advanced.md +134 -0
- package/src/orchestrator/plugins/convex/references/components.md +171 -0
- package/src/orchestrator/plugins/convex/references/function-budget.md +232 -0
- package/src/orchestrator/plugins/convex/references/hot-path-rules.md +371 -0
- package/src/orchestrator/plugins/convex/references/migrations-component.md +170 -0
- package/src/orchestrator/plugins/convex/references/migrations.md +259 -0
- package/src/orchestrator/plugins/convex/references/occ-conflicts.md +126 -0
- package/src/orchestrator/plugins/convex/references/performance-audit.md +80 -0
- package/src/orchestrator/plugins/convex/references/quickstart.md +176 -0
- package/src/orchestrator/plugins/convex/references/subscription-cost.md +252 -0
- package/src/orchestrator/plugins/coolify/SKILL.md +134 -0
- package/src/orchestrator/plugins/coolify/config.ts +29 -0
- package/src/orchestrator/plugins/coolify/references/applications.md +65 -0
- package/src/orchestrator/plugins/coolify/references/ci-cd-webhooks.md +73 -0
- package/src/orchestrator/plugins/coolify/references/databases-services.md +57 -0
- package/src/orchestrator/plugins/coolify/references/docker-compose.md +121 -0
- package/src/orchestrator/plugins/coolify/references/infrastructure.md +77 -0
- package/src/orchestrator/plugins/drizzle/SKILL.md +123 -0
- package/src/orchestrator/plugins/drizzle/config.ts +16 -0
- package/src/orchestrator/plugins/drizzle/references/migrations.md +112 -0
- package/src/orchestrator/plugins/drizzle/references/query-patterns.md +127 -0
- package/src/orchestrator/plugins/drizzle/references/schema-patterns.md +105 -0
- package/src/orchestrator/plugins/expo/SKILL.md +114 -0
- package/src/orchestrator/plugins/expo/config.ts +24 -0
- package/src/orchestrator/plugins/expo/references/eas-build.md +73 -0
- package/src/orchestrator/plugins/expo/references/native-modules.md +71 -0
- package/src/orchestrator/plugins/expo/references/routing.md +83 -0
- package/src/orchestrator/plugins/index.ts +12 -0
- package/src/orchestrator/plugins/linear/SKILL.md +21 -3
- package/src/orchestrator/plugins/sentry/SKILL.md +94 -0
- package/src/orchestrator/plugins/sentry/config.ts +29 -0
- package/src/orchestrator/plugins/sentry/references/error-patterns.md +112 -0
- package/src/orchestrator/plugins/sentry/references/performance.md +66 -0
- package/src/orchestrator/plugins/sentry/references/sdk-setup.md +108 -0
- package/src/orchestrator/plugins/stripe/SKILL.md +138 -0
- package/src/orchestrator/plugins/stripe/config.ts +43 -0
- package/src/orchestrator/plugins/stripe/references/api-patterns.md +57 -0
- package/src/orchestrator/plugins/stripe/references/projects-setup.md +30 -0
- package/src/orchestrator/plugins/stripe/references/upgrade-guide.md +105 -0
- package/src/orchestrator/plugins/types.ts +1 -1
- package/src/orchestrator/skills/backbone-scaffolding/EXAMPLES.md +1 -1
- package/src/orchestrator/skills/backbone-scaffolding/SKILL.md +32 -16
- package/src/orchestrator/plugins/convex/REFERENCE.md +0 -9
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
# Convex Schema & Data Migrations
|
|
2
|
+
|
|
3
|
+
Safe migration of Convex schemas and data when making breaking changes.
|
|
4
|
+
|
|
5
|
+
## Core Constraint
|
|
6
|
+
|
|
7
|
+
Convex will not let you deploy a schema that does not match the data at rest:
|
|
8
|
+
- Cannot add a required field if existing documents don't have it
|
|
9
|
+
- Cannot change a field's type if existing documents have the old type
|
|
10
|
+
- Cannot remove a field from the schema if existing documents still have it
|
|
11
|
+
|
|
12
|
+
## Safe Changes (No Migration Needed)
|
|
13
|
+
|
|
14
|
+
Adding an optional field or a new table requires no migration:
|
|
15
|
+
|
|
16
|
+
```typescript
|
|
17
|
+
// Adding optional field — safe
|
|
18
|
+
users: defineTable({
|
|
19
|
+
name: v.string(),
|
|
20
|
+
bio: v.optional(v.string()), // new optional field
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
// Adding new table — safe
|
|
24
|
+
posts: defineTable({
|
|
25
|
+
userId: v.id("users"),
|
|
26
|
+
title: v.string(),
|
|
27
|
+
}).index("by_user", ["userId"])
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Widen-Migrate-Narrow Workflow
|
|
31
|
+
|
|
32
|
+
Every breaking migration follows this multi-deploy pattern:
|
|
33
|
+
|
|
34
|
+
**Deploy 1 — Widen:**
|
|
35
|
+
1. Update schema to allow both old and new formats (e.g., add optional new field)
|
|
36
|
+
2. Update code to handle both formats when reading
|
|
37
|
+
3. Update code to write the new format for new documents
|
|
38
|
+
4. Deploy
|
|
39
|
+
|
|
40
|
+
**Between deploys — Migrate data:**
|
|
41
|
+
5. Run migration to backfill existing documents
|
|
42
|
+
6. Verify all documents are migrated
|
|
43
|
+
|
|
44
|
+
**Deploy 2 — Narrow:**
|
|
45
|
+
7. Update schema to require the new format only
|
|
46
|
+
8. Remove code that handles the old format
|
|
47
|
+
9. Deploy
|
|
48
|
+
|
|
49
|
+
## Common Patterns
|
|
50
|
+
|
|
51
|
+
### Adding a Required Field
|
|
52
|
+
|
|
53
|
+
```typescript
|
|
54
|
+
// Deploy 1: make optional
|
|
55
|
+
users: defineTable({
|
|
56
|
+
name: v.string(),
|
|
57
|
+
role: v.optional(v.union(v.literal("user"), v.literal("admin"))),
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
// Migration: backfill
|
|
61
|
+
export const addDefaultRole = migrations.define({
|
|
62
|
+
table: "users",
|
|
63
|
+
migrateOne: async (ctx, user) => {
|
|
64
|
+
if (user.role === undefined) {
|
|
65
|
+
await ctx.db.patch(user._id, { role: "user" });
|
|
66
|
+
}
|
|
67
|
+
},
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
// Deploy 2: make required
|
|
71
|
+
users: defineTable({
|
|
72
|
+
name: v.string(),
|
|
73
|
+
role: v.union(v.literal("user"), v.literal("admin")),
|
|
74
|
+
})
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
### Deleting a Field
|
|
78
|
+
|
|
79
|
+
```typescript
|
|
80
|
+
// Deploy 1: Make optional
|
|
81
|
+
// isPro: v.boolean() → isPro: v.optional(v.boolean())
|
|
82
|
+
|
|
83
|
+
// Migration: clear the field
|
|
84
|
+
export const removeIsPro = migrations.define({
|
|
85
|
+
table: "teams",
|
|
86
|
+
migrateOne: async (ctx, team) => {
|
|
87
|
+
if (team.isPro !== undefined) {
|
|
88
|
+
await ctx.db.patch(team._id, { isPro: undefined });
|
|
89
|
+
}
|
|
90
|
+
},
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
// Deploy 2: Remove isPro from schema entirely
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
### Changing a Field Type
|
|
97
|
+
|
|
98
|
+
Create a new field rather than modifying the existing one:
|
|
99
|
+
|
|
100
|
+
```typescript
|
|
101
|
+
// Deploy 1: Add new field, keep old field optional
|
|
102
|
+
// isPro: v.boolean() → isPro: v.optional(v.boolean()), plan: v.optional(...)
|
|
103
|
+
|
|
104
|
+
// Migration: convert old to new
|
|
105
|
+
export const convertToEnum = migrations.define({
|
|
106
|
+
table: "teams",
|
|
107
|
+
migrateOne: async (ctx, team) => {
|
|
108
|
+
if (team.plan === undefined) {
|
|
109
|
+
await ctx.db.patch(team._id, {
|
|
110
|
+
plan: team.isPro ? "pro" : "basic",
|
|
111
|
+
isPro: undefined,
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
},
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
// Deploy 2: Remove isPro, make plan required
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
### Splitting Nested Data Into a Separate Table
|
|
121
|
+
|
|
122
|
+
```typescript
|
|
123
|
+
export const extractPreferences = migrations.define({
|
|
124
|
+
table: "users",
|
|
125
|
+
migrateOne: async (ctx, user) => {
|
|
126
|
+
if (user.preferences === undefined) return;
|
|
127
|
+
const existing = await ctx.db
|
|
128
|
+
.query("userPreferences")
|
|
129
|
+
.withIndex("by_user", (q) => q.eq("userId", user._id))
|
|
130
|
+
.first();
|
|
131
|
+
if (!existing) {
|
|
132
|
+
await ctx.db.insert("userPreferences", {
|
|
133
|
+
userId: user._id,
|
|
134
|
+
...user.preferences,
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
await ctx.db.patch(user._id, { preferences: undefined });
|
|
138
|
+
},
|
|
139
|
+
});
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
## @convex-dev/migrations Component
|
|
143
|
+
|
|
144
|
+
For any non-trivial migration, use this component — it handles batching, cursor-based pagination, state tracking, resume from failure, dry runs, and progress monitoring.
|
|
145
|
+
|
|
146
|
+
See `references/migrations-component.md` for installation, setup, and full API.
|
|
147
|
+
|
|
148
|
+
```bash
|
|
149
|
+
npm install @convex-dev/migrations
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
```typescript
|
|
153
|
+
// convex/convex.config.ts
|
|
154
|
+
import migrations from "@convex-dev/migrations/convex.config.js";
|
|
155
|
+
const app = defineApp();
|
|
156
|
+
app.use(migrations);
|
|
157
|
+
|
|
158
|
+
// convex/migrations.ts
|
|
159
|
+
import { Migrations } from "@convex-dev/migrations";
|
|
160
|
+
export const migrations = new Migrations<DataModel>(components.migrations);
|
|
161
|
+
export const run = migrations.runner();
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
Run from CLI:
|
|
165
|
+
|
|
166
|
+
```bash
|
|
167
|
+
npx convex run migrations:run '{"fn": "migrations:addDefaultRole"}'
|
|
168
|
+
# Dry run first:
|
|
169
|
+
npx convex run migrations:runIt '{"dryRun": true}'
|
|
170
|
+
# Check status:
|
|
171
|
+
npx convex run --component migrations lib:getStatus --watch
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
### Small Table Shortcut
|
|
175
|
+
|
|
176
|
+
For tables with only a few thousand documents, use a single `internalMutation` without the component:
|
|
177
|
+
|
|
178
|
+
```typescript
|
|
179
|
+
export const backfillSmallTable = internalMutation({
|
|
180
|
+
handler: async (ctx) => {
|
|
181
|
+
const docs = await ctx.db.query("smallConfig").collect();
|
|
182
|
+
for (const doc of docs) {
|
|
183
|
+
if (doc.newField === undefined) {
|
|
184
|
+
await ctx.db.patch(doc._id, { newField: "default" });
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
},
|
|
188
|
+
});
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
## Zero-Downtime Strategies
|
|
192
|
+
|
|
193
|
+
### Dual Write (Preferred)
|
|
194
|
+
|
|
195
|
+
Write to both old and new structures. Read from old until migration is complete. Safe to roll back at any point.
|
|
196
|
+
|
|
197
|
+
```typescript
|
|
198
|
+
// Good: writing both formats during migration
|
|
199
|
+
export const createTeam = mutation({
|
|
200
|
+
handler: async (ctx, args) => {
|
|
201
|
+
const plan = args.isPro ? "pro" : "basic";
|
|
202
|
+
await ctx.db.insert("teams", {
|
|
203
|
+
name: args.name,
|
|
204
|
+
isPro: args.isPro, // old format
|
|
205
|
+
plan, // new format
|
|
206
|
+
});
|
|
207
|
+
},
|
|
208
|
+
});
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
### Dual Read
|
|
212
|
+
|
|
213
|
+
Read both formats (preferring new), write only the new format:
|
|
214
|
+
|
|
215
|
+
```typescript
|
|
216
|
+
function getTeamPlan(team: Doc<"teams">): "basic" | "pro" {
|
|
217
|
+
if (team.plan !== undefined) return team.plan;
|
|
218
|
+
return team.isPro ? "pro" : "basic";
|
|
219
|
+
}
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
## Verification
|
|
223
|
+
|
|
224
|
+
Query to check remaining unmigrated documents:
|
|
225
|
+
|
|
226
|
+
```typescript
|
|
227
|
+
export const verifyMigration = query({
|
|
228
|
+
handler: async (ctx) => {
|
|
229
|
+
const remaining = await ctx.db
|
|
230
|
+
.query("users")
|
|
231
|
+
.filter((q) => q.eq(q.field("role"), undefined))
|
|
232
|
+
.take(10);
|
|
233
|
+
return { complete: remaining.length === 0 };
|
|
234
|
+
},
|
|
235
|
+
});
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
## Common Pitfalls
|
|
239
|
+
|
|
240
|
+
1. **Making a field required before migrating data** — deploy rejects because documents lack the field
|
|
241
|
+
2. **Using `.collect()` on large tables** — hits transaction limits; use the migrations component
|
|
242
|
+
3. **Not writing the new format before migrating** — creates missed documents during migration window
|
|
243
|
+
4. **Skipping the dry run** — use `dryRun: true` to validate before touching production data
|
|
244
|
+
5. **Deleting fields prematurely** — prefer `v.optional` with a deprecation comment
|
|
245
|
+
|
|
246
|
+
## Migration Checklist
|
|
247
|
+
|
|
248
|
+
- [ ] Identified the breaking change and planned the multi-deploy workflow
|
|
249
|
+
- [ ] Widened schema to allow both old and new formats
|
|
250
|
+
- [ ] Updated code to handle both formats when reading
|
|
251
|
+
- [ ] Updated code to write the new format for new documents
|
|
252
|
+
- [ ] Deployed widened schema
|
|
253
|
+
- [ ] Defined migration using `@convex-dev/migrations`
|
|
254
|
+
- [ ] Tested with `dryRun: true`
|
|
255
|
+
- [ ] Ran migration and monitored status
|
|
256
|
+
- [ ] Verified all documents are migrated
|
|
257
|
+
- [ ] Narrowed schema to require new format only
|
|
258
|
+
- [ ] Cleaned up code handling old format
|
|
259
|
+
- [ ] Deployed final schema
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
# OCC Conflict Resolution
|
|
2
|
+
|
|
3
|
+
Use these rules when insights, logs, or dashboard health show OCC (Optimistic Concurrency Control) conflicts, mutation retries, or write contention on hot tables.
|
|
4
|
+
|
|
5
|
+
## Core Principle
|
|
6
|
+
|
|
7
|
+
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.
|
|
8
|
+
|
|
9
|
+
## Symptoms
|
|
10
|
+
|
|
11
|
+
- OCC conflict errors in deployment logs or health page
|
|
12
|
+
- Mutations retrying multiple times before succeeding
|
|
13
|
+
- User-visible latency spikes on write-heavy pages
|
|
14
|
+
- `npx convex insights --details` showing high conflict rates
|
|
15
|
+
|
|
16
|
+
## Common Causes
|
|
17
|
+
|
|
18
|
+
### Hot documents
|
|
19
|
+
|
|
20
|
+
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.
|
|
21
|
+
|
|
22
|
+
### Broad read sets causing false conflicts
|
|
23
|
+
|
|
24
|
+
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.
|
|
25
|
+
|
|
26
|
+
### Fan-out from triggers or cascading writes
|
|
27
|
+
|
|
28
|
+
A single user action triggers multiple mutations that all touch related documents. Each mutation competes with the others.
|
|
29
|
+
|
|
30
|
+
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.
|
|
31
|
+
|
|
32
|
+
### Write-then-read chains
|
|
33
|
+
|
|
34
|
+
A mutation writes a document, then a reactive query re-reads it, then another mutation writes it again. Under load, these chains stack up.
|
|
35
|
+
|
|
36
|
+
## Fix Order
|
|
37
|
+
|
|
38
|
+
### 1. Reduce read set size
|
|
39
|
+
|
|
40
|
+
Narrower reads mean fewer false conflicts.
|
|
41
|
+
|
|
42
|
+
```ts
|
|
43
|
+
// Bad: broad scan creates a wide conflict surface
|
|
44
|
+
const allTasks = await ctx.db.query("tasks").collect();
|
|
45
|
+
const mine = allTasks.filter((t) => t.ownerId === userId);
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
```ts
|
|
49
|
+
// Good: indexed query touches only relevant documents
|
|
50
|
+
const mine = await ctx.db
|
|
51
|
+
.query("tasks")
|
|
52
|
+
.withIndex("by_owner", (q) => q.eq("ownerId", userId))
|
|
53
|
+
.collect();
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
### 2. Split hot documents
|
|
57
|
+
|
|
58
|
+
When many writers target the same document, split the contention point.
|
|
59
|
+
|
|
60
|
+
```ts
|
|
61
|
+
// Bad: every vote increments the same counter document
|
|
62
|
+
const counter = await ctx.db.get(pollCounterId);
|
|
63
|
+
await ctx.db.patch(pollCounterId, { count: counter!.count + 1 });
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
```ts
|
|
67
|
+
// Good: shard the counter across multiple documents, aggregate on read
|
|
68
|
+
const shardIndex = Math.floor(Math.random() * SHARD_COUNT);
|
|
69
|
+
const shardId = shardIds[shardIndex];
|
|
70
|
+
const shard = await ctx.db.get(shardId);
|
|
71
|
+
await ctx.db.patch(shardId, { count: shard!.count + 1 });
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Aggregate the shards in a query or scheduled job when you need the total.
|
|
75
|
+
|
|
76
|
+
### 3. Skip no-op writes
|
|
77
|
+
|
|
78
|
+
Writes that do not change data still participate in conflict detection and trigger invalidation.
|
|
79
|
+
|
|
80
|
+
```ts
|
|
81
|
+
// Bad: patches even when nothing changed
|
|
82
|
+
await ctx.db.patch(doc._id, { status: args.status });
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
```ts
|
|
86
|
+
// Good: only write when the value actually differs
|
|
87
|
+
if (doc.status !== args.status) {
|
|
88
|
+
await ctx.db.patch(doc._id, { status: args.status });
|
|
89
|
+
}
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
### 4. Move non-critical work to scheduled functions
|
|
93
|
+
|
|
94
|
+
If a mutation does primary work plus secondary bookkeeping (analytics, notifications, cache warming), the bookkeeping extends the transaction's lifetime and read/write set.
|
|
95
|
+
|
|
96
|
+
```ts
|
|
97
|
+
// Bad: analytics update in the same transaction as the user action
|
|
98
|
+
await ctx.db.patch(userId, { lastActiveAt: Date.now() });
|
|
99
|
+
await ctx.db.insert("analytics", { event: "action", userId, ts: Date.now() });
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
```ts
|
|
103
|
+
// Good: schedule the bookkeeping so the primary transaction is smaller
|
|
104
|
+
await ctx.db.patch(userId, { lastActiveAt: Date.now() });
|
|
105
|
+
await ctx.scheduler.runAfter(0, internal.analytics.recordEvent, {
|
|
106
|
+
event: "action",
|
|
107
|
+
userId,
|
|
108
|
+
});
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
### 5. Combine competing writes
|
|
112
|
+
|
|
113
|
+
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.
|
|
114
|
+
|
|
115
|
+
Do not introduce artificial locks or queues unless the above steps have been tried first.
|
|
116
|
+
|
|
117
|
+
## Related: Invalidation Scope
|
|
118
|
+
|
|
119
|
+
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.
|
|
120
|
+
|
|
121
|
+
## Verification
|
|
122
|
+
|
|
123
|
+
1. OCC conflict rate has dropped in insights or dashboard
|
|
124
|
+
2. Mutation latency is lower and more consistent
|
|
125
|
+
3. No data correctness regressions from splitting or scheduling changes
|
|
126
|
+
4. Sibling writers to the same hot documents were fixed consistently
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# Convex Performance Audit
|
|
2
|
+
|
|
3
|
+
Diagnose and fix performance problems in Convex applications, one problem class at a time.
|
|
4
|
+
|
|
5
|
+
## First Step: Gather Signals
|
|
6
|
+
|
|
7
|
+
1. Run `npx convex insights --details` (use `--prod`, `--preview-name`, or `--deployment-name` as needed)
|
|
8
|
+
2. If CLI is too old: `npx -y convex@latest insights --details`
|
|
9
|
+
3. If runtime signals unavailable, audit from code — but keep guardrails: don't recommend structural work without a measured signal
|
|
10
|
+
|
|
11
|
+
## Signal Routing
|
|
12
|
+
|
|
13
|
+
After gathering signals, identify the problem class and read the matching reference file:
|
|
14
|
+
|
|
15
|
+
| Signal | Reference |
|
|
16
|
+
|--------|-----------|
|
|
17
|
+
| High bytes or documents read, JS filtering, unnecessary joins | `references/hot-path-rules.md` |
|
|
18
|
+
| OCC conflict errors, write contention, mutation retries | `references/occ-conflicts.md` |
|
|
19
|
+
| High subscription count, slow UI updates, excessive re-renders | `references/subscription-cost.md` |
|
|
20
|
+
| Function timeouts, transaction size errors, large payloads | `references/function-budget.md` |
|
|
21
|
+
| General "it's slow" with no specific signal | Start with `references/hot-path-rules.md` |
|
|
22
|
+
|
|
23
|
+
Multiple problem classes can overlap. Read the most relevant reference first.
|
|
24
|
+
|
|
25
|
+
## Workflow
|
|
26
|
+
|
|
27
|
+
### 1. Scope the problem
|
|
28
|
+
|
|
29
|
+
Pick one concrete user flow. Write down:
|
|
30
|
+
- Entrypoint functions
|
|
31
|
+
- Client callsites (`useQuery`, `usePaginatedQuery`, `useMutation`)
|
|
32
|
+
- Tables read and tables written
|
|
33
|
+
- Whether the path is high-read, high-write, or both
|
|
34
|
+
|
|
35
|
+
### 2. Trace the full read and write set
|
|
36
|
+
|
|
37
|
+
For each function:
|
|
38
|
+
1. Trace every `ctx.db.get()` and `ctx.db.query()`
|
|
39
|
+
2. Trace every `ctx.db.patch()`, `ctx.db.replace()`, `ctx.db.insert()`
|
|
40
|
+
3. Note foreign-key lookups, JS-side filtering, and full-document reads
|
|
41
|
+
4. Identify sibling functions touching the same tables
|
|
42
|
+
5. Identify reactive stats/aggregates on the same page
|
|
43
|
+
|
|
44
|
+
### 3. Apply fixes from the relevant reference
|
|
45
|
+
|
|
46
|
+
Read the reference file matching your problem class. Each reference includes specific patterns and a recommended fix order.
|
|
47
|
+
|
|
48
|
+
Do not stop at the single function named by an insight. Trace sibling readers and writers touching the same tables.
|
|
49
|
+
|
|
50
|
+
### 4. Fix sibling functions together
|
|
51
|
+
|
|
52
|
+
When one function has a bug, audit sibling functions for the same pattern. If one list query switches to a digest table, inspect the other list queries for that table. If one mutation needs no-op write protection, inspect all other writers to the same table.
|
|
53
|
+
|
|
54
|
+
### 5. Escalate invasive fixes
|
|
55
|
+
|
|
56
|
+
If the fix is invasive, cross-cutting, or migration-heavy:
|
|
57
|
+
- Introducing digest or summary tables across multiple flows
|
|
58
|
+
- Splitting documents to isolate frequently-updated fields
|
|
59
|
+
- Reworking pagination strategy across several screens
|
|
60
|
+
- Switching to a new index that needs migration-safe rollout
|
|
61
|
+
|
|
62
|
+
Stop and present options before editing. Consult `references/migrations.md` when correctness depends on handling old and new states during rollout.
|
|
63
|
+
|
|
64
|
+
### 6. Verify
|
|
65
|
+
|
|
66
|
+
Confirm:
|
|
67
|
+
1. Results are the same as before — no dropped records
|
|
68
|
+
2. Eliminated reads/writes are no longer in the hot path
|
|
69
|
+
3. Fallback behavior works when denormalized/indexed fields are missing
|
|
70
|
+
4. No unnecessary invalidation when data is unchanged
|
|
71
|
+
5. Every relevant sibling reader and writer was inspected
|
|
72
|
+
|
|
73
|
+
## Reference Files
|
|
74
|
+
|
|
75
|
+
- `references/hot-path-rules.md` — Read amplification, invalidation, denormalization, indexes, digest tables
|
|
76
|
+
- `references/occ-conflicts.md` — Write contention, OCC resolution, hot document splitting
|
|
77
|
+
- `references/subscription-cost.md` — Reactive query cost, subscription granularity, point-in-time reads
|
|
78
|
+
- `references/function-budget.md` — Execution limits, transaction size, large documents, payload size
|
|
79
|
+
|
|
80
|
+
Also see [Convex Best Practices](https://docs.convex.dev/understanding/best-practices/).
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
# Convex Quickstart
|
|
2
|
+
|
|
3
|
+
Set up a working Convex project as fast as possible.
|
|
4
|
+
|
|
5
|
+
## Scaffolding Templates
|
|
6
|
+
|
|
7
|
+
Use `npm create convex@latest` for new projects. Pass project name and template to avoid interactive prompts:
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm create convex@latest my-app -- -t react-vite-shadcn
|
|
11
|
+
cd my-app
|
|
12
|
+
npm install
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
| Template | Stack |
|
|
16
|
+
|----------|-------|
|
|
17
|
+
| `react-vite-shadcn` | React + Vite + Tailwind + shadcn/ui |
|
|
18
|
+
| `nextjs-shadcn` | Next.js App Router + Tailwind + shadcn/ui |
|
|
19
|
+
| `react-vite-clerk-shadcn` | React + Vite + Clerk auth + shadcn/ui |
|
|
20
|
+
| `nextjs-clerk` | Next.js + Clerk auth |
|
|
21
|
+
| `nextjs-convexauth-shadcn` | Next.js + Convex Auth + shadcn/ui |
|
|
22
|
+
| `bare` | Convex backend only, no frontend |
|
|
23
|
+
|
|
24
|
+
Default: `react-vite-shadcn` for simple apps, `nextjs-shadcn` for apps needing SSR or API routes.
|
|
25
|
+
|
|
26
|
+
You can use any GitHub repo as a template:
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
npm create convex@latest my-app -- -t owner/repo
|
|
30
|
+
npm create convex@latest my-app -- -t owner/repo#branch
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Adding Convex to an Existing App
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
npm install convex
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Then ask the user to run `npx convex dev` in their terminal.
|
|
40
|
+
|
|
41
|
+
## ConvexProvider Wiring
|
|
42
|
+
|
|
43
|
+
Create the `ConvexReactClient` at module scope, not inside a component:
|
|
44
|
+
|
|
45
|
+
```tsx
|
|
46
|
+
// Bad: re-creates the client on every render
|
|
47
|
+
function App() {
|
|
48
|
+
const convex = new ConvexReactClient(import.meta.env.VITE_CONVEX_URL as string);
|
|
49
|
+
return <ConvexProvider client={convex}>...</ConvexProvider>;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Good: created once at module scope
|
|
53
|
+
const convex = new ConvexReactClient(import.meta.env.VITE_CONVEX_URL as string);
|
|
54
|
+
function App() {
|
|
55
|
+
return <ConvexProvider client={convex}>...</ConvexProvider>;
|
|
56
|
+
}
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
#### React (Vite)
|
|
60
|
+
|
|
61
|
+
```tsx
|
|
62
|
+
// src/main.tsx
|
|
63
|
+
import { ConvexProvider, ConvexReactClient } from "convex/react";
|
|
64
|
+
|
|
65
|
+
const convex = new ConvexReactClient(import.meta.env.VITE_CONVEX_URL as string);
|
|
66
|
+
|
|
67
|
+
createRoot(document.getElementById("root")!).render(
|
|
68
|
+
<StrictMode>
|
|
69
|
+
<ConvexProvider client={convex}>
|
|
70
|
+
<App />
|
|
71
|
+
</ConvexProvider>
|
|
72
|
+
</StrictMode>,
|
|
73
|
+
);
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
#### Next.js (App Router)
|
|
77
|
+
|
|
78
|
+
```tsx
|
|
79
|
+
// app/ConvexClientProvider.tsx
|
|
80
|
+
"use client";
|
|
81
|
+
import { ConvexProvider, ConvexReactClient } from "convex/react";
|
|
82
|
+
|
|
83
|
+
const convex = new ConvexReactClient(process.env.NEXT_PUBLIC_CONVEX_URL!);
|
|
84
|
+
|
|
85
|
+
export function ConvexClientProvider({ children }: { children: ReactNode }) {
|
|
86
|
+
return <ConvexProvider client={convex}>{children}</ConvexProvider>;
|
|
87
|
+
}
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
## Environment Variables
|
|
91
|
+
|
|
92
|
+
| Framework | Variable |
|
|
93
|
+
|-----------|----------|
|
|
94
|
+
| Vite | `VITE_CONVEX_URL` |
|
|
95
|
+
| Next.js | `NEXT_PUBLIC_CONVEX_URL` |
|
|
96
|
+
| Remix | `CONVEX_URL` |
|
|
97
|
+
| React Native | `EXPO_PUBLIC_CONVEX_URL` |
|
|
98
|
+
|
|
99
|
+
`npx convex dev` writes the correct variable to `.env.local` automatically.
|
|
100
|
+
|
|
101
|
+
## Agent Mode (Cloud/Headless)
|
|
102
|
+
|
|
103
|
+
Set `CONVEX_AGENT_MODE=anonymous` in `.env.local` to run a local anonymous backend without interactive browser login:
|
|
104
|
+
|
|
105
|
+
```bash
|
|
106
|
+
CONVEX_AGENT_MODE=anonymous npx convex dev
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
## The Dev Loop
|
|
110
|
+
|
|
111
|
+
`npx convex dev` is a long-running watcher — it's interactive on first run (browser-based OAuth). **Ask the user to run this themselves.** Once running it:
|
|
112
|
+
- Creates a Convex project and dev deployment
|
|
113
|
+
- Writes the deployment URL to `.env.local`
|
|
114
|
+
- Creates `convex/_generated/` with types
|
|
115
|
+
- Watches for changes and syncs continuously
|
|
116
|
+
|
|
117
|
+
Deploy to production separately:
|
|
118
|
+
|
|
119
|
+
```bash
|
|
120
|
+
npx convex deploy
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
## First Function: Verification Round-Trip
|
|
124
|
+
|
|
125
|
+
`convex/schema.ts`:
|
|
126
|
+
|
|
127
|
+
```ts
|
|
128
|
+
import { defineSchema, defineTable } from "convex/server";
|
|
129
|
+
import { v } from "convex/values";
|
|
130
|
+
|
|
131
|
+
export default defineSchema({
|
|
132
|
+
tasks: defineTable({
|
|
133
|
+
text: v.string(),
|
|
134
|
+
completed: v.boolean(),
|
|
135
|
+
}),
|
|
136
|
+
});
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
`convex/tasks.ts`:
|
|
140
|
+
|
|
141
|
+
```ts
|
|
142
|
+
import { query, mutation } from "./_generated/server";
|
|
143
|
+
import { v } from "convex/values";
|
|
144
|
+
|
|
145
|
+
export const list = query({
|
|
146
|
+
args: {},
|
|
147
|
+
handler: async (ctx) => {
|
|
148
|
+
return await ctx.db.query("tasks").collect();
|
|
149
|
+
},
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
export const create = mutation({
|
|
153
|
+
args: { text: v.string() },
|
|
154
|
+
returns: v.null(),
|
|
155
|
+
handler: async (ctx, args) => {
|
|
156
|
+
await ctx.db.insert("tasks", { text: args.text, completed: false });
|
|
157
|
+
return null;
|
|
158
|
+
},
|
|
159
|
+
});
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
## Other Frameworks
|
|
163
|
+
|
|
164
|
+
- [Vue](https://docs.convex.dev/quickstart/vue)
|
|
165
|
+
- [Svelte](https://docs.convex.dev/quickstart/svelte)
|
|
166
|
+
- [React Native](https://docs.convex.dev/quickstart/react-native)
|
|
167
|
+
- [TanStack Start](https://docs.convex.dev/quickstart/tanstack-start)
|
|
168
|
+
- [Remix](https://docs.convex.dev/quickstart/remix)
|
|
169
|
+
- [Node.js](https://docs.convex.dev/quickstart/nodejs)
|
|
170
|
+
|
|
171
|
+
## Next Steps
|
|
172
|
+
|
|
173
|
+
- Add authentication → `references/auth-setup.md`
|
|
174
|
+
- Schema migrations → `references/migrations.md`
|
|
175
|
+
- Performance optimization → `references/performance-audit.md`
|
|
176
|
+
- Component creation → `references/components.md`
|