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,114 @@
|
|
|
1
|
+
# WorkOS AuthKit
|
|
2
|
+
|
|
3
|
+
Official docs:
|
|
4
|
+
|
|
5
|
+
- https://docs.convex.dev/auth/authkit/
|
|
6
|
+
- https://docs.convex.dev/auth/authkit/add-to-app
|
|
7
|
+
- https://docs.convex.dev/auth/authkit/auto-provision
|
|
8
|
+
|
|
9
|
+
Use this when the app already uses WorkOS or the user wants AuthKit specifically.
|
|
10
|
+
|
|
11
|
+
## Workflow
|
|
12
|
+
|
|
13
|
+
1. Confirm the user wants WorkOS AuthKit
|
|
14
|
+
2. Determine whether they want:
|
|
15
|
+
- a Convex-managed WorkOS team
|
|
16
|
+
- an existing WorkOS team
|
|
17
|
+
3. Ask whether the user wants local-only setup or production-ready setup now
|
|
18
|
+
4. Read the official Convex and WorkOS AuthKit guide
|
|
19
|
+
5. Create or update `convex.json` for the app's framework and real local port
|
|
20
|
+
6. Follow the correct branch of the setup flow based on that choice
|
|
21
|
+
7. Configure the required WorkOS environment variables
|
|
22
|
+
8. Configure `convex/auth.config.ts` for WorkOS-issued JWTs
|
|
23
|
+
9. Wire the client provider and callback flow
|
|
24
|
+
10. Verify authenticated requests reach Convex
|
|
25
|
+
11. If the user wants production-ready setup, make sure the production WorkOS configuration is covered too
|
|
26
|
+
12. Only add `storeUser` or a `users` table if the app needs first-class user rows inside Convex
|
|
27
|
+
|
|
28
|
+
## What To Do
|
|
29
|
+
|
|
30
|
+
- Read the official Convex and WorkOS AuthKit guide before writing setup code
|
|
31
|
+
- Determine whether the user wants a Convex-managed WorkOS team or an existing WorkOS team
|
|
32
|
+
- Treat `convex.json` as a first-class part of the AuthKit setup, not an optional extra
|
|
33
|
+
- Follow the current setup flow from the docs instead of relying on older examples
|
|
34
|
+
|
|
35
|
+
## Key Setup Areas
|
|
36
|
+
|
|
37
|
+
- package installation for the app's framework
|
|
38
|
+
- `convex.json` with the `authKit` section for dev, and preview or prod if needed
|
|
39
|
+
- environment variables such as `WORKOS_CLIENT_ID`, `WORKOS_API_KEY`, and redirect configuration
|
|
40
|
+
- `convex/auth.config.ts` wiring for WorkOS-issued JWTs
|
|
41
|
+
- client provider setup and token flow into Convex
|
|
42
|
+
- login callback and redirect configuration
|
|
43
|
+
|
|
44
|
+
## Files and Env Vars To Expect
|
|
45
|
+
|
|
46
|
+
- `convex.json`
|
|
47
|
+
- `convex/auth.config.ts`
|
|
48
|
+
- frontend auth provider wiring
|
|
49
|
+
- callback or redirect route setup where the framework requires it
|
|
50
|
+
- WorkOS environment variables commonly include:
|
|
51
|
+
- `WORKOS_CLIENT_ID`
|
|
52
|
+
- `WORKOS_API_KEY`
|
|
53
|
+
- `WORKOS_COOKIE_PASSWORD`
|
|
54
|
+
- `VITE_WORKOS_CLIENT_ID`
|
|
55
|
+
- `VITE_WORKOS_REDIRECT_URI`
|
|
56
|
+
- `NEXT_PUBLIC_WORKOS_REDIRECT_URI`
|
|
57
|
+
|
|
58
|
+
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.
|
|
59
|
+
|
|
60
|
+
## Concrete Steps
|
|
61
|
+
|
|
62
|
+
1. Choose Convex-managed or existing WorkOS team
|
|
63
|
+
2. Create or update `convex.json` with the `authKit` section for the framework in use
|
|
64
|
+
3. Make sure the dev `redirectUris`, `appHomepageUrl`, `corsOrigins`, and local redirect env vars match the app's actual local port
|
|
65
|
+
4. For a managed WorkOS team, run `npx convex dev` and follow the interactive onboarding flow
|
|
66
|
+
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`
|
|
67
|
+
6. Create or update `convex/auth.config.ts` for WorkOS JWT validation
|
|
68
|
+
7. Run the normal Convex dev or deploy flow so backend config is synced
|
|
69
|
+
8. Wire the WorkOS client provider in the app
|
|
70
|
+
9. Configure callback and redirect handling
|
|
71
|
+
10. Verify the user can sign in and return to the app
|
|
72
|
+
11. Verify Convex sees the authenticated user after login
|
|
73
|
+
12. If the user wants production-ready setup, configure the production client ID, API key, redirect URI, and deployment settings too
|
|
74
|
+
|
|
75
|
+
## Gotchas
|
|
76
|
+
|
|
77
|
+
- The docs split setup between Convex-managed and existing WorkOS teams, so ask which path the user wants if it is not obvious
|
|
78
|
+
- Keep dev and prod WorkOS configuration separate where the docs call for different client IDs or API keys
|
|
79
|
+
- Only add `storeUser` or a `users` table if the app needs first-class user rows inside Convex
|
|
80
|
+
- Do not mix dev and prod WorkOS credentials or redirect URIs
|
|
81
|
+
- If the repo already contains WorkOS setup, preserve the current tenant model unless the user wants to change it
|
|
82
|
+
- 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.
|
|
83
|
+
- `convex.json` is not optional for the managed AuthKit flow. It drives redirect URI, homepage URL, CORS configuration, and local env var generation.
|
|
84
|
+
- 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.
|
|
85
|
+
- Vite can fall off `5173` if other apps are already running. Do not assume the default port still matches the generated AuthKit config.
|
|
86
|
+
- 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."
|
|
87
|
+
|
|
88
|
+
## Production
|
|
89
|
+
|
|
90
|
+
- Ask whether the user wants dev-only setup or production-ready setup
|
|
91
|
+
- If the answer is production-ready, make sure the production WorkOS client ID, API key, redirect URI, and Convex deployment config are all covered
|
|
92
|
+
- Verify the production redirect and callback settings before calling the task complete
|
|
93
|
+
- Do not silently write a notes file into the repo by default. If the user wants rollout or handoff docs, create one explicitly.
|
|
94
|
+
|
|
95
|
+
## Validation
|
|
96
|
+
|
|
97
|
+
- Verify the user can complete the login flow and return to the app
|
|
98
|
+
- Verify the callback URL matches the real frontend port in local dev
|
|
99
|
+
- Verify Convex receives authenticated requests after login
|
|
100
|
+
- Verify `convex.json` matches the framework and chosen WorkOS setup path
|
|
101
|
+
- Verify `convex/auth.config.ts` matches the chosen WorkOS setup path
|
|
102
|
+
- Verify environment variables differ correctly between local and production where needed
|
|
103
|
+
- If production-ready setup was requested, verify the production WorkOS configuration is also covered
|
|
104
|
+
|
|
105
|
+
## Checklist
|
|
106
|
+
|
|
107
|
+
- [ ] Confirm the user wants WorkOS AuthKit
|
|
108
|
+
- [ ] Ask whether the user wants local-only setup or production-ready setup
|
|
109
|
+
- [ ] Choose Convex-managed or existing WorkOS team
|
|
110
|
+
- [ ] Create or update `convex.json`
|
|
111
|
+
- [ ] Configure WorkOS environment variables
|
|
112
|
+
- [ ] Configure `convex/auth.config.ts`
|
|
113
|
+
- [ ] Verify authenticated requests reach Convex after login
|
|
114
|
+
- [ ] If requested, configure the production deployment too
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
# Advanced Component Patterns
|
|
2
|
+
|
|
3
|
+
Additional patterns for Convex components that go beyond the basics covered in the main skill file.
|
|
4
|
+
|
|
5
|
+
## Function Handles for callbacks
|
|
6
|
+
|
|
7
|
+
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.
|
|
8
|
+
|
|
9
|
+
```ts
|
|
10
|
+
// App side: create a handle and pass it to the component
|
|
11
|
+
import { createFunctionHandle } from "convex/server";
|
|
12
|
+
|
|
13
|
+
export const startJob = mutation({
|
|
14
|
+
handler: async (ctx) => {
|
|
15
|
+
const handle = await createFunctionHandle(internal.myModule.processItem);
|
|
16
|
+
await ctx.runMutation(components.workpool.enqueue, {
|
|
17
|
+
callback: handle,
|
|
18
|
+
});
|
|
19
|
+
},
|
|
20
|
+
});
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
```ts
|
|
24
|
+
// Component side: accept and invoke the handle
|
|
25
|
+
import { v } from "convex/values";
|
|
26
|
+
import type { FunctionHandle } from "convex/server";
|
|
27
|
+
import { mutation } from "./_generated/server.js";
|
|
28
|
+
|
|
29
|
+
export const enqueue = mutation({
|
|
30
|
+
args: { callback: v.string() },
|
|
31
|
+
handler: async (ctx, args) => {
|
|
32
|
+
const handle = args.callback as FunctionHandle<"mutation">;
|
|
33
|
+
await ctx.scheduler.runAfter(0, handle, {});
|
|
34
|
+
},
|
|
35
|
+
});
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Deriving validators from schema
|
|
39
|
+
|
|
40
|
+
Instead of manually repeating field types in return validators, extend the schema validator:
|
|
41
|
+
|
|
42
|
+
```ts
|
|
43
|
+
import { v } from "convex/values";
|
|
44
|
+
import schema from "./schema.js";
|
|
45
|
+
|
|
46
|
+
const notificationDoc = schema.tables.notifications.validator.extend({
|
|
47
|
+
_id: v.id("notifications"),
|
|
48
|
+
_creationTime: v.number(),
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
export const getLatest = query({
|
|
52
|
+
args: {},
|
|
53
|
+
returns: v.nullable(notificationDoc),
|
|
54
|
+
handler: async (ctx) => {
|
|
55
|
+
return await ctx.db.query("notifications").order("desc").first();
|
|
56
|
+
},
|
|
57
|
+
});
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## Static configuration with a globals table
|
|
61
|
+
|
|
62
|
+
A common pattern for component configuration is a single-document "globals" table:
|
|
63
|
+
|
|
64
|
+
```ts
|
|
65
|
+
// schema.ts
|
|
66
|
+
export default defineSchema({
|
|
67
|
+
globals: defineTable({
|
|
68
|
+
maxRetries: v.number(),
|
|
69
|
+
webhookUrl: v.optional(v.string()),
|
|
70
|
+
}),
|
|
71
|
+
// ... other tables
|
|
72
|
+
});
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
```ts
|
|
76
|
+
// lib.ts
|
|
77
|
+
export const configure = mutation({
|
|
78
|
+
args: { maxRetries: v.number(), webhookUrl: v.optional(v.string()) },
|
|
79
|
+
returns: v.null(),
|
|
80
|
+
handler: async (ctx, args) => {
|
|
81
|
+
const existing = await ctx.db.query("globals").first();
|
|
82
|
+
if (existing) {
|
|
83
|
+
await ctx.db.patch(existing._id, args);
|
|
84
|
+
} else {
|
|
85
|
+
await ctx.db.insert("globals", args);
|
|
86
|
+
}
|
|
87
|
+
return null;
|
|
88
|
+
},
|
|
89
|
+
});
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
## Class-based client wrappers
|
|
93
|
+
|
|
94
|
+
For components with many functions or configuration options, a class-based client provides a cleaner API. This pattern is common in published components.
|
|
95
|
+
|
|
96
|
+
```ts
|
|
97
|
+
// src/client/index.ts
|
|
98
|
+
import type { GenericMutationCtx, GenericDataModel } from "convex/server";
|
|
99
|
+
import type { ComponentApi } from "../component/_generated/component.js";
|
|
100
|
+
|
|
101
|
+
type MutationCtx = Pick<GenericMutationCtx<GenericDataModel>, "runMutation">;
|
|
102
|
+
|
|
103
|
+
export class Notifications {
|
|
104
|
+
constructor(
|
|
105
|
+
private component: ComponentApi,
|
|
106
|
+
private options?: { defaultChannel?: string },
|
|
107
|
+
) {}
|
|
108
|
+
|
|
109
|
+
async send(ctx: MutationCtx, args: { userId: string; message: string }) {
|
|
110
|
+
return await ctx.runMutation(this.component.lib.send, {
|
|
111
|
+
...args,
|
|
112
|
+
channel: this.options?.defaultChannel ?? "default",
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
```ts
|
|
119
|
+
// App usage
|
|
120
|
+
import { Notifications } from "@convex-dev/notifications";
|
|
121
|
+
import { components } from "./_generated/api";
|
|
122
|
+
|
|
123
|
+
const notifications = new Notifications(components.notifications, {
|
|
124
|
+
defaultChannel: "alerts",
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
export const send = mutation({
|
|
128
|
+
args: { message: v.string() },
|
|
129
|
+
handler: async (ctx, args) => {
|
|
130
|
+
const userId = await getAuthUserId(ctx);
|
|
131
|
+
await notifications.send(ctx, { userId, message: args.message });
|
|
132
|
+
},
|
|
133
|
+
});
|
|
134
|
+
```
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
# Convex Component Creation
|
|
2
|
+
|
|
3
|
+
Create reusable Convex components with clear boundaries and a small app-facing API.
|
|
4
|
+
|
|
5
|
+
## Core Concepts
|
|
6
|
+
|
|
7
|
+
Components are isolated units of backend logic with their own tables and functions. They must be justified — prefer normal app code if the feature doesn't need isolated tables or reusable persistent state.
|
|
8
|
+
|
|
9
|
+
**Golden Rule — Top-Down Execution:** Data and context always flow from app to component, never from component to app. The component cannot access `ctx.auth`, cannot read `process.env`, and cannot call app functions.
|
|
10
|
+
|
|
11
|
+
## Architecture Choice
|
|
12
|
+
|
|
13
|
+
| Goal | Shape | Reference |
|
|
14
|
+
|------|-------|-----------|
|
|
15
|
+
| Component for this app only | Local | Default |
|
|
16
|
+
| Publish or share across apps | Packaged | `references/components-advanced.md` |
|
|
17
|
+
| Not sure | Default to local | — |
|
|
18
|
+
|
|
19
|
+
Default: put under `convex/components/<componentName>/`
|
|
20
|
+
|
|
21
|
+
## Component Skeleton
|
|
22
|
+
|
|
23
|
+
```ts
|
|
24
|
+
// convex/components/notifications/convex.config.ts
|
|
25
|
+
import { defineComponent } from "convex/server";
|
|
26
|
+
export default defineComponent("notifications");
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
```ts
|
|
30
|
+
// convex/components/notifications/schema.ts
|
|
31
|
+
export default defineSchema({
|
|
32
|
+
notifications: defineTable({
|
|
33
|
+
userId: v.string(),
|
|
34
|
+
message: v.string(),
|
|
35
|
+
read: v.boolean(),
|
|
36
|
+
}).index("by_user", ["userId"]),
|
|
37
|
+
});
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
```ts
|
|
41
|
+
// convex/components/notifications/lib.ts
|
|
42
|
+
import { mutation, query } from "./_generated/server.js";
|
|
43
|
+
|
|
44
|
+
export const send = mutation({
|
|
45
|
+
args: { userId: v.string(), message: v.string() },
|
|
46
|
+
returns: v.id("notifications"),
|
|
47
|
+
handler: async (ctx, args) => {
|
|
48
|
+
return await ctx.db.insert("notifications", {
|
|
49
|
+
userId: args.userId,
|
|
50
|
+
message: args.message,
|
|
51
|
+
read: false,
|
|
52
|
+
});
|
|
53
|
+
},
|
|
54
|
+
});
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
```ts
|
|
58
|
+
// convex/convex.config.ts
|
|
59
|
+
import { defineApp } from "convex/server";
|
|
60
|
+
import notifications from "./components/notifications/convex.config.js";
|
|
61
|
+
|
|
62
|
+
const app = defineApp();
|
|
63
|
+
app.use(notifications);
|
|
64
|
+
export default app;
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
```ts
|
|
68
|
+
// convex/notifications.ts — app-side wrapper
|
|
69
|
+
export const sendNotification = mutation({
|
|
70
|
+
args: { message: v.string() },
|
|
71
|
+
returns: v.null(),
|
|
72
|
+
handler: async (ctx, args) => {
|
|
73
|
+
const userId = await getAuthUserId(ctx);
|
|
74
|
+
if (!userId) throw new Error("Not authenticated");
|
|
75
|
+
await ctx.runMutation(components.notifications.lib.send, {
|
|
76
|
+
userId,
|
|
77
|
+
message: args.message,
|
|
78
|
+
});
|
|
79
|
+
return null;
|
|
80
|
+
},
|
|
81
|
+
});
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Note: a function in `convex/components/notifications/lib.ts` is called as `components.notifications.lib.send`.
|
|
85
|
+
|
|
86
|
+
## Critical Rules
|
|
87
|
+
|
|
88
|
+
- **Auth stays in the app** — `ctx.auth` is not available inside components. Resolve auth in the app and pass the userId.
|
|
89
|
+
- **Env access stays in the app** — component functions cannot read `process.env`.
|
|
90
|
+
- **Parent IDs cross the boundary as strings** — use `v.string()`, not `v.id("parentTable")` for app-owned tables inside component args.
|
|
91
|
+
- **Import from component's own generated files** — use `./_generated/server` not the app's generated files.
|
|
92
|
+
- **Never expose component functions directly to clients** — create app wrappers when client access is needed.
|
|
93
|
+
- **HTTP routes stay in the app** — if the component defines HTTP handlers, mount them in `convex/http.ts`.
|
|
94
|
+
- **Use `paginator` from `convex-helpers`** for pagination across component boundaries — built-in `.paginate()` doesn't work across the boundary.
|
|
95
|
+
- **Add `args` and `returns` validators to all public component functions** — the boundary requires explicit type contracts.
|
|
96
|
+
|
|
97
|
+
## Key Patterns
|
|
98
|
+
|
|
99
|
+
### Auth and Env Access
|
|
100
|
+
|
|
101
|
+
```ts
|
|
102
|
+
// Good: app resolves auth and env, passes explicit values
|
|
103
|
+
const userId = await getAuthUserId(ctx);
|
|
104
|
+
if (!userId) throw new Error("Not authenticated");
|
|
105
|
+
|
|
106
|
+
await ctx.runAction(components.translator.translate, {
|
|
107
|
+
userId,
|
|
108
|
+
apiKey: process.env.OPENAI_API_KEY,
|
|
109
|
+
text: args.text,
|
|
110
|
+
});
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
### IDs Across the Boundary
|
|
114
|
+
|
|
115
|
+
```ts
|
|
116
|
+
// Bad: parent app table IDs are not valid component validators
|
|
117
|
+
args: { userId: v.id("users") }
|
|
118
|
+
|
|
119
|
+
// Good: treat parent-owned IDs as strings at the boundary
|
|
120
|
+
args: { userId: v.string() }
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
### Client-Facing API
|
|
124
|
+
|
|
125
|
+
```ts
|
|
126
|
+
// Bad: component function directly callable by clients
|
|
127
|
+
export const send = components.notifications.send;
|
|
128
|
+
|
|
129
|
+
// Good: re-export through an app mutation
|
|
130
|
+
export const sendNotification = mutation({
|
|
131
|
+
args: { message: v.string() },
|
|
132
|
+
handler: async (ctx, args) => {
|
|
133
|
+
const userId = await getAuthUserId(ctx);
|
|
134
|
+
if (!userId) throw new Error("Not authenticated");
|
|
135
|
+
await ctx.runMutation(components.notifications.lib.send, { userId, message: args.message });
|
|
136
|
+
return null;
|
|
137
|
+
},
|
|
138
|
+
});
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
## Step-by-Step Workflow
|
|
142
|
+
|
|
143
|
+
1. Plan what tables the component owns, what public functions it exposes, what data must be passed from the app, and what stays in the app as wrappers
|
|
144
|
+
2. Create `convex.config.ts`, `schema.ts`, and function files under `convex/components/<name>/`
|
|
145
|
+
3. Import `query`, `mutation`, `action` from the component's own `./_generated/server`
|
|
146
|
+
4. Wire into the app with `app.use(...)` in `convex/convex.config.ts`
|
|
147
|
+
5. Call the component from the app through `components.<name>` using `ctx.runQuery`/`ctx.runMutation`/`ctx.runAction`
|
|
148
|
+
6. Create app wrapper functions for any client access needed
|
|
149
|
+
7. Run `npx convex dev` and fix codegen/type issues
|
|
150
|
+
|
|
151
|
+
## Validation
|
|
152
|
+
|
|
153
|
+
Try validation in this order:
|
|
154
|
+
1. `npx convex codegen --component-dir convex/components/<name>`
|
|
155
|
+
2. `npx convex codegen`
|
|
156
|
+
3. `npx convex dev`
|
|
157
|
+
|
|
158
|
+
## Advanced Patterns
|
|
159
|
+
|
|
160
|
+
For function handles (callbacks), deriving validators from schema, globals tables, and class-based client wrappers, see `references/components-advanced.md`.
|
|
161
|
+
|
|
162
|
+
## Checklist
|
|
163
|
+
|
|
164
|
+
- [ ] Confirmed a component is the right abstraction
|
|
165
|
+
- [ ] Planned tables, public API, boundaries, and app wrappers
|
|
166
|
+
- [ ] Component lives under `convex/components/<name>/`
|
|
167
|
+
- [ ] Component imports from its own `./_generated/server`
|
|
168
|
+
- [ ] Auth, env access, and HTTP routes stay in the app
|
|
169
|
+
- [ ] Parent app IDs cross the boundary as `v.string()`
|
|
170
|
+
- [ ] Public functions have `args` and `returns` validators
|
|
171
|
+
- [ ] Ran `npx convex dev` and fixed codegen or type issues
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
# Function Budget
|
|
2
|
+
|
|
3
|
+
Use these rules when functions are hitting execution limits, transaction size errors, or returning excessively large payloads to the client.
|
|
4
|
+
|
|
5
|
+
## Core Principle
|
|
6
|
+
|
|
7
|
+
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.
|
|
8
|
+
|
|
9
|
+
## Limits to Know
|
|
10
|
+
|
|
11
|
+
These are the current values from the [Convex limits docs](https://docs.convex.dev/production/state/limits). Check that page for the latest numbers.
|
|
12
|
+
|
|
13
|
+
| Resource | Limit |
|
|
14
|
+
|---|---|
|
|
15
|
+
| Query/mutation execution time | 1 second (user code only, excludes DB operations) |
|
|
16
|
+
| Action execution time | 10 minutes |
|
|
17
|
+
| Data read per transaction | 16 MiB |
|
|
18
|
+
| Data written per transaction | 16 MiB |
|
|
19
|
+
| Documents scanned per transaction | 32,000 (includes documents filtered out by `.filter`) |
|
|
20
|
+
| Index ranges read per transaction | 4,096 (each `db.get` and `db.query` call) |
|
|
21
|
+
| Documents written per transaction | 16,000 |
|
|
22
|
+
| Individual document size | 1 MiB |
|
|
23
|
+
| Function return value size | 16 MiB |
|
|
24
|
+
|
|
25
|
+
## Symptoms
|
|
26
|
+
|
|
27
|
+
- "Function execution took too long" errors
|
|
28
|
+
- "Transaction too large" or read/write set size errors
|
|
29
|
+
- Slow queries that read many documents
|
|
30
|
+
- Client receiving large payloads that slow down page load
|
|
31
|
+
- `npx convex insights --details` showing high bytes read
|
|
32
|
+
|
|
33
|
+
## Common Causes
|
|
34
|
+
|
|
35
|
+
### Unbounded collection
|
|
36
|
+
|
|
37
|
+
A query that calls `.collect()` on a table without a reasonable limit. As the table grows, the query reads more and more documents.
|
|
38
|
+
|
|
39
|
+
### Large document reads on hot paths
|
|
40
|
+
|
|
41
|
+
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.
|
|
42
|
+
|
|
43
|
+
### Mutation doing too much work
|
|
44
|
+
|
|
45
|
+
A single mutation that updates hundreds of documents, backfills data, or rebuilds derived state in one transaction.
|
|
46
|
+
|
|
47
|
+
### Returning too much data to the client
|
|
48
|
+
|
|
49
|
+
A query returning full documents when the client only needs a few fields.
|
|
50
|
+
|
|
51
|
+
## Fix Order
|
|
52
|
+
|
|
53
|
+
### 1. Bound your reads
|
|
54
|
+
|
|
55
|
+
Never `.collect()` without a limit on a table that can grow unbounded.
|
|
56
|
+
|
|
57
|
+
```ts
|
|
58
|
+
// Bad: unbounded read, breaks as the table grows
|
|
59
|
+
const messages = await ctx.db.query("messages").collect();
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
```ts
|
|
63
|
+
// Good: paginate or limit
|
|
64
|
+
const messages = await ctx.db
|
|
65
|
+
.query("messages")
|
|
66
|
+
.withIndex("by_channel", (q) => q.eq("channelId", channelId))
|
|
67
|
+
.order("desc")
|
|
68
|
+
.take(50);
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### 2. Read smaller shapes
|
|
72
|
+
|
|
73
|
+
If the list page only needs title, author, and date, do not read full documents with rich content fields.
|
|
74
|
+
|
|
75
|
+
Use digest or summary tables for hot list pages. See `hot-path-rules.md` for the digest table pattern.
|
|
76
|
+
|
|
77
|
+
### 3. Break large mutations into batches
|
|
78
|
+
|
|
79
|
+
If a mutation needs to update hundreds of documents, split it into a self-scheduling chain.
|
|
80
|
+
|
|
81
|
+
```ts
|
|
82
|
+
// Bad: one mutation updating every row
|
|
83
|
+
export const backfillAll = internalMutation({
|
|
84
|
+
handler: async (ctx) => {
|
|
85
|
+
const docs = await ctx.db.query("items").collect();
|
|
86
|
+
for (const doc of docs) {
|
|
87
|
+
await ctx.db.patch(doc._id, { newField: computeValue(doc) });
|
|
88
|
+
}
|
|
89
|
+
},
|
|
90
|
+
});
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
```ts
|
|
94
|
+
// Good: cursor-based batch processing
|
|
95
|
+
export const backfillBatch = internalMutation({
|
|
96
|
+
args: { cursor: v.optional(v.string()), batchSize: v.optional(v.number()) },
|
|
97
|
+
handler: async (ctx, args) => {
|
|
98
|
+
const batchSize = args.batchSize ?? 100;
|
|
99
|
+
const result = await ctx.db
|
|
100
|
+
.query("items")
|
|
101
|
+
.paginate({ cursor: args.cursor ?? null, numItems: batchSize });
|
|
102
|
+
|
|
103
|
+
for (const doc of result.page) {
|
|
104
|
+
if (doc.newField === undefined) {
|
|
105
|
+
await ctx.db.patch(doc._id, { newField: computeValue(doc) });
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (!result.isDone) {
|
|
110
|
+
await ctx.scheduler.runAfter(0, internal.items.backfillBatch, {
|
|
111
|
+
cursor: result.continueCursor,
|
|
112
|
+
batchSize,
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
},
|
|
116
|
+
});
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
### 4. Move heavy work to actions
|
|
120
|
+
|
|
121
|
+
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.
|
|
122
|
+
|
|
123
|
+
Actions run outside the transaction and can call mutations to write results back.
|
|
124
|
+
|
|
125
|
+
```ts
|
|
126
|
+
// Bad: heavy computation inside a mutation
|
|
127
|
+
export const processUpload = mutation({
|
|
128
|
+
handler: async (ctx, args) => {
|
|
129
|
+
const result = expensiveComputation(args.data);
|
|
130
|
+
await ctx.db.insert("results", result);
|
|
131
|
+
},
|
|
132
|
+
});
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
```ts
|
|
136
|
+
// Good: action for heavy work, mutation for the write
|
|
137
|
+
export const processUpload = action({
|
|
138
|
+
handler: async (ctx, args) => {
|
|
139
|
+
const result = expensiveComputation(args.data);
|
|
140
|
+
await ctx.runMutation(internal.results.store, { result });
|
|
141
|
+
},
|
|
142
|
+
});
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
### 5. Trim return values
|
|
146
|
+
|
|
147
|
+
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.
|
|
148
|
+
|
|
149
|
+
```ts
|
|
150
|
+
// Bad: returns full documents including large content fields
|
|
151
|
+
export const list = query({
|
|
152
|
+
handler: async (ctx) => {
|
|
153
|
+
return await ctx.db.query("articles").take(20);
|
|
154
|
+
},
|
|
155
|
+
});
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
```ts
|
|
159
|
+
// Good: project to only the fields the client needs
|
|
160
|
+
export const list = query({
|
|
161
|
+
handler: async (ctx) => {
|
|
162
|
+
const articles = await ctx.db.query("articles").take(20);
|
|
163
|
+
return articles.map((a) => ({
|
|
164
|
+
_id: a._id,
|
|
165
|
+
title: a.title,
|
|
166
|
+
author: a.author,
|
|
167
|
+
createdAt: a._creationTime,
|
|
168
|
+
}));
|
|
169
|
+
},
|
|
170
|
+
});
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
### 6. Replace `ctx.runQuery` and `ctx.runMutation` with helper functions
|
|
174
|
+
|
|
175
|
+
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.
|
|
176
|
+
|
|
177
|
+
```ts
|
|
178
|
+
// Bad: unnecessary overhead from ctx.runQuery inside a mutation
|
|
179
|
+
export const createProject = mutation({
|
|
180
|
+
handler: async (ctx, args) => {
|
|
181
|
+
const user = await ctx.runQuery(api.users.getCurrentUser);
|
|
182
|
+
await ctx.db.insert("projects", { ...args, ownerId: user._id });
|
|
183
|
+
},
|
|
184
|
+
});
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
```ts
|
|
188
|
+
// Good: plain helper function, no extra overhead
|
|
189
|
+
export const createProject = mutation({
|
|
190
|
+
handler: async (ctx, args) => {
|
|
191
|
+
const user = await getCurrentUser(ctx);
|
|
192
|
+
await ctx.db.insert("projects", { ...args, ownerId: user._id });
|
|
193
|
+
},
|
|
194
|
+
});
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
Exception: components require `ctx.runQuery`/`ctx.runMutation`. Use them there, but prefer helpers everywhere else.
|
|
198
|
+
|
|
199
|
+
### 7. Avoid unnecessary `runAction` calls
|
|
200
|
+
|
|
201
|
+
`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).
|
|
202
|
+
|
|
203
|
+
```ts
|
|
204
|
+
// Bad: runAction overhead for no reason
|
|
205
|
+
export const processItems = action({
|
|
206
|
+
handler: async (ctx, args) => {
|
|
207
|
+
for (const item of args.items) {
|
|
208
|
+
await ctx.runAction(internal.items.processOne, { item });
|
|
209
|
+
}
|
|
210
|
+
},
|
|
211
|
+
});
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
```ts
|
|
215
|
+
// Good: plain function call
|
|
216
|
+
export const processItems = action({
|
|
217
|
+
handler: async (ctx, args) => {
|
|
218
|
+
for (const item of args.items) {
|
|
219
|
+
await processOneItem(ctx, { item });
|
|
220
|
+
}
|
|
221
|
+
},
|
|
222
|
+
});
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
## Verification
|
|
226
|
+
|
|
227
|
+
1. No function execution or transaction size errors
|
|
228
|
+
2. `npx convex insights --details` shows reduced bytes read
|
|
229
|
+
3. Large mutations are batched and self-scheduling
|
|
230
|
+
4. Client payloads are reasonably sized for the UI they serve
|
|
231
|
+
5. `ctx.runQuery`/`ctx.runMutation` in queries and mutations replaced with helpers where possible
|
|
232
|
+
6. Sibling functions with similar patterns were checked
|