"Alerts"

Alerts tell you when something goes wrong in a project: functions start failing, a cron run fails, or a backup fails. Every alert lands in the dashboard inbox, and can also be sent by email, to your own webhook, or to a Discord channel.

Rules, channels and alert history belong to an environment, so production and development alert independently. Switch environment in the project header to manage each one.

Where to find them

Open a project, go to Settings and choose Alerts. The section has three parts:

  • Inbox: alerts raised in this environment, newest first, with read and unread state
  • Rules: what to watch and where to send it
  • Channels: email, webhook and Discord destinations, each with a Send test alert button

When an environment has unread alerts, a red unread alerts chip appears in the project header. Click it to jump to the inbox.

Rules

Tether checks every enabled rule once a minute.

Rule Fires when Settings
Function errors (count) At least threshold function calls failed within the window Threshold, window (1 to 1440 minutes), optional function name
Function errors (rate) At least threshold percent of calls failed within the window Threshold (%), window, minimum calls, optional function name
Cron failure Any cron run fails Optionally a single cron
Backup failure Any backup (scheduled or manual) fails None

Function rules read the same execution history as the Functions tab. The minimum calls setting stops a rate rule firing on a quiet window, for example one failure out of two calls.

Cron and backup rules alert on each failure once. Failures from before the rule was created are ignored, and several failures found in the same minute are grouped into one alert.

Cooldown

Each rule has a cooldown (default 30 minutes). After a rule fires, it won't fire again until the cooldown has passed, even if the condition is still true. This stops a flapping function or a cron that fails every minute from flooding your inbox.

For cron and backup rules, failures that happen during the cooldown are counted as seen and won't be reported later. Set the cooldown to No cooldown if you want an alert for every failure.

If a function rule's condition is still true when the cooldown ends, it fires again. That repeat is your reminder that the problem hasn't gone away.

Channels

Every alert goes to the inbox. Pick extra channels per rule.

Email

Sends a plain text and HTML email to up to 10 recipients.

Email needs an email provider configured on the Tether server (see Server configuration). Without one, the email channel is shown as unavailable in the dashboard and alerts continue to reach the inbox and your other channels.

Webhook

Tether sends a POST with a JSON body for every alert:

{
  "event": "alert.triggered",
  "alert": {
    "id": "8d0c6a2e-0d3b-4e8f-9a51-2c7f0f6d1b9a",
    "ruleId": "b1a6f3c4-5d7e-4f80-a912-3b4c5d6e7f80",
    "ruleName": "Function errors reach 5",
    "kind": "function_errors",
    "severity": "warning",
    "title": "7 errors in functions",
    "message": "7 of 52 calls failed in the last 5 minutes (threshold 5).",
    "details": { "metric": "count", "errors": 7, "total": 52, "threshold": 5, "windowMinutes": 5 },
    "createdAt": "2026-10-18T09:41:00+00:00"
  },
  "project": { "id": "…", "name": "Studio" },
  "environment": "production",
  "url": "https://tetherdb.co.uk/dashboard/projects/…?env=production&tab=settings&settings=alerts"
}

event is alert.test for test alerts. severity is info, warning or critical. Any 2xx response counts as delivered. Tether waits up to 10 seconds, doesn't follow redirects and doesn't retry.

Webhooks must use a public http or https address. Private and local addresses (such as localhost, 10.x.x.x or 192.168.x.x) are rejected.

Verifying signatures

Add a signing secret to a webhook channel and every request includes two extra headers:

Header Value
X-Tether-Timestamp Unix time in seconds when the request was signed
X-Tether-Signature sha256= followed by the hex HMAC-SHA256 of {timestamp}.{raw body} using your secret

Verify the signature against the raw request body before parsing it, and reject old timestamps to stop replays:

import { createHmac, timingSafeEqual } from 'node:crypto';

export function verifyTetherWebhook(rawBody: string, headers: Headers, secret: string) {
  const timestamp = headers.get('x-tether-timestamp') ?? '';
  const signature = headers.get('x-tether-signature') ?? '';

  // Reject anything older than five minutes
  if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) return false;

  const expected = 'sha256=' + createHmac('sha256', secret)
    .update(`${timestamp}.${rawBody}`)
    .digest('hex');

  return expected.length === signature.length
    && timingSafeEqual(Buffer.from(expected), Buffer.from(signature));
}

