Frameworks and ORMs
Knocker intentionally does not ship framework adapters. Your app owns HTTP routing and ORM choices; Knocker owns durable webhook storage, verification, dedupe, and later handler dispatch.
Web Frameworks
Section titled “Web Frameworks”A framework adapter would still need to read your framework’s request object, preserve the raw signed body, pass headers through, and return a plain status response. That code is short, app-specific, and easier to audit when it lives next to the route it serves.
Keeping this glue in docs also keeps Knocker’s packages small: no web framework dependency choices leak into the durable webhook core.
The route glue is small in every framework:
- read the raw request body bytes exactly as the provider sent them
- pass headers and query params through to
receive(...) - return
result.status_code
Use receive(...) from your route for normal verified ingress. Use
ingest(...) only when another trusted layer has already decided the
verification outcome.
Route Examples
Section titled “Route Examples”from fastapi import Request, Response
@api.post("/webhooks/provider")async def provider_webhook(request: Request): result = webhooks.receive( endpoint="provider", body=await request.body(), headers=dict(request.headers), query=dict(request.query_params), ) return Response(status_code=result.status_code)-- Bind these from your framework route:-- :body_blob is the exact raw request body bytes.-- :headers_json and :query_json are JSON objects from the request.SELECT knocker_receive( 'provider', 'token-header', json_array('dev-secret'), json_object(), 'POST', :headers_json, :body_blob, :query_json, NULL, NULL, NULL, NULL, 'knocker.events', 3) AS result_json;
-- Return json_extract(result_json, '$.status_code') to the webhook sender.Bun.serve({ async fetch(request) { const url = new URL(request.url); if (request.method !== "POST" || url.pathname !== "/webhooks/provider") { return new Response("not found", { status: 404 }); }
const result = webhooks.receive({ endpoint: "provider", body: Buffer.from(await request.arrayBuffer()), headers: Object.fromEntries(request.headers), query: Object.fromEntries(url.searchParams), });
return new Response(null, { status: Number(result.status_code) }); },});post "/webhooks/provider" do {:ok, body, conn} = Plug.Conn.read_body(conn)
{:ok, result} = KnockerSqlite.receive(webhooks, %{ endpoint: "provider", provider: "token-header", secrets: ["dev-secret"], body: body, headers: Map.new(conn.req_headers), query: conn.query_params })
send_resp(conn, result["status_code"], "")endfunc webhookHandler(w http.ResponseWriter, r *http.Request) { body, err := io.ReadAll(r.Body) if err != nil { http.Error(w, "bad request", http.StatusBadRequest) return }
headers := map[string]any{} for name, values := range r.Header { if len(values) > 0 { headers[name] = values[0] } }
result, err := webhooks.Receive(knockersqlite.ReceiveParams{ Endpoint: "provider", Body: body, Headers: headers, }) if err != nil { http.Error(w, "bad request", http.StatusBadRequest) return }
w.WriteHeader(result.StatusCode)}app.post("/webhooks/provider", express.raw({ type: "*/*" }), (req, res) => { const result = webhooks.receive({ endpoint: "provider", body: req.body, headers: req.headers, query: req.query, });
res.sendStatus(result.status_code);});post "/webhooks/provider" do result = webhooks.receive( endpoint: "provider", body: request.body.read, headers: request.env.select { |key, _| key.start_with?("HTTP_") }, query: params.to_h, )
status result["status_code"]endORMs And App Writes
Section titled “ORMs And App Writes”The handler transaction is the important integration point for ORMs. If your handler writes app state, do it through Knocker’s transaction handle or through an ORM session/connection bound to that same SQLite transaction.
Do not open a second independent SQLite connection inside the handler and expect it to be part of the webhook acknowledgement transaction. That second connection may commit even if the handler later fails.
In the examples below, helper names like insertInvoiceUsingKnockerTx(...)
stand in for your ORM/repository code bound to Knocker’s transaction handle.
The point is the ownership boundary: app writes and Knocker’s handled/ack
transition share one SQLite transaction.
import json
@automation.handle("invoice.created")def handle_invoice(event, tx): payload = json.loads(event.body) tx.execute( "INSERT INTO invoices(provider_id, total_cents) VALUES (?, ?)", [payload["id"], payload["data"]["object"]["amount_due"]], )BEGIN IMMEDIATE;
-- Bindings do this around your handler:SELECT knocker_mark_processing(:event_id, :attempt_count);
-- Your ORM/app write must use this same transaction.INSERT INTO invoices(provider_id, total_cents)VALUES (:provider_id, :total_cents);
SELECT knocker_mark_handled(:event_id, :duration_ms);SELECT honker_ack(:job_id, :worker_id);
COMMIT;webhooks.registerHandler("stripe", (event, tx) => { const payload = JSON.parse(String(event.body_blob)); insertInvoiceUsingKnockerTx(tx, { providerId: payload.id, totalCents: payload.data.object.amount_due, });}, "invoice.created");webhooks = KnockerSqlite.register_handler(webhooks, "stripe", "invoice.created", fn event, tx -> payload = Jason.decode!(event["body_blob"])
insert_invoice_using_knocker_tx(tx, %{ provider_id: payload["id"], total_cents: payload["data"]["object"]["amount_due"] }) end)webhooks.RegisterEventHandler("stripe", "invoice.created", func(event knockersqlite.Event, tx *knockersqlite.Tx) error { var payload StripeInvoiceEvent if err := json.Unmarshal(event.Body, &payload); err != nil { return err } _, err := tx.Exec( "INSERT INTO invoices(provider_id, total_cents) VALUES (?, ?)", payload.ID, payload.Data.Object.AmountDue, ) return err})webhooks.registerHandler("stripe", "invoice.created", (event, tx) => { const payload = JSON.parse(Buffer.from(event.body_blob).toString("utf8")); tx.prepare(` INSERT INTO invoices(provider_id, total_cents) VALUES (?, ?) `).run(payload.id, payload.data.object.amount_due);});webhooks.register_handler("stripe", event_type: "invoice.created") do |event, tx| payload = JSON.parse(event["body_blob"]) insert_invoice_using_knocker_tx( tx, provider_id: payload["id"], total_cents: payload["data"]["object"]["amount_due"], )end