vibesuite 1.3.2 → 2.0.1

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.
Files changed (75) hide show
  1. package/README.md +8 -1
  2. package/assets/.agent/skills/avoid-feature-creep/SKILL.md +307 -0
  3. package/assets/.agent/skills/avoid-feature-creep/agents/openai.yaml +3 -0
  4. package/assets/.agent/skills/avoid-feature-creep/assets/large-logo.png +0 -0
  5. package/assets/.agent/skills/avoid-feature-creep/assets/small-logo.svg +17 -0
  6. package/assets/.agent/skills/convex/SKILL.md +62 -0
  7. package/assets/.agent/skills/convex/agents/openai.yaml +3 -0
  8. package/assets/.agent/skills/convex/assets/large-logo.png +0 -0
  9. package/assets/.agent/skills/convex/assets/small-logo.svg +17 -0
  10. package/assets/.agent/skills/convex-agents/SKILL.md +516 -0
  11. package/assets/.agent/skills/convex-agents/agents/openai.yaml +3 -0
  12. package/assets/.agent/skills/convex-agents/assets/large-logo.png +0 -0
  13. package/assets/.agent/skills/convex-agents/assets/small-logo.svg +17 -0
  14. package/assets/.agent/skills/convex-best-practices/SKILL.md +369 -0
  15. package/assets/.agent/skills/convex-best-practices/agents/openai.yaml +3 -0
  16. package/assets/.agent/skills/convex-best-practices/assets/large-logo.png +0 -0
  17. package/assets/.agent/skills/convex-best-practices/assets/small-logo.svg +17 -0
  18. package/assets/.agent/skills/convex-component-authoring/SKILL.md +457 -0
  19. package/assets/.agent/skills/convex-component-authoring/agents/openai.yaml +3 -0
  20. package/assets/.agent/skills/convex-component-authoring/assets/large-logo.png +0 -0
  21. package/assets/.agent/skills/convex-component-authoring/assets/small-logo.svg +17 -0
  22. package/assets/.agent/skills/convex-cron-jobs/SKILL.md +604 -0
  23. package/assets/.agent/skills/convex-cron-jobs/agents/openai.yaml +3 -0
  24. package/assets/.agent/skills/convex-cron-jobs/assets/large-logo.png +0 -0
  25. package/assets/.agent/skills/convex-cron-jobs/assets/small-logo.svg +17 -0
  26. package/assets/.agent/skills/convex-file-storage/SKILL.md +467 -0
  27. package/assets/.agent/skills/convex-file-storage/agents/openai.yaml +3 -0
  28. package/assets/.agent/skills/convex-file-storage/assets/large-logo.png +0 -0
  29. package/assets/.agent/skills/convex-file-storage/assets/small-logo.svg +17 -0
  30. package/assets/.agent/skills/convex-functions/SKILL.md +458 -0
  31. package/assets/.agent/skills/convex-functions/agents/openai.yaml +3 -0
  32. package/assets/.agent/skills/convex-functions/assets/large-logo.png +0 -0
  33. package/assets/.agent/skills/convex-functions/assets/small-logo.svg +17 -0
  34. package/assets/.agent/skills/convex-http-actions/SKILL.md +733 -0
  35. package/assets/.agent/skills/convex-http-actions/agents/openai.yaml +3 -0
  36. package/assets/.agent/skills/convex-http-actions/assets/large-logo.png +0 -0
  37. package/assets/.agent/skills/convex-http-actions/assets/small-logo.svg +17 -0
  38. package/assets/.agent/skills/convex-migrations/SKILL.md +712 -0
  39. package/assets/.agent/skills/convex-migrations/agents/openai.yaml +3 -0
  40. package/assets/.agent/skills/convex-migrations/assets/large-logo.png +0 -0
  41. package/assets/.agent/skills/convex-migrations/assets/small-logo.svg +17 -0
  42. package/assets/.agent/skills/convex-realtime/SKILL.md +443 -0
  43. package/assets/.agent/skills/convex-realtime/agents/openai.yaml +3 -0
  44. package/assets/.agent/skills/convex-realtime/assets/large-logo.png +0 -0
  45. package/assets/.agent/skills/convex-realtime/assets/small-logo.svg +17 -0
  46. package/assets/.agent/skills/convex-schema-validator/SKILL.md +400 -0
  47. package/assets/.agent/skills/convex-schema-validator/agents/openai.yaml +3 -0
  48. package/assets/.agent/skills/convex-schema-validator/assets/large-logo.png +0 -0
  49. package/assets/.agent/skills/convex-schema-validator/assets/small-logo.svg +17 -0
  50. package/assets/.agent/skills/convex-security-audit/SKILL.md +539 -0
  51. package/assets/.agent/skills/convex-security-audit/agents/openai.yaml +3 -0
  52. package/assets/.agent/skills/convex-security-audit/assets/large-logo.png +0 -0
  53. package/assets/.agent/skills/convex-security-audit/assets/small-logo.svg +17 -0
  54. package/assets/.agent/skills/convex-security-check/SKILL.md +378 -0
  55. package/assets/.agent/skills/convex-security-check/agents/openai.yaml +3 -0
  56. package/assets/.agent/skills/convex-security-check/assets/large-logo.png +0 -0
  57. package/assets/.agent/skills/convex-security-check/assets/small-logo.svg +17 -0
  58. package/assets/.agent/skills/github-ops/SKILL.md +4 -4
  59. package/assets/.agent/skills/google-trends/SKILL.md +7 -7
  60. package/assets/.agent/skills/optimize-agent-context/SKILL.md +97 -0
  61. package/assets/.agent/skills/youtube-pipeline/SKILL.md +10 -10
  62. package/assets/.agent/workflows/LEGACY/init_smart_ops.md +2 -2
  63. package/assets/.agent/workflows/agent_reset.md +2 -2
  64. package/assets/.agent/workflows/mode-orchestrator.md +114 -640
  65. package/assets/.agent/workflows/mode-visionary.md +192 -0
  66. package/assets/.agent/workflows/optimize-agent-context.md +54 -0
  67. package/assets/.agent/workflows/remotion-build.md +17 -17
  68. package/assets/.agent/workflows/stitch.md +4 -4
  69. package/assets/VibeCode-Agents/custom_modes.yaml +1257 -0
  70. package/assets/VibeCode-Agents/vibe-orchestrator.yaml +427 -145
  71. package/assets/VibeCode-Agents/vibe-visionary.yaml +617 -0
  72. package/package.json +2 -2
  73. package/src/cli.js +416 -20
  74. package/src/harness.js +281 -0
  75. package/src/store.js +239 -0
