httpAction and ctx.db: why it doesn't work
You get a webhook — Moneroo, Stripe, whatever. You want to save the event straight to the database. Makes sense, right?
// What you'd want to do
http.route({
path: "/webhooks/payment",
method: "POST",
handler: httpAction(async (ctx, req) => {
const data = await req.json();
await ctx.db.insert("events", data); // ❌ Error
})
});It breaks. And the error message isn't very clear the first time.
The reason: an httpAction lives outside Convex's transactional system. It can receive HTTP requests, read headers, parse the body — but it can't write to the database directly.
To write, it has to go through a mutation.
http.route({
path: "/webhooks/payment",
method: "POST",
handler: httpAction(async (ctx, req) => {
const rawBody = await req.text();
// Verify the signature first
const signature = req.headers.get("x-moneroo-signature") ?? "";
if (!verifySignature(rawBody, signature)) {
return new Response("Unauthorized", { status: 401 });
}
// Delegate the write to a mutation
await ctx.runMutation(internal.webhooks.handle, JSON.parse(rawBody));
return new Response("OK", { status: 200 });
})
});The division of labor:
— httpAction: receives, verifies, forwards
— mutation: writes to the database, transactionally
Always verify the signature before runMutation. If you delegate first and verify after, you've already written unverified data to the database.