Convex httpAction: why ctx.db is inaccessible and how to work around it

2 min readApril 29, 2026#convex#webhook#httpaction#backend

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.