Sometimes you need to let the outside world call your app — a Stripe webhook, a Zapier trigger, a partner integration. Routes under /api/public/ skip authentication and are designed exactly for this.
/The non-negotiables
- 01Verify signatures: Every webhook provider signs their payload. Verify with the shared secret before you trust a byte.
- 02Validate input: Use a schema (Zod) to reject malformed payloads instead of crashing.
- 03Return quickly: Do the minimum work needed and enqueue the rest — most providers time out at 10 seconds.
- 04Never return PII: Public means public. Assume the response is scraped and logged.
ts
// Verify a webhook signature before processing
const signature = request.headers.get("x-webhook-signature");
const body = await request.text();
const expected = createHmac("sha256", process.env.WEBHOOK_SECRET!)
.update(body)
.digest("hex");
if (!signature || !timingSafeEqual(Buffer.from(signature), Buffer.from(expected))) {
return new Response("Invalid signature", { status: 401 });
}Stable URLs for cron & webhooks
Use your project--<id>.lovable.app URL when configuring external services. It never changes, even if you rename the project.