@@ -0,0 +1,733 @@
1
+ ---
2
+ name: convex-http-actions
3
+ displayName: Convex HTTP Actions
4
+ description: External API integration and webhook handling including HTTP endpoint routing, request/response handling, authentication, CORS configuration, and webhook signature validation
5
+ version: 1.0.0
6
+ author: Convex
7
+ tags: [convex, http, actions, webhooks, api, endpoints]
8
+ ---
9
+
10
+ # Convex HTTP Actions
11
+
12
+ Build HTTP endpoints for webhooks, external API integrations, and custom routes in Convex applications.
13
+
14
+ ## Documentation Sources
15
+
16
+ Before implementing, do not assume; fetch the latest documentation:
17
+
18
+ - Primary: https://docs.convex.dev/functions/http-actions
19
+ - Actions Overview: https://docs.convex.dev/functions/actions
20
+ - Authentication: https://docs.convex.dev/auth
21
+ - For broader context: https://docs.convex.dev/llms.txt
22
+
23
+ ## Instructions
24
+
25
+ ### HTTP Actions Overview
26
+
27
+ HTTP actions allow you to define HTTP endpoints in Convex that can:
28
+
29
+ - Receive webhooks from third-party services
30
+ - Create custom API routes
31
+ - Handle file uploads
32
+ - Integrate with external services
33
+ - Serve dynamic content
34
+
35
+ ### Basic HTTP Router Setup
36
+
37
+ ```typescript
38
+ // convex/http.ts
39
+ import { httpRouter } from "convex/server";
40
+ import { httpAction } from "./_generated/server";
41
+
42
+ const http = httpRouter();
43
+
44
+ // Simple GET endpoint
45
+ http.route({
46
+ path: "/health",
47
+ method: "GET",
48
+ handler: httpAction(async (ctx, request) => {
49
+ return new Response(JSON.stringify({ status: "ok" }), {
50
+ status: 200,
51
+ headers: { "Content-Type": "application/json" },
52
+ });
53
+ }),
54
+ });
55
+
56
+ export default http;
57
+ ```
58
+
59
+ ### Request Handling
60
+
61
+ ```typescript
62
+ // convex/http.ts
63
+ import { httpRouter } from "convex/server";
64
+ import { httpAction } from "./_generated/server";
65
+
66
+ const http = httpRouter();
67
+
68
+ // Handle JSON body
69
+ http.route({
70
+ path: "/api/data",
71
+ method: "POST",
72
+ handler: httpAction(async (ctx, request) => {
73
+ // Parse JSON body
74
+ const body = await request.json();
75
+
76
+ // Access headers
77
+ const authHeader = request.headers.get("Authorization");
78
+
79
+ // Access URL parameters
80
+ const url = new URL(request.url);
81
+ const queryParam = url.searchParams.get("filter");
82
+
83
+ return new Response(
84
+ JSON.stringify({ received: body, filter: queryParam }),
85
+ {
86
+ status: 200,
87
+ headers: { "Content-Type": "application/json" },
88
+ }
89
+ );
90
+ }),
91
+ });
92
+
93
+ // Handle form data
94
+ http.route({
95
+ path: "/api/form",
96
+ method: "POST",
97
+ handler: httpAction(async (ctx, request) => {
98
+ const formData = await request.formData();
99
+ const name = formData.get("name");
100
+ const email = formData.get("email");
101
+
102
+ return new Response(
103
+ JSON.stringify({ name, email }),
104
+ {
105
+ status: 200,
106
+ headers: { "Content-Type": "application/json" },
107
+ }
108
+ );
109
+ }),
110
+ });
111
+
112
+ // Handle raw bytes
113
+ http.route({
114
+ path: "/api/upload",
115
+ method: "POST",
116
+ handler: httpAction(async (ctx, request) => {
117
+ const bytes = await request.bytes();
118
+ const contentType = request.headers.get("Content-Type") ?? "application/octet-stream";
119
+
120
+ // Store in Convex storage
121
+ const blob = new Blob([bytes], { type: contentType });
122
+ const storageId = await ctx.storage.store(blob);
123
+
124
+ return new Response(
125
+ JSON.stringify({ storageId }),
126
+ {
127
+ status: 200,
128
+ headers: { "Content-Type": "application/json" },
129
+ }
130
+ );
131
+ }),
132
+ });
133
+
134
+ export default http;
135
+ ```
136
+
137
+ ### Path Parameters
138
+
139
+ Use path prefix matching for dynamic routes:
140
+
141
+ ```typescript
142
+ // convex/http.ts
143
+ import { httpRouter } from "convex/server";
144
+ import { httpAction } from "./_generated/server";
145
+
146
+ const http = httpRouter();
147
+
148
+ // Match /api/users/* with pathPrefix
149
+ http.route({
150
+ pathPrefix: "/api/users/",
151
+ method: "GET",
152
+ handler: httpAction(async (ctx, request) => {
153
+ const url = new URL(request.url);
154
+ // Extract user ID from path: /api/users/123 -> "123"
155
+ const userId = url.pathname.replace("/api/users/", "");
156
+
157
+ return new Response(
158
+ JSON.stringify({ userId }),
159
+ {
160
+ status: 200,
161
+ headers: { "Content-Type": "application/json" },
162
+ }
163
+ );
164
+ }),
165
+ });
166
+
167
+ export default http;
168
+ ```
169
+
170
+ ### CORS Configuration
171
+
172
+ ```typescript
173
+ // convex/http.ts
174
+ import { httpRouter } from "convex/server";
175
+ import { httpAction } from "./_generated/server";
176
+
177
+ const http = httpRouter();
178
+
179
+ // CORS headers helper
180
+ const corsHeaders = {
181
+ "Access-Control-Allow-Origin": "*",
182
+ "Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, OPTIONS",
183
+ "Access-Control-Allow-Headers": "Content-Type, Authorization",
184
+ "Access-Control-Max-Age": "86400",
185
+ };
186
+
187
+ // Handle preflight requests
188
+ http.route({
189
+ path: "/api/data",
190
+ method: "OPTIONS",
191
+ handler: httpAction(async () => {
192
+ return new Response(null, {
193
+ status: 204,
194
+ headers: corsHeaders,
195
+ });
196
+ }),
197
+ });
198
+
199
+ // Actual endpoint with CORS
200
+ http.route({
201
+ path: "/api/data",
202
+ method: "POST",
203
+ handler: httpAction(async (ctx, request) => {
204
+ const body = await request.json();
205
+
206
+ return new Response(
207
+ JSON.stringify({ success: true, data: body }),
208
+ {
209
+ status: 200,
210
+ headers: {
211
+ "Content-Type": "application/json",
212
+ ...corsHeaders,
213
+ },
214
+ }
215
+ );
216
+ }),
217
+ });
218
+
219
+ export default http;
220
+ ```
221
+
222
+ ### Webhook Handling
223
+
224
+ ```typescript
225
+ // convex/http.ts
226
+ import { httpRouter } from "convex/server";
227
+ import { httpAction } from "./_generated/server";
228
+ import { internal } from "./_generated/api";
229
+
230
+ const http = httpRouter();
231
+
232
+ // Stripe webhook
233
+ http.route({
234
+ path: "/webhooks/stripe",
235
+ method: "POST",
236
+ handler: httpAction(async (ctx, request) => {
237
+ const signature = request.headers.get("stripe-signature");
238
+ if (!signature) {
239
+ return new Response("Missing signature", { status: 400 });
240
+ }
241
+
242
+ const body = await request.text();
243
+
244
+ // Verify webhook signature (in action with Node.js)
245
+ try {
246
+ await ctx.runAction(internal.stripe.verifyAndProcessWebhook, {
247
+ body,
248
+ signature,
249
+ });
250
+ return new Response("OK", { status: 200 });
251
+ } catch (error) {
252
+ console.error("Webhook error:", error);
253
+ return new Response("Webhook error", { status: 400 });
254
+ }
255
+ }),
256
+ });
257
+
258
+ // GitHub webhook
259
+ http.route({
260
+ path: "/webhooks/github",
261
+ method: "POST",
262
+ handler: httpAction(async (ctx, request) => {
263
+ const event = request.headers.get("X-GitHub-Event");
264
+ const signature = request.headers.get("X-Hub-Signature-256");
265
+
266
+ if (!signature) {
267
+ return new Response("Missing signature", { status: 400 });
268
+ }
269
+
270
+ const body = await request.text();
271
+
272
+ await ctx.runAction(internal.github.processWebhook, {
273
+ event: event ?? "unknown",
274
+ body,
275
+ signature,
276
+ });
277
+
278
+ return new Response("OK", { status: 200 });
279
+ }),
280
+ });
281
+
282
+ export default http;
283
+ ```
284
+
285
+ ### Webhook Signature Verification
286
+
287
+ ```typescript
288
+ // convex/stripe.ts
289
+ "use node";
290
+
291
+ import { internalAction, internalMutation } from "./_generated/server";
292
+ import { internal } from "./_generated/api";
293
+ import { v } from "convex/values";
294
+ import Stripe from "stripe";
295
+
296
+ const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);
297
+
298
+ export const verifyAndProcessWebhook = internalAction({
299
+ args: {
300
+ body: v.string(),
301
+ signature: v.string(),
302
+ },
303
+ returns: v.null(),
304
+ handler: async (ctx, args) => {
305
+ const webhookSecret = process.env.STRIPE_WEBHOOK_SECRET!;
306
+
307
+ // Verify signature
308
+ const event = stripe.webhooks.constructEvent(
309
+ args.body,
310
+ args.signature,
311
+ webhookSecret
312
+ );
313
+
314
+ // Process based on event type
315
+ switch (event.type) {
316
+ case "checkout.session.completed":
317
+ await ctx.runMutation(internal.payments.handleCheckoutComplete, {
318
+ sessionId: event.data.object.id,
319
+ customerId: event.data.object.customer as string,
320
+ });
321
+ break;
322
+
323
+ case "customer.subscription.updated":
324
+ await ctx.runMutation(internal.subscriptions.handleUpdate, {
325
+ subscriptionId: event.data.object.id,
326
+ status: event.data.object.status,
327
+ });
328
+ break;
329
+ }
330
+
331
+ return null;
332
+ },
333
+ });
334
+ ```
335
+
336
+ ### Authentication in HTTP Actions
337
+
338
+ ```typescript
339
+ // convex/http.ts
340
+ import { httpRouter } from "convex/server";
341
+ import { httpAction } from "./_generated/server";
342
+ import { internal } from "./_generated/api";
343
+
344
+ const http = httpRouter();
345
+
346
+ // API key authentication
347
+ http.route({
348
+ path: "/api/protected",
349
+ method: "GET",
350
+ handler: httpAction(async (ctx, request) => {
351
+ const apiKey = request.headers.get("X-API-Key");
352
+
353
+ if (!apiKey) {
354
+ return new Response(
355
+ JSON.stringify({ error: "Missing API key" }),
356
+ { status: 401, headers: { "Content-Type": "application/json" } }
357
+ );
358
+ }
359
+
360
+ // Validate API key
361
+ const isValid = await ctx.runQuery(internal.auth.validateApiKey, {
362
+ apiKey,
363
+ });
364
+
365
+ if (!isValid) {
366
+ return new Response(
367
+ JSON.stringify({ error: "Invalid API key" }),
368
+ { status: 403, headers: { "Content-Type": "application/json" } }
369
+ );
370
+ }
371
+
372
+ // Process authenticated request
373
+ const data = await ctx.runQuery(internal.data.getProtectedData, {});
374
+
375
+ return new Response(
376
+ JSON.stringify(data),
377
+ { status: 200, headers: { "Content-Type": "application/json" } }
378
+ );
379
+ }),
380
+ });
381
+
382
+ // Bearer token authentication
383
+ http.route({
384
+ path: "/api/user",
385
+ method: "GET",
386
+ handler: httpAction(async (ctx, request) => {
387
+ const authHeader = request.headers.get("Authorization");
388
+
389
+ if (!authHeader?.startsWith("Bearer ")) {
390
+ return new Response(
391
+ JSON.stringify({ error: "Missing or invalid Authorization header" }),
392
+ { status: 401, headers: { "Content-Type": "application/json" } }
393
+ );
394
+ }
395
+
396
+ const token = authHeader.slice(7);
397
+
398
+ // Validate token and get user
399
+ const user = await ctx.runQuery(internal.auth.validateToken, { token });
400
+
401
+ if (!user) {
402
+ return new Response(
403
+ JSON.stringify({ error: "Invalid token" }),
404
+ { status: 403, headers: { "Content-Type": "application/json" } }
405
+ );
406
+ }
407
+
408
+ return new Response(
409
+ JSON.stringify(user),
410
+ { status: 200, headers: { "Content-Type": "application/json" } }
411
+ );
412
+ }),
413
+ });
414
+
415
+ export default http;
416
+ ```
417
+
418
+ ### Calling Mutations and Queries
419
+
420
+ ```typescript
421
+ // convex/http.ts
422
+ import { httpRouter } from "convex/server";
423
+ import { httpAction } from "./_generated/server";
424
+ import { api, internal } from "./_generated/api";
425
+
426
+ const http = httpRouter();
427
+
428
+ http.route({
429
+ path: "/api/items",
430
+ method: "POST",
431
+ handler: httpAction(async (ctx, request) => {
432
+ const body = await request.json();
433
+
434
+ // Call a mutation
435
+ const itemId = await ctx.runMutation(internal.items.create, {
436
+ name: body.name,
437
+ description: body.description,
438
+ });
439
+
440
+ // Query the created item
441
+ const item = await ctx.runQuery(internal.items.get, { id: itemId });
442
+
443
+ return new Response(
444
+ JSON.stringify(item),
445
+ { status: 201, headers: { "Content-Type": "application/json" } }
446
+ );
447
+ }),
448
+ });
449
+
450
+ http.route({
451
+ path: "/api/items",
452
+ method: "GET",
453
+ handler: httpAction(async (ctx, request) => {
454
+ const url = new URL(request.url);
455
+ const limit = parseInt(url.searchParams.get("limit") ?? "10");
456
+
457
+ const items = await ctx.runQuery(internal.items.list, { limit });
458
+
459
+ return new Response(
460
+ JSON.stringify(items),
461
+ { status: 200, headers: { "Content-Type": "application/json" } }
462
+ );
463
+ }),
464
+ });
465
+
466
+ export default http;
467
+ ```
468
+
469
+ ### Error Handling
470
+
471
+ ```typescript
472
+ // convex/http.ts
473
+ import { httpRouter } from "convex/server";
474
+ import { httpAction } from "./_generated/server";
475
+
476
+ const http = httpRouter();
477
+
478
+ // Helper for JSON responses
479
+ function jsonResponse(data: unknown, status = 200) {
480
+ return new Response(JSON.stringify(data), {
481
+ status,
482
+ headers: { "Content-Type": "application/json" },
483
+ });
484
+ }
485
+
486
+ // Helper for error responses
487
+ function errorResponse(message: string, status: number) {
488
+ return jsonResponse({ error: message }, status);
489
+ }
490
+
491
+ http.route({
492
+ path: "/api/process",
493
+ method: "POST",
494
+ handler: httpAction(async (ctx, request) => {
495
+ try {
496
+ // Validate content type
497
+ const contentType = request.headers.get("Content-Type");
498
+ if (!contentType?.includes("application/json")) {
499
+ return errorResponse("Content-Type must be application/json", 415);
500
+ }
501
+
502
+ // Parse body
503
+ let body;
504
+ try {
505
+ body = await request.json();
506
+ } catch {
507
+ return errorResponse("Invalid JSON body", 400);
508
+ }
509
+
510
+ // Validate required fields
511
+ if (!body.data) {
512
+ return errorResponse("Missing required field: data", 400);
513
+ }
514
+
515
+ // Process request
516
+ const result = await ctx.runMutation(internal.process.handle, {
517
+ data: body.data,
518
+ });
519
+
520
+ return jsonResponse({ success: true, result }, 200);
521
+ } catch (error) {
522
+ console.error("Processing error:", error);
523
+ return errorResponse("Internal server error", 500);
524
+ }
525
+ }),
526
+ });
527
+
528
+ export default http;
529
+ ```
530
+
531
+ ### File Downloads
532
+
533
+ ```typescript
534
+ // convex/http.ts
535
+ import { httpRouter } from "convex/server";
536
+ import { httpAction } from "./_generated/server";
537
+ import { Id } from "./_generated/dataModel";
538
+
539
+ const http = httpRouter();
540
+
541
+ http.route({
542
+ pathPrefix: "/files/",
543
+ method: "GET",
544
+ handler: httpAction(async (ctx, request) => {
545
+ const url = new URL(request.url);
546
+ const fileId = url.pathname.replace("/files/", "") as Id<"_storage">;
547
+
548
+ // Get file URL from storage
549
+ const fileUrl = await ctx.storage.getUrl(fileId);
550
+
551
+ if (!fileUrl) {
552
+ return new Response("File not found", { status: 404 });
553
+ }
554
+
555
+ // Redirect to the file URL
556
+ return Response.redirect(fileUrl, 302);
557
+ }),
558
+ });
559
+
560
+ export default http;
561
+ ```
562
+
563
+ ## Examples
564
+
565
+ ### Complete Webhook Integration
566
+
567
+ ```typescript
568
+ // convex/http.ts
569
+ import { httpRouter } from "convex/server";
570
+ import { httpAction } from "./_generated/server";
571
+ import { internal } from "./_generated/api";
572
+
573
+ const http = httpRouter();
574
+
575
+ // Clerk webhook for user sync
576
+ http.route({
577
+ path: "/webhooks/clerk",
578
+ method: "POST",
579
+ handler: httpAction(async (ctx, request) => {
580
+ const svixId = request.headers.get("svix-id");
581
+ const svixTimestamp = request.headers.get("svix-timestamp");
582
+ const svixSignature = request.headers.get("svix-signature");
583
+
584
+ if (!svixId || !svixTimestamp || !svixSignature) {
585
+ return new Response("Missing Svix headers", { status: 400 });
586
+ }
587
+
588
+ const body = await request.text();
589
+
590
+ try {
591
+ await ctx.runAction(internal.clerk.verifyAndProcess, {
592
+ body,
593
+ svixId,
594
+ svixTimestamp,
595
+ svixSignature,
596
+ });
597
+ return new Response("OK", { status: 200 });
598
+ } catch (error) {
599
+ console.error("Clerk webhook error:", error);
600
+ return new Response("Webhook verification failed", { status: 400 });
601
+ }
602
+ }),
603
+ });
604
+
605
+ export default http;
606
+ ```
607
+
608
+ ```typescript
609
+ // convex/clerk.ts
610
+ "use node";
611
+
612
+ import { internalAction, internalMutation } from "./_generated/server";
613
+ import { internal } from "./_generated/api";
614
+ import { v } from "convex/values";
615
+ import { Webhook } from "svix";
616
+
617
+ export const verifyAndProcess = internalAction({
618
+ args: {
619
+ body: v.string(),
620
+ svixId: v.string(),
621
+ svixTimestamp: v.string(),
622
+ svixSignature: v.string(),
623
+ },
624
+ returns: v.null(),
625
+ handler: async (ctx, args) => {
626
+ const webhookSecret = process.env.CLERK_WEBHOOK_SECRET!;
627
+ const wh = new Webhook(webhookSecret);
628
+
629
+ const event = wh.verify(args.body, {
630
+ "svix-id": args.svixId,
631
+ "svix-timestamp": args.svixTimestamp,
632
+ "svix-signature": args.svixSignature,
633
+ }) as { type: string; data: Record<string, unknown> };
634
+
635
+ switch (event.type) {
636
+ case "user.created":
637
+ await ctx.runMutation(internal.users.create, {
638
+ clerkId: event.data.id as string,
639
+ email: (event.data.email_addresses as Array<{ email_address: string }>)[0]?.email_address,
640
+ name: `${event.data.first_name} ${event.data.last_name}`,
641
+ });
642
+ break;
643
+
644
+ case "user.updated":
645
+ await ctx.runMutation(internal.users.update, {
646
+ clerkId: event.data.id as string,
647
+ email: (event.data.email_addresses as Array<{ email_address: string }>)[0]?.email_address,
648
+ name: `${event.data.first_name} ${event.data.last_name}`,
649
+ });
650
+ break;
651
+
652
+ case "user.deleted":
653
+ await ctx.runMutation(internal.users.remove, {
654
+ clerkId: event.data.id as string,
655
+ });
656
+ break;
657
+ }
658
+
659
+ return null;
660
+ },
661
+ });
662
+ ```
663
+
664
+ ### Schema for HTTP API
665
+
666
+ ```typescript
667
+ // convex/schema.ts
668
+ import { defineSchema, defineTable } from "convex/server";
669
+ import { v } from "convex/values";
670
+
671
+ export default defineSchema({
672
+ apiKeys: defineTable({
673
+ key: v.string(),
674
+ userId: v.id("users"),
675
+ name: v.string(),
676
+ createdAt: v.number(),
677
+ lastUsedAt: v.optional(v.number()),
678
+ revokedAt: v.optional(v.number()),
679
+ })
680
+ .index("by_key", ["key"])
681
+ .index("by_user", ["userId"]),
682
+
683
+ webhookEvents: defineTable({
684
+ source: v.string(),
685
+ eventType: v.string(),
686
+ payload: v.any(),
687
+ processedAt: v.number(),
688
+ status: v.union(
689
+ v.literal("success"),
690
+ v.literal("failed")
691
+ ),
692
+ error: v.optional(v.string()),
693
+ })
694
+ .index("by_source", ["source"])
695
+ .index("by_status", ["status"]),
696
+
697
+ users: defineTable({
698
+ clerkId: v.string(),
699
+ email: v.string(),
700
+ name: v.string(),
701
+ }).index("by_clerk_id", ["clerkId"]),
702
+ });
703
+ ```
704
+
705
+ ## Best Practices
706
+
707
+ - Never run `npx convex deploy` unless explicitly instructed
708
+ - Never run any git commands unless explicitly instructed
709
+ - Always validate and sanitize incoming request data
710
+ - Use internal functions for database operations
711
+ - Implement proper error handling with appropriate status codes
712
+ - Add CORS headers for browser-accessible endpoints
713
+ - Verify webhook signatures before processing
714
+ - Log webhook events for debugging
715
+ - Use environment variables for secrets
716
+ - Handle timeouts gracefully
717
+
718
+ ## Common Pitfalls
719
+
720
+ 1. **Missing CORS preflight handler** - Browsers send OPTIONS requests first
721
+ 2. **Not validating webhook signatures** - Security vulnerability
722
+ 3. **Exposing internal functions** - Use internal functions from HTTP actions
723
+ 4. **Forgetting Content-Type headers** - Clients may not parse responses correctly
724
+ 5. **Not handling request body errors** - Invalid JSON will throw
725
+ 6. **Blocking on long operations** - Use scheduled functions for heavy processing
726
+
727
+ ## References
728
+
729
+ - Convex Documentation: https://docs.convex.dev/
730
+ - Convex LLMs.txt: https://docs.convex.dev/llms.txt
731
+ - HTTP Actions: https://docs.convex.dev/functions/http-actions
732
+ - Actions: https://docs.convex.dev/functions/actions
733
+ - Authentication: https://docs.convex.dev/auth