[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"skill:01M6CCD2T2J7BFD2CNV3TGGJEQ":3,"skill-same:01M6CCD2T2J7BFD2CNV3TGGJEQ":100},{"skillNo":4,"name":5,"nameZh":6,"description":7,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":16,"categories":17,"keywords":28,"publishedAt":44,"linkOnly":45,"markdownContent":46,"markdownBody":47,"frontMatter":48,"refName":53,"commitSha":54,"skillDirectory":55,"forksCount":14,"repositoryHtmlUrl":56,"repositoryHomepageUrl":57,"ownerHtmlUrl":58,"ownerBlogUrl":6,"ownerDescription":6,"updatedAt":59,"install":60,"resources":73},"01M6CCD2T2J7BFD2CNV3TGGJEQ","convex-http-actions",null,"External API integration and webhook handling including HTTP endpoint routing, request\u002Fresponse handling, authentication, CORS configuration, and webhook signature validation","baisharg\u002FASTN","baisharg","https:\u002F\u002Favatars.githubusercontent.com\u002Fu\u002F204916775?v=4","01M6CC8XHZYVK2DR22FZ1BESCG","BAISH","01M6CC8XP400QZ9DXAWTC8EG3X",2,"AGPL-3.0","https:\u002F\u002Fgithub.com\u002Fbaisharg\u002FASTN\u002Fblob\u002Fmain\u002F.claude\u002Fskills\u002Fconvex-http-actions\u002FSKILL.md",[18,23],{"categoryNo":19,"categoryName":20,"taxonomyType":21,"taxonomyTypeDesc":22},"01JBSEEDCAT000000000000010","后端与接口","SCENARIO","使用场景",{"categoryNo":24,"categoryName":25,"taxonomyType":26,"taxonomyTypeDesc":27},"01JBSEEDCAT000000000000030","开发工程师","PROFESSION","适用职业",[29,30,31,32,33,34,35,36,37,38,39,40,41,42,43],"convex","http","actions","convex http actions","routing","authentication","validation","integration","endpoint","api","cors","webhook handling","external api integration","webhooks","endpoints","2026-11-10 10:12:30",false,"---\nname: convex-http-actions\ndisplayName: Convex HTTP Actions\ndescription: External API integration and webhook handling including HTTP endpoint routing, request\u002Fresponse handling, authentication, CORS configuration, and webhook signature validation\nversion: 1.0.0\nauthor: Convex\ntags: [convex, http, actions, webhooks, api, endpoints]\n---\n\n# Convex HTTP Actions\n\nBuild HTTP endpoints for webhooks, external API integrations, and custom routes in Convex applications.\n\n## Documentation Sources\n\nBefore implementing, do not assume; fetch the latest documentation:\n\n- Primary: https:\u002F\u002Fdocs.convex.dev\u002Ffunctions\u002Fhttp-actions\n- Actions Overview: https:\u002F\u002Fdocs.convex.dev\u002Ffunctions\u002Factions\n- Authentication: https:\u002F\u002Fdocs.convex.dev\u002Fauth\n- For broader context: https:\u002F\u002Fdocs.convex.dev\u002Fllms.txt\n\n## Instructions\n\n### HTTP Actions Overview\n\nHTTP actions allow you to define HTTP endpoints in Convex that can:\n\n- Receive webhooks from third-party services\n- Create custom API routes\n- Handle file uploads\n- Integrate with external services\n- Serve dynamic content\n\n### Basic HTTP Router Setup\n\n```typescript\n\u002F\u002F convex\u002Fhttp.ts\nimport { httpRouter } from 'convex\u002Fserver'\nimport { httpAction } from '.\u002F_generated\u002Fserver'\n\nconst http = httpRouter()\n\n\u002F\u002F Simple GET endpoint\nhttp.route({\n  path: '\u002Fhealth',\n  method: 'GET',\n  handler: httpAction(async (ctx, request) => {\n    return new Response(JSON.stringify({ status: 'ok' }), {\n      status: 200,\n      headers: { 'Content-Type': 'application\u002Fjson' },\n    })\n  }),\n})\n\nexport default http\n```\n\n### Request Handling\n\n```typescript\n\u002F\u002F convex\u002Fhttp.ts\nimport { httpRouter } from 'convex\u002Fserver'\nimport { httpAction } from '.\u002F_generated\u002Fserver'\n\nconst http = httpRouter()\n\n\u002F\u002F Handle JSON body\nhttp.route({\n  path: '\u002Fapi\u002Fdata',\n  method: 'POST',\n  handler: httpAction(async (ctx, request) => {\n    \u002F\u002F Parse JSON body\n    const body = await request.json()\n\n    \u002F\u002F Access headers\n    const authHeader = request.headers.get('Authorization')\n\n    \u002F\u002F Access URL parameters\n    const url = new URL(request.url)\n    const queryParam = url.searchParams.get('filter')\n\n    return new Response(\n      JSON.stringify({ received: body, filter: queryParam }),\n      {\n        status: 200,\n        headers: { 'Content-Type': 'application\u002Fjson' },\n      },\n    )\n  }),\n})\n\n\u002F\u002F Handle form data\nhttp.route({\n  path: '\u002Fapi\u002Fform',\n  method: 'POST',\n  handler: httpAction(async (ctx, request) => {\n    const formData = await request.formData()\n    const name = formData.get('name')\n    const email = formData.get('email')\n\n    return new Response(JSON.stringify({ name, email }), {\n      status: 200,\n      headers: { 'Content-Type': 'application\u002Fjson' },\n    })\n  }),\n})\n\n\u002F\u002F Handle raw bytes\nhttp.route({\n  path: '\u002Fapi\u002Fupload',\n  method: 'POST',\n  handler: httpAction(async (ctx, request) => {\n    const bytes = await request.bytes()\n    const contentType =\n      request.headers.get('Content-Type') ?? 'application\u002Foctet-stream'\n\n    \u002F\u002F Store in Convex storage\n    const blob = new Blob([bytes], { type: contentType })\n    const storageId = await ctx.storage.store(blob)\n\n    return new Response(JSON.stringify({ storageId }), {\n      status: 200,\n      headers: { 'Content-Type': 'application\u002Fjson' },\n    })\n  }),\n})\n\nexport default http\n```\n\n### Path Parameters\n\nUse path prefix matching for dynamic routes:\n\n```typescript\n\u002F\u002F convex\u002Fhttp.ts\nimport { httpRouter } from 'convex\u002Fserver'\nimport { httpAction } from '.\u002F_generated\u002Fserver'\n\nconst http = httpRouter()\n\n\u002F\u002F Match \u002Fapi\u002Fusers\u002F* with pathPrefix\nhttp.route({\n  pathPrefix: '\u002Fapi\u002Fusers\u002F',\n  method: 'GET',\n  handler: httpAction(async (ctx, request) => {\n    const url = new URL(request.url)\n    \u002F\u002F Extract user ID from path: \u002Fapi\u002Fusers\u002F123 -> \"123\"\n    const userId = url.pathname.replace('\u002Fapi\u002Fusers\u002F', '')\n\n    return new Response(JSON.stringify({ userId }), {\n      status: 200,\n      headers: { 'Content-Type': 'application\u002Fjson' },\n    })\n  }),\n})\n\nexport default http\n```\n\n### CORS Configuration\n\n```typescript\n\u002F\u002F convex\u002Fhttp.ts\nimport { httpRouter } from 'convex\u002Fserver'\nimport { httpAction } from '.\u002F_generated\u002Fserver'\n\nconst http = httpRouter()\n\n\u002F\u002F CORS headers helper\nconst corsHeaders = {\n  'Access-Control-Allow-Origin': '*',\n  'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',\n  'Access-Control-Allow-Headers': 'Content-Type, Authorization',\n  'Access-Control-Max-Age': '86400',\n}\n\n\u002F\u002F Handle preflight requests\nhttp.route({\n  path: '\u002Fapi\u002Fdata',\n  method: 'OPTIONS',\n  handler: httpAction(async () => {\n    return new Response(null, {\n      status: 204,\n      headers: corsHeaders,\n    })\n  }),\n})\n\n\u002F\u002F Actual endpoint with CORS\nhttp.route({\n  path: '\u002Fapi\u002Fdata',\n  method: 'POST',\n  handler: httpAction(async (ctx, request) => {\n    const body = await request.json()\n\n    return new Response(JSON.stringify({ success: true, data: body }), {\n      status: 200,\n      headers: {\n        'Content-Type': 'application\u002Fjson',\n        ...corsHeaders,\n      },\n    })\n  }),\n})\n\nexport default http\n```\n\n### Webhook Handling\n\n```typescript\n\u002F\u002F convex\u002Fhttp.ts\nimport { httpRouter } from 'convex\u002Fserver'\nimport { httpAction } from '.\u002F_generated\u002Fserver'\nimport { internal } from '.\u002F_generated\u002Fapi'\n\nconst http = httpRouter()\n\n\u002F\u002F Stripe webhook\nhttp.route({\n  path: '\u002Fwebhooks\u002Fstripe',\n  method: 'POST',\n  handler: httpAction(async (ctx, request) => {\n    const signature = request.headers.get('stripe-signature')\n    if (!signature) {\n      return new Response('Missing signature', { status: 400 })\n    }\n\n    const body = await request.text()\n\n    \u002F\u002F Verify webhook signature (in action with Node.js)\n    try {\n      await ctx.runAction(internal.stripe.verifyAndProcessWebhook, {\n        body,\n        signature,\n      })\n      return new Response('OK', { status: 200 })\n    } catch (error) {\n      console.error('Webhook error:', error)\n      return new Response('Webhook error', { status: 400 })\n    }\n  }),\n})\n\n\u002F\u002F GitHub webhook\nhttp.route({\n  path: '\u002Fwebhooks\u002Fgithub',\n  method: 'POST',\n  handler: httpAction(async (ctx, request) => {\n    const event = request.headers.get('X-GitHub-Event')\n    const signature = request.headers.get('X-Hub-Signature-256')\n\n    if (!signature) {\n      return new Response('Missing signature', { status: 400 })\n    }\n\n    const body = await request.text()\n\n    await ctx.runAction(internal.github.processWebhook, {\n      event: event ?? 'unknown',\n      body,\n      signature,\n    })\n\n    return new Response('OK', { status: 200 })\n  }),\n})\n\nexport default http\n```\n\n### Webhook Signature Verification\n\n```typescript\n\u002F\u002F convex\u002Fstripe.ts\n'use node'\n\nimport { internalAction, internalMutation } from '.\u002F_generated\u002Fserver'\nimport { internal } from '.\u002F_generated\u002Fapi'\nimport { v } from 'convex\u002Fvalues'\nimport Stripe from 'stripe'\n\nconst stripe = new Stripe(process.env.STRIPE_SECRET_KEY!)\n\nexport const verifyAndProcessWebhook = internalAction({\n  args: {\n    body: v.string(),\n    signature: v.string(),\n  },\n  returns: v.null(),\n  handler: async (ctx, args) => {\n    const webhookSecret = process.env.STRIPE_WEBHOOK_SECRET!\n\n    \u002F\u002F Verify signature\n    const event = stripe.webhooks.constructEvent(\n      args.body,\n      args.signature,\n      webhookSecret,\n    )\n\n    \u002F\u002F Process based on event type\n    switch (event.type) {\n      case 'checkout.session.completed':\n        await ctx.runMutation(internal.payments.handleCheckoutComplete, {\n          sessionId: event.data.object.id,\n          customerId: event.data.object.customer as string,\n        })\n        break\n\n      case 'customer.subscription.updated':\n        await ctx.runMutation(internal.subscriptions.handleUpdate, {\n          subscriptionId: event.data.object.id,\n          status: event.data.object.status,\n        })\n        break\n    }\n\n    return null\n  },\n})\n```\n\n### Authentication in HTTP Actions\n\n```typescript\n\u002F\u002F convex\u002Fhttp.ts\nimport { httpRouter } from 'convex\u002Fserver'\nimport { httpAction } from '.\u002F_generated\u002Fserver'\nimport { internal } from '.\u002F_generated\u002Fapi'\n\nconst http = httpRouter()\n\n\u002F\u002F API key authentication\nhttp.route({\n  path: '\u002Fapi\u002Fprotected',\n  method: 'GET',\n  handler: httpAction(async (ctx, request) => {\n    const apiKey = request.headers.get('X-API-Key')\n\n    if (!apiKey) {\n      return new Response(JSON.stringify({ error: 'Missing API key' }), {\n        status: 401,\n        headers: { 'Content-Type': 'application\u002Fjson' },\n      })\n    }\n\n    \u002F\u002F Validate API key\n    const isValid = await ctx.runQuery(internal.auth.validateApiKey, {\n      apiKey,\n    })\n\n    if (!isValid) {\n      return new Response(JSON.stringify({ error: 'Invalid API key' }), {\n        status: 403,\n        headers: { 'Content-Type': 'application\u002Fjson' },\n      })\n    }\n\n    \u002F\u002F Process authenticated request\n    const data = await ctx.runQuery(internal.data.getProtectedData, {})\n\n    return new Response(JSON.stringify(data), {\n      status: 200,\n      headers: { 'Content-Type': 'application\u002Fjson' },\n    })\n  }),\n})\n\n\u002F\u002F Bearer token authentication\nhttp.route({\n  path: '\u002Fapi\u002Fuser',\n  method: 'GET',\n  handler: httpAction(async (ctx, request) => {\n    const authHeader = request.headers.get('Authorization')\n\n    if (!authHeader?.startsWith('Bearer ')) {\n      return new Response(\n        JSON.stringify({ error: 'Missing or invalid Authorization header' }),\n        { status: 401, headers: { 'Content-Type': 'application\u002Fjson' } },\n      )\n    }\n\n    const token = authHeader.slice(7)\n\n    \u002F\u002F Validate token and get user\n    const user = await ctx.runQuery(internal.auth.validateToken, { token })\n\n    if (!user) {\n      return new Response(JSON.stringify({ error: 'Invalid token' }), {\n        status: 403,\n        headers: { 'Content-Type': 'application\u002Fjson' },\n      })\n    }\n\n    return new Response(JSON.stringify(user), {\n      status: 200,\n      headers: { 'Content-Type': 'application\u002Fjson' },\n    })\n  }),\n})\n\nexport default http\n```\n\n### Calling Mutations and Queries\n\n```typescript\n\u002F\u002F convex\u002Fhttp.ts\nimport { httpRouter } from 'convex\u002Fserver'\nimport { httpAction } from '.\u002F_generated\u002Fserver'\nimport { api, internal } from '.\u002F_generated\u002Fapi'\n\nconst http = httpRouter()\n\nhttp.route({\n  path: '\u002Fapi\u002Fitems',\n  method: 'POST',\n  handler: httpAction(async (ctx, request) => {\n    const body = await request.json()\n\n    \u002F\u002F Call a mutation\n    const itemId = await ctx.runMutation(internal.items.create, {\n      name: body.name,\n      description: body.description,\n    })\n\n    \u002F\u002F Query the created item\n    const item = await ctx.runQuery(internal.items.get, { id: itemId })\n\n    return new Response(JSON.stringify(item), {\n      status: 201,\n      headers: { 'Content-Type': 'application\u002Fjson' },\n    })\n  }),\n})\n\nhttp.route({\n  path: '\u002Fapi\u002Fitems',\n  method: 'GET',\n  handler: httpAction(async (ctx, request) => {\n    const url = new URL(request.url)\n    const limit = parseInt(url.searchParams.get('limit') ?? '10')\n\n    const items = await ctx.runQuery(internal.items.list, { limit })\n\n    return new Response(JSON.stringify(items), {\n      status: 200,\n      headers: { 'Content-Type': 'application\u002Fjson' },\n    })\n  }),\n})\n\nexport default http\n```\n\n### Error Handling\n\n```typescript\n\u002F\u002F convex\u002Fhttp.ts\nimport { httpRouter } from 'convex\u002Fserver'\nimport { httpAction } from '.\u002F_generated\u002Fserver'\n\nconst http = httpRouter()\n\n\u002F\u002F Helper for JSON responses\nfunction jsonResponse(data: unknown, status = 200) {\n  return new Response(JSON.stringify(data), {\n    status,\n    headers: { 'Content-Type': 'application\u002Fjson' },\n  })\n}\n\n\u002F\u002F Helper for error responses\nfunction errorResponse(message: string, status: number) {\n  return jsonResponse({ error: message }, status)\n}\n\nhttp.route({\n  path: '\u002Fapi\u002Fprocess',\n  method: 'POST',\n  handler: httpAction(async (ctx, request) => {\n    try {\n      \u002F\u002F Validate content type\n      const contentType = request.headers.get('Content-Type')\n      if (!contentType?.includes('application\u002Fjson')) {\n        return errorResponse('Content-Type must be application\u002Fjson', 415)\n      }\n\n      \u002F\u002F Parse body\n      let body\n      try {\n        body = await request.json()\n      } catch {\n        return errorResponse('Invalid JSON body', 400)\n      }\n\n      \u002F\u002F Validate required fields\n      if (!body.data) {\n        return errorResponse('Missing required field: data', 400)\n      }\n\n      \u002F\u002F Process request\n      const result = await ctx.runMutation(internal.process.handle, {\n        data: body.data,\n      })\n\n      return jsonResponse({ success: true, result }, 200)\n    } catch (error) {\n      console.error('Processing error:', error)\n      return errorResponse('Internal server error', 500)\n    }\n  }),\n})\n\nexport default http\n```\n\n### File Downloads\n\n```typescript\n\u002F\u002F convex\u002Fhttp.ts\nimport { httpRouter } from 'convex\u002Fserver'\nimport { httpAction } from '.\u002F_generated\u002Fserver'\nimport { Id } from '.\u002F_generated\u002FdataModel'\n\nconst http = httpRouter()\n\nhttp.route({\n  pathPrefix: '\u002Ffiles\u002F',\n  method: 'GET',\n  handler: httpAction(async (ctx, request) => {\n    const url = new URL(request.url)\n    const fileId = url.pathname.replace('\u002Ffiles\u002F', '') as Id\u003C'_storage'>\n\n    \u002F\u002F Get file URL from storage\n    const fileUrl = await ctx.storage.getUrl(fileId)\n\n    if (!fileUrl) {\n      return new Response('File not found', { status: 404 })\n    }\n\n    \u002F\u002F Redirect to the file URL\n    return Response.redirect(fileUrl, 302)\n  }),\n})\n\nexport default http\n```\n\n## Examples\n\n### Complete Webhook Integration\n\n```typescript\n\u002F\u002F convex\u002Fhttp.ts\nimport { httpRouter } from 'convex\u002Fserver'\nimport { httpAction } from '.\u002F_generated\u002Fserver'\nimport { internal } from '.\u002F_generated\u002Fapi'\n\nconst http = httpRouter()\n\n\u002F\u002F Clerk webhook for user sync\nhttp.route({\n  path: '\u002Fwebhooks\u002Fclerk',\n  method: 'POST',\n  handler: httpAction(async (ctx, request) => {\n    const svixId = request.headers.get('svix-id')\n    const svixTimestamp = request.headers.get('svix-timestamp')\n    const svixSignature = request.headers.get('svix-signature')\n\n    if (!svixId || !svixTimestamp || !svixSignature) {\n      return new Response('Missing Svix headers', { status: 400 })\n    }\n\n    const body = await request.text()\n\n    try {\n      await ctx.runAction(internal.clerk.verifyAndProcess, {\n        body,\n        svixId,\n        svixTimestamp,\n        svixSignature,\n      })\n      return new Response('OK', { status: 200 })\n    } catch (error) {\n      console.error('Clerk webhook error:', error)\n      return new Response('Webhook verification failed', { status: 400 })\n    }\n  }),\n})\n\nexport default http\n```\n\n```typescript\n\u002F\u002F convex\u002Fclerk.ts\n'use node'\n\nimport { internalAction, internalMutation } from '.\u002F_generated\u002Fserver'\nimport { internal } from '.\u002F_generated\u002Fapi'\nimport { v } from 'convex\u002Fvalues'\nimport { Webhook } from 'svix'\n\nexport const verifyAndProcess = internalAction({\n  args: {\n    body: v.string(),\n    svixId: v.string(),\n    svixTimestamp: v.string(),\n    svixSignature: v.string(),\n  },\n  returns: v.null(),\n  handler: async (ctx, args) => {\n    const webhookSecret = process.env.CLERK_WEBHOOK_SECRET!\n    const wh = new Webhook(webhookSecret)\n\n    const event = wh.verify(args.body, {\n      'svix-id': args.svixId,\n      'svix-timestamp': args.svixTimestamp,\n      'svix-signature': args.svixSignature,\n    }) as { type: string; data: Record\u003Cstring, unknown> }\n\n    switch (event.type) {\n      case 'user.created':\n        await ctx.runMutation(internal.users.create, {\n          clerkId: event.data.id as string,\n          email: (\n            event.data.email_addresses as Array\u003C{ email_address: string }>\n          )[0]?.email_address,\n          name: `${event.data.first_name} ${event.data.last_name}`,\n        })\n        break\n\n      case 'user.updated':\n        await ctx.runMutation(internal.users.update, {\n          clerkId: event.data.id as string,\n          email: (\n            event.data.email_addresses as Array\u003C{ email_address: string }>\n          )[0]?.email_address,\n          name: `${event.data.first_name} ${event.data.last_name}`,\n        })\n        break\n\n      case 'user.deleted':\n        await ctx.runMutation(internal.users.remove, {\n          clerkId: event.data.id as string,\n        })\n        break\n    }\n\n    return null\n  },\n})\n```\n\n### Schema for HTTP API\n\n```typescript\n\u002F\u002F convex\u002Fschema.ts\nimport { defineSchema, defineTable } from 'convex\u002Fserver'\nimport { v } from 'convex\u002Fvalues'\n\nexport default defineSchema({\n  apiKeys: defineTable({\n    key: v.string(),\n    userId: v.id('users'),\n    name: v.string(),\n    createdAt: v.number(),\n    lastUsedAt: v.optional(v.number()),\n    revokedAt: v.optional(v.number()),\n  })\n    .index('by_key', ['key'])\n    .index('by_user', ['userId']),\n\n  webhookEvents: defineTable({\n    source: v.string(),\n    eventType: v.string(),\n    payload: v.any(),\n    processedAt: v.number(),\n    status: v.union(v.literal('success'), v.literal('failed')),\n    error: v.optional(v.string()),\n  })\n    .index('by_source', ['source'])\n    .index('by_status', ['status']),\n\n  users: defineTable({\n    clerkId: v.string(),\n    email: v.string(),\n    name: v.string(),\n  }).index('by_clerk_id', ['clerkId']),\n})\n```\n\n## Best Practices\n\n- Never run `npx convex deploy` unless explicitly instructed\n- Never run any git commands unless explicitly instructed\n- Always validate and sanitize incoming request data\n- Use internal functions for database operations\n- Implement proper error handling with appropriate status codes\n- Add CORS headers for browser-accessible endpoints\n- Verify webhook signatures before processing\n- Log webhook events for debugging\n- Use environment variables for secrets\n- Handle timeouts gracefully\n\n## Common Pitfalls\n\n1. **Missing CORS preflight handler** - Browsers send OPTIONS requests first\n2. **Not validating webhook signatures** - Security vulnerability\n3. **Exposing internal functions** - Use internal functions from HTTP actions\n4. **Forgetting Content-Type headers** - Clients may not parse responses correctly\n5. **Not handling request body errors** - Invalid JSON will throw\n6. **Blocking on long operations** - Use scheduled functions for heavy processing\n\n## References\n\n- Convex Documentation: https:\u002F\u002Fdocs.convex.dev\u002F\n- Convex LLMs.txt: https:\u002F\u002Fdocs.convex.dev\u002Fllms.txt\n- HTTP Actions: https:\u002F\u002Fdocs.convex.dev\u002Ffunctions\u002Fhttp-actions\n- Actions: https:\u002F\u002Fdocs.convex.dev\u002Ffunctions\u002Factions\n- Authentication: https:\u002F\u002Fdocs.convex.dev\u002Fauth\n","# Convex HTTP Actions\n\nBuild HTTP endpoints for webhooks, external API integrations, and custom routes in Convex applications.\n\n## Documentation Sources\n\nBefore implementing, do not assume; fetch the latest documentation:\n\n- Primary: https:\u002F\u002Fdocs.convex.dev\u002Ffunctions\u002Fhttp-actions\n- Actions Overview: https:\u002F\u002Fdocs.convex.dev\u002Ffunctions\u002Factions\n- Authentication: https:\u002F\u002Fdocs.convex.dev\u002Fauth\n- For broader context: https:\u002F\u002Fdocs.convex.dev\u002Fllms.txt\n\n## Instructions\n\n### HTTP Actions Overview\n\nHTTP actions allow you to define HTTP endpoints in Convex that can:\n\n- Receive webhooks from third-party services\n- Create custom API routes\n- Handle file uploads\n- Integrate with external services\n- Serve dynamic content\n\n### Basic HTTP Router Setup\n\n```typescript\n\u002F\u002F convex\u002Fhttp.ts\nimport { httpRouter } from 'convex\u002Fserver'\nimport { httpAction } from '.\u002F_generated\u002Fserver'\n\nconst http = httpRouter()\n\n\u002F\u002F Simple GET endpoint\nhttp.route({\n  path: '\u002Fhealth',\n  method: 'GET',\n  handler: httpAction(async (ctx, request) => {\n    return new Response(JSON.stringify({ status: 'ok' }), {\n      status: 200,\n      headers: { 'Content-Type': 'application\u002Fjson' },\n    })\n  }),\n})\n\nexport default http\n```\n\n### Request Handling\n\n```typescript\n\u002F\u002F convex\u002Fhttp.ts\nimport { httpRouter } from 'convex\u002Fserver'\nimport { httpAction } from '.\u002F_generated\u002Fserver'\n\nconst http = httpRouter()\n\n\u002F\u002F Handle JSON body\nhttp.route({\n  path: '\u002Fapi\u002Fdata',\n  method: 'POST',\n  handler: httpAction(async (ctx, request) => {\n    \u002F\u002F Parse JSON body\n    const body = await request.json()\n\n    \u002F\u002F Access headers\n    const authHeader = request.headers.get('Authorization')\n\n    \u002F\u002F Access URL parameters\n    const url = new URL(request.url)\n    const queryParam = url.searchParams.get('filter')\n\n    return new Response(\n      JSON.stringify({ received: body, filter: queryParam }),\n      {\n        status: 200,\n        headers: { 'Content-Type': 'application\u002Fjson' },\n      },\n    )\n  }),\n})\n\n\u002F\u002F Handle form data\nhttp.route({\n  path: '\u002Fapi\u002Fform',\n  method: 'POST',\n  handler: httpAction(async (ctx, request) => {\n    const formData = await request.formData()\n    const name = formData.get('name')\n    const email = formData.get('email')\n\n    return new Response(JSON.stringify({ name, email }), {\n      status: 200,\n      headers: { 'Content-Type': 'application\u002Fjson' },\n    })\n  }),\n})\n\n\u002F\u002F Handle raw bytes\nhttp.route({\n  path: '\u002Fapi\u002Fupload',\n  method: 'POST',\n  handler: httpAction(async (ctx, request) => {\n    const bytes = await request.bytes()\n    const contentType =\n      request.headers.get('Content-Type') ?? 'application\u002Foctet-stream'\n\n    \u002F\u002F Store in Convex storage\n    const blob = new Blob([bytes], { type: contentType })\n    const storageId = await ctx.storage.store(blob)\n\n    return new Response(JSON.stringify({ storageId }), {\n      status: 200,\n      headers: { 'Content-Type': 'application\u002Fjson' },\n    })\n  }),\n})\n\nexport default http\n```\n\n### Path Parameters\n\nUse path prefix matching for dynamic routes:\n\n```typescript\n\u002F\u002F convex\u002Fhttp.ts\nimport { httpRouter } from 'convex\u002Fserver'\nimport { httpAction } from '.\u002F_generated\u002Fserver'\n\nconst http = httpRouter()\n\n\u002F\u002F Match \u002Fapi\u002Fusers\u002F* with pathPrefix\nhttp.route({\n  pathPrefix: '\u002Fapi\u002Fusers\u002F',\n  method: 'GET',\n  handler: httpAction(async (ctx, request) => {\n    const url = new URL(request.url)\n    \u002F\u002F Extract user ID from path: \u002Fapi\u002Fusers\u002F123 -> \"123\"\n    const userId = url.pathname.replace('\u002Fapi\u002Fusers\u002F', '')\n\n    return new Response(JSON.stringify({ userId }), {\n      status: 200,\n      headers: { 'Content-Type': 'application\u002Fjson' },\n    })\n  }),\n})\n\nexport default http\n```\n\n### CORS Configuration\n\n```typescript\n\u002F\u002F convex\u002Fhttp.ts\nimport { httpRouter } from 'convex\u002Fserver'\nimport { httpAction } from '.\u002F_generated\u002Fserver'\n\nconst http = httpRouter()\n\n\u002F\u002F CORS headers helper\nconst corsHeaders = {\n  'Access-Control-Allow-Origin': '*',\n  'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',\n  'Access-Control-Allow-Headers': 'Content-Type, Authorization',\n  'Access-Control-Max-Age': '86400',\n}\n\n\u002F\u002F Handle preflight requests\nhttp.route({\n  path: '\u002Fapi\u002Fdata',\n  method: 'OPTIONS',\n  handler: httpAction(async () => {\n    return new Response(null, {\n      status: 204,\n      headers: corsHeaders,\n    })\n  }),\n})\n\n\u002F\u002F Actual endpoint with CORS\nhttp.route({\n  path: '\u002Fapi\u002Fdata',\n  method: 'POST',\n  handler: httpAction(async (ctx, request) => {\n    const body = await request.json()\n\n    return new Response(JSON.stringify({ success: true, data: body }), {\n      status: 200,\n      headers: {\n        'Content-Type': 'application\u002Fjson',\n        ...corsHeaders,\n      },\n    })\n  }),\n})\n\nexport default http\n```\n\n### Webhook Handling\n\n```typescript\n\u002F\u002F convex\u002Fhttp.ts\nimport { httpRouter } from 'convex\u002Fserver'\nimport { httpAction } from '.\u002F_generated\u002Fserver'\nimport { internal } from '.\u002F_generated\u002Fapi'\n\nconst http = httpRouter()\n\n\u002F\u002F Stripe webhook\nhttp.route({\n  path: '\u002Fwebhooks\u002Fstripe',\n  method: 'POST',\n  handler: httpAction(async (ctx, request) => {\n    const signature = request.headers.get('stripe-signature')\n    if (!signature) {\n      return new Response('Missing signature', { status: 400 })\n    }\n\n    const body = await request.text()\n\n    \u002F\u002F Verify webhook signature (in action with Node.js)\n    try {\n      await ctx.runAction(internal.stripe.verifyAndProcessWebhook, {\n        body,\n        signature,\n      })\n      return new Response('OK', { status: 200 })\n    } catch (error) {\n      console.error('Webhook error:', error)\n      return new Response('Webhook error', { status: 400 })\n    }\n  }),\n})\n\n\u002F\u002F GitHub webhook\nhttp.route({\n  path: '\u002Fwebhooks\u002Fgithub',\n  method: 'POST',\n  handler: httpAction(async (ctx, request) => {\n    const event = request.headers.get('X-GitHub-Event')\n    const signature = request.headers.get('X-Hub-Signature-256')\n\n    if (!signature) {\n      return new Response('Missing signature', { status: 400 })\n    }\n\n    const body = await request.text()\n\n    await ctx.runAction(internal.github.processWebhook, {\n      event: event ?? 'unknown',\n      body,\n      signature,\n    })\n\n    return new Response('OK', { status: 200 })\n  }),\n})\n\nexport default http\n```\n\n### Webhook Signature Verification\n\n```typescript\n\u002F\u002F convex\u002Fstripe.ts\n'use node'\n\nimport { internalAction, internalMutation } from '.\u002F_generated\u002Fserver'\nimport { internal } from '.\u002F_generated\u002Fapi'\nimport { v } from 'convex\u002Fvalues'\nimport Stripe from 'stripe'\n\nconst stripe = new Stripe(process.env.STRIPE_SECRET_KEY!)\n\nexport const verifyAndProcessWebhook = internalAction({\n  args: {\n    body: v.string(),\n    signature: v.string(),\n  },\n  returns: v.null(),\n  handler: async (ctx, args) => {\n    const webhookSecret = process.env.STRIPE_WEBHOOK_SECRET!\n\n    \u002F\u002F Verify signature\n    const event = stripe.webhooks.constructEvent(\n      args.body,\n      args.signature,\n      webhookSecret,\n    )\n\n    \u002F\u002F Process based on event type\n    switch (event.type) {\n      case 'checkout.session.completed':\n        await ctx.runMutation(internal.payments.handleCheckoutComplete, {\n          sessionId: event.data.object.id,\n          customerId: event.data.object.customer as string,\n        })\n        break\n\n      case 'customer.subscription.updated':\n        await ctx.runMutation(internal.subscriptions.handleUpdate, {\n          subscriptionId: event.data.object.id,\n          status: event.data.object.status,\n        })\n        break\n    }\n\n    return null\n  },\n})\n```\n\n### Authentication in HTTP Actions\n\n```typescript\n\u002F\u002F convex\u002Fhttp.ts\nimport { httpRouter } from 'convex\u002Fserver'\nimport { httpAction } from '.\u002F_generated\u002Fserver'\nimport { internal } from '.\u002F_generated\u002Fapi'\n\nconst http = httpRouter()\n\n\u002F\u002F API key authentication\nhttp.route({\n  path: '\u002Fapi\u002Fprotected',\n  method: 'GET',\n  handler: httpAction(async (ctx, request) => {\n    const apiKey = request.headers.get('X-API-Key')\n\n    if (!apiKey) {\n      return new Response(JSON.stringify({ error: 'Missing API key' }), {\n        status: 401,\n        headers: { 'Content-Type': 'application\u002Fjson' },\n      })\n    }\n\n    \u002F\u002F Validate API key\n    const isValid = await ctx.runQuery(internal.auth.validateApiKey, {\n      apiKey,\n    })\n\n    if (!isValid) {\n      return new Response(JSON.stringify({ error: 'Invalid API key' }), {\n        status: 403,\n        headers: { 'Content-Type': 'application\u002Fjson' },\n      })\n    }\n\n    \u002F\u002F Process authenticated request\n    const data = await ctx.runQuery(internal.data.getProtectedData, {})\n\n    return new Response(JSON.stringify(data), {\n      status: 200,\n      headers: { 'Content-Type': 'application\u002Fjson' },\n    })\n  }),\n})\n\n\u002F\u002F Bearer token authentication\nhttp.route({\n  path: '\u002Fapi\u002Fuser',\n  method: 'GET',\n  handler: httpAction(async (ctx, request) => {\n    const authHeader = request.headers.get('Authorization')\n\n    if (!authHeader?.startsWith('Bearer ')) {\n      return new Response(\n        JSON.stringify({ error: 'Missing or invalid Authorization header' }),\n        { status: 401, headers: { 'Content-Type': 'application\u002Fjson' } },\n      )\n    }\n\n    const token = authHeader.slice(7)\n\n    \u002F\u002F Validate token and get user\n    const user = await ctx.runQuery(internal.auth.validateToken, { token })\n\n    if (!user) {\n      return new Response(JSON.stringify({ error: 'Invalid token' }), {\n        status: 403,\n        headers: { 'Content-Type': 'application\u002Fjson' },\n      })\n    }\n\n    return new Response(JSON.stringify(user), {\n      status: 200,\n      headers: { 'Content-Type': 'application\u002Fjson' },\n    })\n  }),\n})\n\nexport default http\n```\n\n### Calling Mutations and Queries\n\n```typescript\n\u002F\u002F convex\u002Fhttp.ts\nimport { httpRouter } from 'convex\u002Fserver'\nimport { httpAction } from '.\u002F_generated\u002Fserver'\nimport { api, internal } from '.\u002F_generated\u002Fapi'\n\nconst http = httpRouter()\n\nhttp.route({\n  path: '\u002Fapi\u002Fitems',\n  method: 'POST',\n  handler: httpAction(async (ctx, request) => {\n    const body = await request.json()\n\n    \u002F\u002F Call a mutation\n    const itemId = await ctx.runMutation(internal.items.create, {\n      name: body.name,\n      description: body.description,\n    })\n\n    \u002F\u002F Query the created item\n    const item = await ctx.runQuery(internal.items.get, { id: itemId })\n\n    return new Response(JSON.stringify(item), {\n      status: 201,\n      headers: { 'Content-Type': 'application\u002Fjson' },\n    })\n  }),\n})\n\nhttp.route({\n  path: '\u002Fapi\u002Fitems',\n  method: 'GET',\n  handler: httpAction(async (ctx, request) => {\n    const url = new URL(request.url)\n    const limit = parseInt(url.searchParams.get('limit') ?? '10')\n\n    const items = await ctx.runQuery(internal.items.list, { limit })\n\n    return new Response(JSON.stringify(items), {\n      status: 200,\n      headers: { 'Content-Type': 'application\u002Fjson' },\n    })\n  }),\n})\n\nexport default http\n```\n\n### Error Handling\n\n```typescript\n\u002F\u002F convex\u002Fhttp.ts\nimport { httpRouter } from 'convex\u002Fserver'\nimport { httpAction } from '.\u002F_generated\u002Fserver'\n\nconst http = httpRouter()\n\n\u002F\u002F Helper for JSON responses\nfunction jsonResponse(data: unknown, status = 200) {\n  return new Response(JSON.stringify(data), {\n    status,\n    headers: { 'Content-Type': 'application\u002Fjson' },\n  })\n}\n\n\u002F\u002F Helper for error responses\nfunction errorResponse(message: string, status: number) {\n  return jsonResponse({ error: message }, status)\n}\n\nhttp.route({\n  path: '\u002Fapi\u002Fprocess',\n  method: 'POST',\n  handler: httpAction(async (ctx, request) => {\n    try {\n      \u002F\u002F Validate content type\n      const contentType = request.headers.get('Content-Type')\n      if (!contentType?.includes('application\u002Fjson')) {\n        return errorResponse('Content-Type must be application\u002Fjson', 415)\n      }\n\n      \u002F\u002F Parse body\n      let body\n      try {\n        body = await request.json()\n      } catch {\n        return errorResponse('Invalid JSON body', 400)\n      }\n\n      \u002F\u002F Validate required fields\n      if (!body.data) {\n        return errorResponse('Missing required field: data', 400)\n      }\n\n      \u002F\u002F Process request\n      const result = await ctx.runMutation(internal.process.handle, {\n        data: body.data,\n      })\n\n      return jsonResponse({ success: true, result }, 200)\n    } catch (error) {\n      console.error('Processing error:', error)\n      return errorResponse('Internal server error', 500)\n    }\n  }),\n})\n\nexport default http\n```\n\n### File Downloads\n\n```typescript\n\u002F\u002F convex\u002Fhttp.ts\nimport { httpRouter } from 'convex\u002Fserver'\nimport { httpAction } from '.\u002F_generated\u002Fserver'\nimport { Id } from '.\u002F_generated\u002FdataModel'\n\nconst http = httpRouter()\n\nhttp.route({\n  pathPrefix: '\u002Ffiles\u002F',\n  method: 'GET',\n  handler: httpAction(async (ctx, request) => {\n    const url = new URL(request.url)\n    const fileId = url.pathname.replace('\u002Ffiles\u002F', '') as Id\u003C'_storage'>\n\n    \u002F\u002F Get file URL from storage\n    const fileUrl = await ctx.storage.getUrl(fileId)\n\n    if (!fileUrl) {\n      return new Response('File not found', { status: 404 })\n    }\n\n    \u002F\u002F Redirect to the file URL\n    return Response.redirect(fileUrl, 302)\n  }),\n})\n\nexport default http\n```\n\n## Examples\n\n### Complete Webhook Integration\n\n```typescript\n\u002F\u002F convex\u002Fhttp.ts\nimport { httpRouter } from 'convex\u002Fserver'\nimport { httpAction } from '.\u002F_generated\u002Fserver'\nimport { internal } from '.\u002F_generated\u002Fapi'\n\nconst http = httpRouter()\n\n\u002F\u002F Clerk webhook for user sync\nhttp.route({\n  path: '\u002Fwebhooks\u002Fclerk',\n  method: 'POST',\n  handler: httpAction(async (ctx, request) => {\n    const svixId = request.headers.get('svix-id')\n    const svixTimestamp = request.headers.get('svix-timestamp')\n    const svixSignature = request.headers.get('svix-signature')\n\n    if (!svixId || !svixTimestamp || !svixSignature) {\n      return new Response('Missing Svix headers', { status: 400 })\n    }\n\n    const body = await request.text()\n\n    try {\n      await ctx.runAction(internal.clerk.verifyAndProcess, {\n        body,\n        svixId,\n        svixTimestamp,\n        svixSignature,\n      })\n      return new Response('OK', { status: 200 })\n    } catch (error) {\n      console.error('Clerk webhook error:', error)\n      return new Response('Webhook verification failed', { status: 400 })\n    }\n  }),\n})\n\nexport default http\n```\n\n```typescript\n\u002F\u002F convex\u002Fclerk.ts\n'use node'\n\nimport { internalAction, internalMutation } from '.\u002F_generated\u002Fserver'\nimport { internal } from '.\u002F_generated\u002Fapi'\nimport { v } from 'convex\u002Fvalues'\nimport { Webhook } from 'svix'\n\nexport const verifyAndProcess = internalAction({\n  args: {\n    body: v.string(),\n    svixId: v.string(),\n    svixTimestamp: v.string(),\n    svixSignature: v.string(),\n  },\n  returns: v.null(),\n  handler: async (ctx, args) => {\n    const webhookSecret = process.env.CLERK_WEBHOOK_SECRET!\n    const wh = new Webhook(webhookSecret)\n\n    const event = wh.verify(args.body, {\n      'svix-id': args.svixId,\n      'svix-timestamp': args.svixTimestamp,\n      'svix-signature': args.svixSignature,\n    }) as { type: string; data: Record\u003Cstring, unknown> }\n\n    switch (event.type) {\n      case 'user.created':\n        await ctx.runMutation(internal.users.create, {\n          clerkId: event.data.id as string,\n          email: (\n            event.data.email_addresses as Array\u003C{ email_address: string }>\n          )[0]?.email_address,\n          name: `${event.data.first_name} ${event.data.last_name}`,\n        })\n        break\n\n      case 'user.updated':\n        await ctx.runMutation(internal.users.update, {\n          clerkId: event.data.id as string,\n          email: (\n            event.data.email_addresses as Array\u003C{ email_address: string }>\n          )[0]?.email_address,\n          name: `${event.data.first_name} ${event.data.last_name}`,\n        })\n        break\n\n      case 'user.deleted':\n        await ctx.runMutation(internal.users.remove, {\n          clerkId: event.data.id as string,\n        })\n        break\n    }\n\n    return null\n  },\n})\n```\n\n### Schema for HTTP API\n\n```typescript\n\u002F\u002F convex\u002Fschema.ts\nimport { defineSchema, defineTable } from 'convex\u002Fserver'\nimport { v } from 'convex\u002Fvalues'\n\nexport default defineSchema({\n  apiKeys: defineTable({\n    key: v.string(),\n    userId: v.id('users'),\n    name: v.string(),\n    createdAt: v.number(),\n    lastUsedAt: v.optional(v.number()),\n    revokedAt: v.optional(v.number()),\n  })\n    .index('by_key', ['key'])\n    .index('by_user', ['userId']),\n\n  webhookEvents: defineTable({\n    source: v.string(),\n    eventType: v.string(),\n    payload: v.any(),\n    processedAt: v.number(),\n    status: v.union(v.literal('success'), v.literal('failed')),\n    error: v.optional(v.string()),\n  })\n    .index('by_source', ['source'])\n    .index('by_status', ['status']),\n\n  users: defineTable({\n    clerkId: v.string(),\n    email: v.string(),\n    name: v.string(),\n  }).index('by_clerk_id', ['clerkId']),\n})\n```\n\n## Best Practices\n\n- Never run `npx convex deploy` unless explicitly instructed\n- Never run any git commands unless explicitly instructed\n- Always validate and sanitize incoming request data\n- Use internal functions for database operations\n- Implement proper error handling with appropriate status codes\n- Add CORS headers for browser-accessible endpoints\n- Verify webhook signatures before processing\n- Log webhook events for debugging\n- Use environment variables for secrets\n- Handle timeouts gracefully\n\n## Common Pitfalls\n\n1. **Missing CORS preflight handler** - Browsers send OPTIONS requests first\n2. **Not validating webhook signatures** - Security vulnerability\n3. **Exposing internal functions** - Use internal functions from HTTP actions\n4. **Forgetting Content-Type headers** - Clients may not parse responses correctly\n5. **Not handling request body errors** - Invalid JSON will throw\n6. **Blocking on long operations** - Use scheduled functions for heavy processing\n\n## References\n\n- Convex Documentation: https:\u002F\u002Fdocs.convex.dev\u002F\n- Convex LLMs.txt: https:\u002F\u002Fdocs.convex.dev\u002Fllms.txt\n- HTTP Actions: https:\u002F\u002Fdocs.convex.dev\u002Ffunctions\u002Fhttp-actions\n- Actions: https:\u002F\u002Fdocs.convex.dev\u002Ffunctions\u002Factions\n- Authentication: https:\u002F\u002Fdocs.convex.dev\u002Fauth\n",{"name":5,"tags":49,"author":50,"version":51,"description":7,"displayName":52},[29,30,31,42,38,43],"Convex","1.0.0","Convex HTTP Actions","main","83212e6d909b6a0fe9e93fd5a3ff1723300f5b5b",".claude\u002Fskills\u002Fconvex-http-actions","https:\u002F\u002Fgithub.com\u002Fbaisharg\u002FASTN","https:\u002F\u002Fsafetytalent.org","https:\u002F\u002Fgithub.com\u002Fbaisharg","2026-11-13 23:47:58",{"source":8,"skillName":5,"commands":61,"prompt":71,"zipAvailable":72},[62,65,68],{"tool":63,"command":64},"npx","npx skills add baisharg\u002FASTN --skill convex-http-actions",{"tool":66,"command":67},"bunx","bunx skills add baisharg\u002FASTN --skill convex-http-actions",{"tool":69,"command":70},"pnpm","pnpm dlx skills add baisharg\u002FASTN --skill convex-http-actions","安装技能，技能地址：https:\u002F\u002Fgithub.com\u002Fbaisharg\u002FASTN\u002Fblob\u002Fmain\u002F.claude\u002Fskills\u002Fconvex-http-actions\u002FSKILL.md",true,[74,80,86,92,96],{"path":75,"resourceType":76,"resourceTypeDesc":77,"sizeBytes":78,"sourceHtmlUrl":79},"kendex.settings.toml.example","OTHER","其他",320,"https:\u002F\u002Fgithub.com\u002Fvanillagreencom\u002Fkendex\u002Fblob\u002Fmain\u002Fskills\u002Fdeep-research\u002Fkendex.settings.toml.example",{"path":81,"resourceType":82,"resourceTypeDesc":83,"sizeBytes":84,"sourceHtmlUrl":85},"README.md","REFERENCE","参考文档",1268,"https:\u002F\u002Fgithub.com\u002Fvanillagreencom\u002Fkendex\u002Fblob\u002Fmain\u002Fskills\u002Fdeep-research\u002FREADME.md",{"path":87,"resourceType":88,"resourceTypeDesc":89,"sizeBytes":90,"sourceHtmlUrl":91},"scripts\u002Fdeep-research","SCRIPT","脚本",41855,"https:\u002F\u002Fgithub.com\u002Fvanillagreencom\u002Fkendex\u002Fblob\u002Fmain\u002Fskills\u002Fdeep-research\u002Fscripts\u002Fdeep-research",{"path":93,"resourceType":82,"resourceTypeDesc":83,"sizeBytes":94,"sourceHtmlUrl":95},"templates\u002Ffindings.md",551,"https:\u002F\u002Fgithub.com\u002Fvanillagreencom\u002Fkendex\u002Fblob\u002Fmain\u002Fskills\u002Fdeep-research\u002Ftemplates\u002Ffindings.md",{"path":97,"resourceType":88,"resourceTypeDesc":89,"sizeBytes":98,"sourceHtmlUrl":99},"tests\u002Fdeep-research.test.mjs",45997,"https:\u002F\u002Fgithub.com\u002Fvanillagreencom\u002Fkendex\u002Fblob\u002Fmain\u002Fskills\u002Fdeep-research\u002Ftests\u002Fdeep-research.test.mjs",[101,120,133,147,161,178],{"skillNo":102,"name":103,"nameZh":6,"description":104,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":105,"categories":106,"keywords":110,"publishedAt":44},"01M6CCE5DVER8DQWJDY51HAJF7","schema-builder","Design and generate Convex database schemas with proper validation, indexes, and relationships. Use when creating schema.ts or modifying table definitions.","https:\u002F\u002Fgithub.com\u002Fbaisharg\u002FASTN\u002Fblob\u002Fmain\u002F.claude\u002Fskills\u002Fschema-builder\u002FSKILL.md",[107],{"categoryNo":108,"categoryName":109,"taxonomyType":21,"taxonomyTypeDesc":22},"01JBSEEDCAT000000000000015","视觉设计",[111,112,113,114,35,115,116,117,118,29,119],"schema builder","schema","builder","indexes","use","table","database","design","generate",{"skillNo":121,"name":122,"nameZh":6,"description":123,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":124,"categories":125,"keywords":126,"publishedAt":44},"01M6CCE36AA57CG36R4VHKFRXV","integration-tanstack-start","PostHog integration for TanStack Start full-stack applications","https:\u002F\u002Fgithub.com\u002Fbaisharg\u002FASTN\u002Fblob\u002Fmain\u002F.claude\u002Fskills\u002Fposthog-integration-tanstack-start\u002FSKILL.md",[],[36,127,128,129,130,131,132],"start","tanstack","integration tanstack start","posthog","applications","stack",{"skillNo":134,"name":135,"nameZh":6,"description":136,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":137,"categories":138,"keywords":139,"publishedAt":44},"01M6CCE0FK6N8XAB093X7TTQ4F","migration-helper","Plan and execute Convex schema migrations safely, including adding fields, creating tables, and data transformations. Use when schema changes affect existing data.","https:\u002F\u002Fgithub.com\u002Fbaisharg\u002FASTN\u002Fblob\u002Fmain\u002F.claude\u002Fskills\u002Fmigration-helper\u002FSKILL.md",[],[140,141,142,143,144,145,115,112,146,29],"migration","migration helper","schema migrations safely","data","plan","execute","migrations",{"skillNo":148,"name":149,"nameZh":6,"description":150,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":151,"categories":152,"keywords":153,"publishedAt":44},"01M6CCDY3644G8WXXP0BDNMZPR","gsd:sprint","Autonomous phase execution with Codex validation. Runs plan\u002Fexecute\u002Fverify cycles in a bash loop until done or halted.","https:\u002F\u002Fgithub.com\u002Fbaisharg\u002FASTN\u002Fblob\u002Fmain\u002F.claude\u002Fskills\u002Fgsd-sprint\u002FSKILL.md",[],[149,154,155,35,127,156,157,158,159,144,160,145],"resume","codex","execution","verify","Bash","loop","autonomous",{"skillNo":162,"name":163,"nameZh":6,"description":164,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":165,"categories":166,"keywords":167,"publishedAt":44},"01M6CCDVTZRTFWWGREMV6FF956","gsd:second-opinion","Get a second opinion from OpenAI Codex (GPT-5.2-codex) on code, architecture, or implementation decisions. Use when you want independent verification or an alternative perspective.","https:\u002F\u002Fgithub.com\u002Fbaisharg\u002FASTN\u002Fblob\u002Fmain\u002F.claude\u002Fskills\u002Fgsd-second-opinion\u002FSKILL.md",[],[168,169,170,155,171,172,173,174,175,176,115,177],"gsd:second opinion","second opinion","question","architecture","code","verification","openai","gpt","decisions","implementation",{"skillNo":179,"name":180,"nameZh":6,"description":181,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":182,"categories":183,"keywords":184,"publishedAt":44},"01M6CCDSDPYYNEDERHP01N30PT","gsd:milestone-sprint","Run entire milestone autonomously with Codex validation. Auto-detects current milestone, executes all phases, and runs audit.","https:\u002F\u002Fgithub.com\u002Fbaisharg\u002FASTN\u002Fblob\u002Fmain\u002F.claude\u002Fskills\u002Fgsd-milestone-sprint\u002FSKILL.md",[],[185,186,35,187,188,189,154,155],"gsd:milestone sprint","run","audit","interactive","auto"]