convex-http-actions — скилл для Claude Code: HTTP-эндпоинты в Convex
Добавляет HTTP-эндпоинты в convex/http.ts: приём вебхуков с проверкой подписи, REST-маршруты, CORS, стриминг и загрузка файлов.
npx skills add waynesutton/builder-skills --skill convex-http-actions -a claude-codeЧто делает convex-http-actions
Скилл описывает, как устроены публичные HTTP-эндпоинты Convex. Они живут на домене развёртывания .convex.site, а не .convex.cloud. Главное правило: обработчик httpAction считает каждый байт запроса недоверенным, проверяет его и только потом вызывает internal.* функции через ctx.runQuery или ctx.runMutation. К ctx.db он не обращается напрямую.
Роутер обязан лежать в convex/http.ts и быть экспортом по умолчанию — файл с другим именем просто проигнорируется. Внутри доступны ctx.runQuery, ctx.runMutation, ctx.runAction, ctx.storage, ctx.scheduler и ctx.auth. Аргументы всё равно проходят валидаторы целевой функции, так что кривая форма данных упадёт там с понятной ошибкой.
Для вебхуков тело читается один раз через request.text(): если сначала распарсить JSON, байты изменятся и подпись не сойдётся. Провайдеры повторяют доставку при любом ответе вне 2xx, поэтому события нужно дедуплицировать по id внутри мутации. Секрет вебхука берётся из переменной окружения — без неё обработчик вернёт 500. Тела запроса и ответа ограничены 20MB.
Какие задачи решает
- принять вебхук от Stripe и проверить подпись
- отдать внешнему клиенту без Convex SDK обычный HTTP API
- запустить скачивание или стриминг байтов в браузер
- загрузить файл по HTTP вместо upload URL
- убрать повторную обработку одного и того же события
Примеры запросов агенту
- добавь вебхук для Stripe в convex/http.ts с проверкой подписи
- сделай REST-маршрут POST /api/items, который пишет через internalMutation
- настрой дедупликацию событий по eventId в таблице webhookEvents
Как установить convex-http-actions
1Через Skills CLI (Vercel)
npx skills add waynesutton/builder-skills --skill convex-http-actions -a claude-code2Вручную: положить файлы
~/.claude/skills/convex-http-actions/SKILL.mdПапка скилла целиком; для одного проекта — .claude/skills/convex-http-actions/
1Через Skills CLI (Vercel)
npx skills add waynesutton/builder-skills --skill convex-http-actions -a codex2Вручную: положить файлы
.agents/skills/convex-http-actions/SKILL.mdДля всех проектов — ~/.agents/skills/
1Через Skills CLI (Vercel)
npx skills add waynesutton/builder-skills --skill convex-http-actions -a cursor2Вручную: положить файлы
.cursor/skills/convex-http-actions/SKILL.mdCursor читает и .agents/skills/, и .claude/skills/
1GitHub CLI
gh skill install waynesutton/builder-skills convex-http-actions2Вручную: положить файлы
.github/skills/convex-http-actions/SKILL.md1Через Skills CLI (Vercel)
npx skills add waynesutton/builder-skills --skill convex-http-actions -a gemini-cli2Командой платформы
gemini skills install https://github.com/waynesutton/builder-skills --path skills/convex-http-actionsВручную: положить файлы
.devin/skills/convex-http-actions/SKILL.mdСтарый путь .windsurf/skills/ тоже читается
Через Skills CLI (Vercel)
npx skills add waynesutton/builder-skills --skill convex-http-actions -a opencodeClaude.ai
Упакуйте папку в zip и загрузите в Settings → Capabilities → Skills
По шагам, с командами для каждой платформы: Как установить скилл в Claude Code: пять способов
Из того же набора14
Похожие
Рядом в теме «Бэкенд и API»
Всё по теме «Бэкенд и API» →Вопросы о convex-http-actions
Как установить convex-http-actions в Claude?
Выполните npx skills add waynesutton/builder-skills --skill convex-http-actions -a claude-code. Скилл появится в ~/.claude/skills/convex-http-actions/SKILL.md.
Что делает convex-http-actions?
Описывает публичные HTTP-эндпоинты Convex: приём вебхуков с проверкой подписи, REST-маршруты, CORS, заголовки авторизации, стриминг ответов и загрузку файлов по HTTP.
Когда вместо httpAction хватит обычной мутации?
Если запрос шлёт ваше же приложение через convex/react, берите query и mutation. httpAction нужен внешним сервисам и клиентам без Convex SDK.
Исходный текст
SKILL.md · Apache-2.0
---
name: convex-http-actions
description: Adds HTTP endpoints in convex/http.ts: webhook receivers with signature checks, REST style routes, CORS, auth headers, streaming responses, and file uploads over HTTP. Use when integrating Stripe, Clerk, Resend, or any service that calls back into the app, or when a client needs a plain HTTP API.
---
# Convex HTTP actions
Defines public HTTP endpoints served from the deployment's `.convex.site` domain. The one rule: an `httpAction` handler treats every byte of the request as untrusted, verifies it, then calls `internal.*` functions with `ctx.runQuery` or `ctx.runMutation`. It never reads or writes `ctx.db` directly.
## When to reach for this
- A third party (Stripe, Clerk, Resend, GitHub) needs a URL to POST events to
- A client without the Convex SDK (mobile app, CLI, another backend) needs a plain HTTP API
- A browser needs to download or stream bytes without going through `useQuery`
- A script or form needs to upload bytes over HTTP instead of an upload URL
## httpAction or mutation
| Caller | Use |
| --- | --- |
| Your own React or Next.js app using `convex/react` | `query` and `mutation`. Skip HTTP actions. |
| External service sending webhooks | `httpAction` |
| Client that cannot use the Convex SDK | `httpAction` |
| Scheduled or internal work | `internalMutation` or `internalAction` |
HTTP actions get no argument validators and no automatic auth. They run in the default Convex runtime, so Web APIs (`fetch`, `crypto.subtle`, `Response`, `ReadableStream`) work without `"use node"`. Request and response bodies are capped at 20MB.
## Router skeleton
The router must live at `convex/http.ts` and be the default export. A router in any other file is ignored.
```typescript
// convex/http.ts
import { httpRouter } from "convex/server";
import { httpAction } from "./_generated/server";
import { internal } from "./_generated/api";
const http = httpRouter();
http.route({
path: "/api/items",
method: "POST",
handler: httpAction(async (ctx, request) => {
let body: { name?: unknown };
try {
body = await request.json();
} catch {
return json({ error: "Invalid JSON body" }, 400);
}
if (typeof body.name !== "string") {
return json({ error: "name is required" }, 400);
}
const id = await ctx.runMutation(internal.items.create, { name: body.name });
return json({ id }, 201);
}),
});
export default http;
// JSON response helper shared by every route in this file
function json(data: unknown, status = 200): Response {
return new Response(JSON.stringify(data), {
status,
headers: { "Content-Type": "application/json" },
});
}
```
The endpoint is reachable at `https://<deployment>.convex.site/api/items`. Note `.convex.site`, not `.convex.cloud`. `path` matches exactly; use `pathPrefix: "/api/items/"` for dynamic segments and read the tail from `new URL(request.url).pathname`.
## Calling queries and mutations from the handler
`ctx.runQuery`, `ctx.runMutation`, `ctx.runAction`, `ctx.storage`, `ctx.scheduler`, and `ctx.auth` are all available inside `httpAction`. Point them at `internal.*` functions so database logic is not also exposed as a public Convex function. Arguments still pass through the target's validators, so a bad shape fails there with a clear error.
```typescript
// convex/items.ts
import { internalMutation } from "./_generated/server";
import { v } from "convex/values";
export const create = internalMutation({
args: { name: v.string() },
returns: v.id("items"),
handler: async (ctx, args) => {
return await ctx.db.insert("items", { name: args.name });
},
});
```
For slow work (sending email, calling an LLM), respond 200 right away and hand off with `ctx.scheduler.runAfter(0, internal.jobs.process, args)`. Providers time out and retry if the handler stalls.
## Webhook with signature verification
Read the raw body once with `request.text()`. Parsing first changes the bytes and breaks the signature. This generic HMAC SHA-256 pattern covers most providers; Stripe, Clerk, and Resend specifics are in the reference below.
```typescript
// convex/http.ts (add to the router above)
http.route({
path: "/webhooks/provider",
method: "POST",
handler: httpAction(async (ctx, request) => {
const signature = request.headers.get("x-signature");
if (!signature) return new Response("Missing signature", { status: 400 });
const raw = await request.text();
const secret = process.env.PROVIDER_WEBHOOK_SECRET;
if (!secret) return new Response("Webhook secret not configured", { status: 500 });
if (!(await verifyHmac(raw, signature, secret))) {
return new Response("Invalid signature", { status: 401 });
}
const event = JSON.parse(raw) as { id: string; type: string; data: unknown };
// The mutation returns early if this event id was already processed.
await ctx.runMutation(internal.webhooks.record, {
source: "provider",
eventId: event.id,
type: event.type,
payload: event.data,
});
return new Response(null, { status: 200 });
}),
});
async function verifyHmac(payload: string, signature: string, secret: string): Promise<boolean> {
const enc = new TextEncoder();
const key = await crypto.subtle.importKey(
"raw",
enc.encode(secret),
{ name: "HMAC", hash: "SHA-256" },
false,
["sign"],
);
const mac = await crypto.subtle.sign("HMAC", key, enc.encode(payload));
const expected = Array.from(new Uint8Array(mac))
.map((b) => b.toString(16).padStart(2, "0"))
.join("");
return safeEqual(expected, signature);
}
// Constant time compare so response timing does not leak the signature
function safeEqual(a: string, b: string): boolean {
if (a.length !== b.length) return false;
let diff = 0;
for (let i = 0; i < a.length; i++) diff |= a.charCodeAt(i) ^ b.charCodeAt(i);
return diff === 0;
}
```
Providers retry on any non 2xx and sometimes on network flakes, so the same event arrives more than once. Deduplicate by event id inside the mutation, which is a transaction:
```typescript
// convex/webhooks.ts
import { internalMutation } from "./_generated/server";
import { v } from "convex/values";
export const record = internalMutation({
args: { source: v.string(), eventId: v.string(), type: v.string(), payload: v.any() },
returns: v.null(),
handler: async (ctx, args) => {
const seen = await ctx.db
.query("webhookEvents")
.withIndex("by_source_and_event_id", (q) =>
q.eq("source", args.source).eq("eventId", args.eventId),
)
.unique();
if (seen) return null;
await ctx.db.insert("webhookEvents", args);
// Apply the event's side effects here, in the same transaction.
return null;
},
});
```
Schema: `webhookEvents: defineTable({ source: v.string(), eventId: v.string(), type: v.string(), payload: v.any() }).index("by_source_and_event_id", ["source", "eventId"])`.
Open [references/webhooks.md](references/webhooks.md) for Stripe (`constructEvent` in a Node action), Clerk and Resend (svix headers), replay protection with timestamps, and a fuller idempotency table with status tracking.
## CORS, path params, auth headers, streaming, files
Browsers send an `OPTIONS` preflight before cross origin `POST`s, so each browser facing path needs an `OPTIONS` route returning the same `Access-Control-*` headers as the real route. Bearer tokens from your configured auth provider are read with `await ctx.auth.getUserIdentity()`, the same call used in queries. Streaming works by returning `new Response(readableStream)`, and files are served with `ctx.storage.get(storageId)` or accepted with `request.blob()` then `ctx.storage.store(blob)`.
Open [references/rest-and-cors.md](references/rest-and-cors.md) when building a REST style API: path parameters and id validation, the CORS preflight pair, JSON error conventions, bearer and API key auth, streaming responses, and file upload and download routes.
## Common mistakes
| Mistake | Why it breaks | Do instead |
| --- | --- | --- |
| Router in `convex/api.ts` or `convex/routes.ts` | Only `convex/http.ts` is loaded as the router | Keep one `http.ts` with `export default http` |
| Calling `https://<deployment>.convex.cloud/webhooks/...` | HTTP actions live on `.convex.site` | Use the `.convex.site` URL in the provider dashboard |
| `await request.json()` before verifying the signature | The reserialized body no longer matches the signed bytes | `request.text()` once, verify, then `JSON.parse` |
| Trusting the body because "it came from Stripe" | Anyone can POST to a public URL | Verify the signature on every request |
| Using `api.*` targets from `runMutation` | Exposes the same logic twice, once unauthenticated | Target `internal.*` functions |
| No `OPTIONS` route for a browser called endpoint | Preflight fails, the real request never sends | Add an `OPTIONS` route per path with CORS headers |
| Doing minutes of work inside the handler | Provider times out and retries, duplicating work | Return 200 fast, schedule with `ctx.scheduler.runAfter` |
| Processing the same event twice | Providers retry on any non 2xx | Dedupe by event id in the mutation |
| Missing `Content-Type: application/json` on responses | Clients get text they cannot parse | Use a `json()` helper for every JSON response |
## Checklist
- [ ] Router is at `convex/http.ts` and exported as default
- [ ] Every route parses the body inside `try/catch` and returns 400 on bad input
- [ ] Handlers call `internal.*` functions, never `ctx.db`
- [ ] Webhook routes read `request.text()` once and verify the signature before parsing
- [ ] Webhook secrets come from `process.env` and are set in the deployment, not hardcoded
- [ ] Events are deduplicated by provider event id inside a mutation
- [ ] Slow work is scheduled, and the handler returns 2xx quickly
- [ ] Browser called routes have a matching `OPTIONS` route
- [ ] The provider dashboard points at the `.convex.site` URL
- [ ] JSON responses set `Content-Type: application/json`
## Docs
- https://docs.convex.dev/llms.txt
- https://docs.convex.dev/functions/http-actions
- https://docs.convex.dev/auth/clerk
- https://docs.convex.dev/file-storage/serve-files
© waynesutton · Apache-2.0 · waynesutton/builder-skills