The secret is never shown again after you save it. Leave the field blank when editing to keep it, or tick Remove the signing secret.

Discord

Posts a formatted embed to a Discord channel, coloured by severity, with the project, environment and rule. To get a webhook URL in Discord, open Server settings, then Integrations, then Webhooks, create a webhook and copy its URL. It looks like https://discord.com/api/webhooks/<id>/<token>.

Alerts never mention @everyone or roles.

Testing a channel

Send test alert sends a sample alert (severity info) to that channel straight away and shows whether it was delivered. Test alerts don't appear in the inbox. Each channel also shows the result of its most recent delivery, so you can spot a broken webhook.

A channel sends at most 30 alerts an hour and one test a minute. Past that, deliveries are skipped and marked as such.

Inbox

Alerts are kept for 30 days, up to 1,000 per environment. Each alert shows which channels it was sent to and whether each delivery was sent, failed or skipped. Hover a delivery to see the error.

Click the envelope on an alert to mark it read or unread, or use Mark all as read.

API

All endpoints are environment-scoped and need a dashboard session or an OAuth2 token with the settings permission for the project.

Base path: /api/v1/projects/{projectId}/env/{envName}/alerts

Method Path Description
GET / List alerts. Query: unread=true, limit (max 200), offset
GET /unread-count { "unreadCount": 3 }
PATCH /{alertId} Body { "read": true }
POST /read-all Mark every alert read
GET /capabilities Which channel types this server supports
GET / POST /rules List or create rules
PATCH / DELETE /rules/{ruleId} Update or delete a rule
GET / POST /channels List or create channels
PATCH / DELETE /channels/{channelId} Update or delete a channel
POST /channels/{channelId}/test Send a test alert

Create a rule:

POST /api/v1/projects/{projectId}/env/production/alerts/rules
{
  "name": "Checkout error rate",
  "kind": "function_errors",
  "config": {
    "metric": "rate",
    "threshold": 10,
    "windowMinutes": 10,
    "minExecutions": 20,
    "functionName": "checkout.create"
  },
  "cooldownMinutes": 60,
  "channelIds": ["<channel id>"]
}

kind is function_errors, cron_failure (optional config.cronId) or backup_failure. cooldownMinutes is 0 to 10080 (one week).

Create a channel:

POST /api/v1/projects/{projectId}/env/production/alerts/channels
{ "name": "Ops", "kind": "webhook", "config": { "url": "https://example.com/hooks/tether", "secret": "…" } }

kind is email (config.recipients), webhook (config.url, optional config.secret) or discord (config.url). Responses never include webhook secrets, and Discord tokens are masked.

Server configuration

Self-hosting? Email is optional and configured with environment variables on the Tether server. Set TETHER_EMAIL_FROM plus one provider. SMTP is used if both are set.

Variable Description
TETHER_EMAIL_FROM Sender, for example Tether <[email protected]> (required)
TETHER_SMTP_HOST SMTP server hostname
TETHER_SMTP_PORT Port. Defaults to 587 for starttls, 465 for tls, 25 for none
TETHER_SMTP_USERNAME / TETHER_SMTP_PASSWORD SMTP credentials
TETHER_SMTP_TLS starttls (default), tls or none
TETHER_EMAIL_API_URL HTTP email API endpoint, used when no SMTP host is set
TETHER_EMAIL_API_KEY Sent as Authorization: Bearer <key> to the HTTP API

The HTTP provider sends POST with { "from", "to": [...], "subject", "text", "html" }, which works with Resend's /emails endpoint and similar transactional email APIs.

Other alert settings:

Variable Description
TETHER_DASHBOARD_URL Used to link alerts back to the dashboard (first entry if comma-separated)
TETHER_ALERTS_ALLOW_PRIVATE_WEBHOOKS Set to true to allow webhooks to private addresses, for example on an internal network