Notifications
When a guard pauses a flag, Quoin tells the people who can act on it — by email and by one signed webhook per environment.
A flag's own changes are in the audit log. Notifications are only for the events that should interrupt someone. In this version there is one such event: a guard paused a flag.
Built: guard-pause notifications; the other three event types are not
As of QN-04b, Quoin delivers a guard pause by email and by a signed webhook or Slack post. You configure these on
/settings/notifications. The settings page also lists "SRM detected", "Experiment overdue" and "Flag expired".
Those three rows are disabled and read "Coming soon", because nothing in Quoin detects them on a schedule yet.
Email goes through the sender that is configured on the server. Until an operator sets up a sender, the server
prints each message to its own log instead of it sending the message. The printed message includes the addresses of
the recipients. The retention of the log is therefore the retention of those addresses.
Who is told, and where
Settings apply to one environment each. The environment that you choose in the sidebar is the environment that you edit. Quoin delivers a pause in production with the settings of production.
- Email goes to every admin and owner who has joined the workspace. Quoin does not email viewers, editors or pending invites.
- Slack or webhook goes to the one URL that is connected for that environment. A Slack incoming webhook
(
https://hooks.slack.com/…) gets a readabletextline beside the fields below. Any otherhttpsendpoint gets the fields alone.
Both switches on the "Guard paused a flag" row start on. If you turn one off, that channel stops for that environment. If you turn both off, guard-pause notifications stop there. Only admins and owners can see or change this page.
Connecting a webhook
Paste the URL on /settings/notifications and choose Connect. Quoin accepts an https:// URL with a host name.
Quoin refuses these items:
- Plain
http. - A user name or password in the URL.
- IP addresses.
localhost.- Internal names such as
.local,.internal,.lanand.home.arpa.
Quoin also resolves the name when you connect it. Quoin refuses a name that points to a private, loopback, link-local or cloud-metadata address. Quoin resolves the name again before every send. If a name starts to point to one of those addresses, Quoin does not post to it, and it records the delivery as failed. The check is the same check that guards database connections.
Quoin stores the URL encrypted and never shows it in full. The page shows the host and a mask. For a Slack incoming webhook, the page also shows the first characters of its workspace and channel ids, as the own settings of Slack do.
Connecting also creates a signing secret. A dialog shows it once. Copy it into your receiver at that time. To get a
new secret, remove the webhook and connect it again. Test sends a small signed request ("event": "test"). With it,
you can check the receiver before a real pause.
What a guard-pause request carries
{
"event": "guard_paused",
"environment": "production",
"flag": "exports.csv_v2",
"guard": "error-rate",
"signal": { "event": "error_rate", "window": "1h" },
"baseline": 0.003,
"ceiling": 0.02,
"observed": 0.046,
"coverage": 0.1,
"revision": "r-0418",
"at": "<the pause time, ISO 8601>",
"url": "https://quoin.example/rollouts/exports.csv_v2"
}The payload has the flag key, the name of the guard and the rates that it read. It also has the coverage at which the
guard paused and the revision that it wrote. The payload never contains anything about a person. It has no email
address, no user id and no unit id. Rates are fractions: 0.046 is 4.6%.
Verifying the signature
Every request carries two headers:
X-Quoin-Timestamp— seconds since the epoch when Quoin signed the request.X-Quoin-Signature—v1=followed by the hex HMAC-SHA256 of<timestamp>.<raw body>under your signing secret.
Check both headers before you trust the body. Follow these steps:
- Recompute the signature over the raw bytes that you received.
- Compare the signatures in constant time.
- Refuse a timestamp that is too far from your own clock. This prevents the replay of an old request.
Pick a tolerance that suits your receiver and pass it in. Quoin does not choose a tolerance for you.
import { createHmac, timingSafeEqual } from 'node:crypto';
export function verifyQuoinSignature(
secret: string,
rawBody: string,
headers: Headers,
toleranceMs: number,
now = Date.now(),
): boolean {
const timestamp = headers.get('x-quoin-timestamp');
const signature = headers.get('x-quoin-signature');
if (!timestamp || !signature || !/^\d+$/.test(timestamp)) return false;
if (Math.abs(now - Number(timestamp) * 1000) > toleranceMs) return false;
const expected = `v1=${createHmac('sha256', secret).update(`${timestamp}.${rawBody}`).digest('hex')}`;
return expected.length === signature.length && timingSafeEqual(Buffer.from(expected), Buffer.from(signature));
}Retries and the delivery log
Each channel gets up to three attempts. Quoin waits between the attempts for the same backoff that the exposure queue of the SDK uses. Each attempt waits at most ten seconds for an answer. These rules apply to the answers:
- If a receiver answers
2xx, the retries stop. - If the answer is a
429or a5xx, or there is no answer, Quoin retries. - If the answer is any other
4xx, Quoin does not retry, because the same request would get the same answer. - Quoin does not follow redirects.
Every attempt writes one delivery row. The row has these items:
- The event type.
- The environment.
- The flag key.
- The name of the guard.
- The channel.
- The attempt number.
- The state:
sent,retryingorfailed. - The HTTP status.
- The four rates that the request carried (
baseline,ceiling,observed,coverage).
Quoin stores no recipient address and nothing about a user. A guard that only would have paused sends nothing. Automatic pausing is off until an operator turns it on.
Self-hosting
Quoin envelope-encrypts webhook URLs and signing secrets. It seals each one with its own random key. It seals that key
with NOTIFICATION_KEK, a Worker secret (pnpm wrangler secret put NOTIFICATION_KEK). If NOTIFICATION_KEK is not
set, local development uses a fixed development key. Local development also prints emails to the server log, with the
addresses of the recipients. If you change the key, every stored webhook becomes unreadable. After a rotation, remove
the webhooks and connect them again.