Alerts, notifications, webhooks & log drains
Rules that watch a project and page you by email or webhook, deploy notifications for Slack / Discord / email — plus the signature every outgoing POST carries so your receiver can trust it.
Metrics
Alert rules live on the project's Alerts tab. Each rule has a metric, a threshold, a delivery channel (email or webhook) and a target. Rules are evaluated every minute; a rule that fires will not fire again inside its window (5 minutes by default).
| Metric | Threshold | Fires when |
|---|---|---|
cpu | 1–100 % | CPU used by the project’s pods, as a share of their CPU limit (or request), stays at or above the threshold for the whole window. Sampled every minute from the cluster. |
memory | 1–100 % | Working-set memory as a share of the memory limit (or request) stays at or above the threshold for the whole window. |
restart_loop | count | Container restarts on the production pods reach the threshold. |
deploy_failed | — | The newest deployment failed (once per failure). |
http_5xx_rate | count / min | 5xx responses per minute, averaged over the window, reach the threshold. Measured at the edge for every request to the project’s domains. |
latency_p95 | ms | The p95 response time over the window (edge-measured, request-weighted) reaches this many milliseconds. |
cron_failed | runs in a row | Any cron job in the project has failed this many consecutive runs. The payload names the job, its last HTTP status and error. |
401 or 403 on every tick is the most common silent failure. Buildfyio sends Authorization: Bearer $CRON_SECRET only when that variable exists on the project — if your handler checks it, add CRON_SECRET on the Env variables tab and redeploy. The Cron tab flags any job that has failed five runs in a row and shows this hint; a cron_failed rule with threshold 5 turns it into a page.CPU, memory, 5xx rate and latency are the same numbers the project's Analytics tab charts (last 24 h / 7 d): edge metrics come from the access log of every request to the project's domains, resource metrics from the cluster's per-minute samples.
Deploy notifications
Separate from alert rules, a project can post its deployment lifecycle to a channel. On the project's Settings page, Deploy notifications takes any number of destinations — a Slack incoming webhook, a Discord channel webhook or an email address — each subscribed to deploy.started, deploy.ready and/or deploy.failed. Messages carry the project, environment, branch and commit, the URL on ready and the failure reason on failed, with a link back to the deployment.
- Slack — create an Incoming Webhook in your Slack app and paste the
https://hooks.slack.com/services/…URL. Buildfyio posts a mrkdwn message. - Discord — Channel settings → Integrations → Webhooks → copy the
https://discord.com/api/webhooks/…URL. Buildfyio posts an embed coloured by outcome. - Email — a plain-text message per event to one address.
Webhook payload
Webhook rules POST a JSON object to your URL with content-type: application/json and user-agent: Buildfyio-Alerts/1.0. The fields depend on the metric; every payload carries metric, and a dashboard test carries test: true.
{
"metric": "cron_failed",
"project": "my-shop",
"cronJobId": "cro_…",
"cronJob": "retention-purge",
"path": "/api/cron/retention",
"consecutiveFailures": 12,
"lastStatus": 401,
"lastError": "HTTP 401: Cron secret invalid",
"hint": "The endpoint rejected the call. If it checks Authorization: Bearer $CRON_SECRET, set CRON_SECRET in the project env variables."
}Verifying the signature
Every webhook rule and every log drain has its own signing secret (whsec_…). It is shown once when the rule or drain is created and can be rotated with Regenerate secret; Buildfyio never displays it again. Each delivery carries:
X-Buildfyio-Signature: t=1760000000,v1=5f1c…e9a0
t— Unix time (seconds) when the request was signed.v1— hex HMAC-SHA256, keyed with your secret, over the string<t>.<raw request body>— the timestamp, a literal dot, then the exact bytes of the body.
To verify:
- Read the raw body before any JSON parsing — re-serialising changes the bytes.
- Reject if
tis more than a few minutes from your clock (replay protection). - Recompute the HMAC and compare it to
v1with a constant-time comparison.
import { createHmac, timingSafeEqual } from 'node:crypto';
// Express / Fastify / Next.js route handler — read the RAW body, not a parsed object.
export function verifyBuildfyio(rawBody: string, header: string, secret: string): boolean {
const parts = Object.fromEntries(header.split(',').map((kv) => kv.split('=')));
const t = Number(parts.t);
if (!Number.isInteger(t) || Math.abs(Date.now() / 1000 - t) > 300) return false; // replay window
const expected = createHmac('sha256', secret).update(`${t}.${rawBody}`).digest('hex');
const given = String(parts.v1 ?? '');
return given.length === expected.length && timingSafeEqual(Buffer.from(given), Buffer.from(expected));
}
// app.post('/hooks/buildfyio', express.raw({ type: '*/*' }), (req, res) => {
// if (!verifyBuildfyio(req.body.toString(), req.header('x-buildfyio-signature') ?? '', process.env.BUILDFYIO_WEBHOOK_SECRET!)) {
// return res.status(401).end();
// }
// const event = JSON.parse(req.body.toString());
// ...
// });Log drains
Log drains are configured on the project's Data tab (the Logs page links there). Runtime log lines are batched and POSTed to each drain in the shape its type expects — raw JSON array for http / webhook, Datadog, Axiom and Loki push formats for the others — and the same X-Buildfyio-Signature header is computed over whichever body is sent, with the drain's own secret. Any auth token you store on the drain is still sent as before; the signature is in addition to it.
Outgoing webhooks
Beyond alert rules, a project (or a whole workspace) can register webhook endpoints on the Hooks tab under Outgoing webhooks. Each endpoint picks the events it wants and gets its own whsec_… signing secret, shown once. Every delivery is a JSON POST with content-type: application/json, user-agent: Buildfyio-Webhooks/1.0, the same X-Buildfyio-Signature header described above (verify it exactly the same way), plus X-Buildfyio-Event (the type), X-Buildfyio-Delivery (a unique id — use it to de-duplicate) and X-Buildfyio-Attempt.
| Event | Sent when |
|---|---|
deployment.created | A deployment starts building (once per deployment). |
deployment.ready | The deployment is live; data.deployment.url is its URL. |
deployment.failed | Build or deploy failed; data.deployment.reason says why. |
deployment.cancelled | Cancelled by a user, superseded, or skipped by an ignore rule. |
domain.verified | A custom domain’s DNS now points at Buildfyio. |
domain.failed | A verified domain stopped resolving to us, or a new one is still unverified after an hour. |
alert.fired | Any alert rule of the project fired; data.alert is the alert payload shown above. |
The body is always the same envelope:
{
"type": "deployment.ready",
"createdAt": "2026-10-21T09:12:44.120Z",
"org": { "id": "org_…" },
"project": { "id": "prj_…", "name": "my-shop", "slug": "my-shop" },
"data": {
"deployment": {
"id": "dpl_…",
"status": "ready",
"env": "production",
"branch": "main",
"sha": "36cf1b7…",
"author": "jane",
"message": "fix checkout",
"url": "https://my-shop.buildfyio.com",
"reason": null,
"dashboardUrl": "https://buildfyio.com/dashboard/deployments/dpl_…"
}
}
}- Respond 2xx quickly (within 10 seconds) and do the work afterwards. Any other status, a timeout or a connection error counts as a failed attempt.
- Failed deliveries are retried after 1 min, 5 min, 30 min, 2 h, 6 h and 12 h — seven attempts over roughly 24 hours — then marked failed. The Hooks tab lists every delivery with its status, last response code and error, and a Redeliver button that sends the same payload again as a new delivery.
- Deliveries can arrive out of order and, after a retry, more than once; key your handling on
X-Buildfyio-Deliveryor ondata.deployment.id+type. - Test on an endpoint row sends a
pingevent through the same pipeline so you can check your signature code before anything happens on the project. - Endpoint URLs must be public http(s) hosts — private, loopback and link-local addresses are refused when the endpoint is saved and again on every attempt.