Deliver a message in person.
A notification is a badge you have to notice, in a tray you have to open, competing with forty others. This is the opposite: a 3D character walks into the corner of your page, looks at the person using it, and tells them, with a link to the thing it is about. One line of code. An accessible fallback when there is no GPU. And a CLI that announces when your build finishes.
The playground
Everything below runs the real library in this tab. Change the rules, send a message, and watch what the engine decides. Nothing here is a mock.
Compose
Rules
Verdicts
From your terminal, to your screen
Anything that can make an HTTPS request can reach the browser tab you already have open. A message is always delivered to the caller's own sessions, so a key can never be used to interrupt somebody else.
# announce when a long command finishes, keeping its exit code npx @axis/herald watch -- npm test # or say something directly npx @axis/herald say "Migration finished" --from db
curl -X POST /api/herald/announce \ -H "Authorization: Bearer $AXIS_WS_API_KEY" \ -H 'content-type: application/json' \ -d '{"text":"Deploy is green","importance":80}'
Your rail
This tab listens on your own delivery rail while you are signed in. Send the command on the left from anywhere and your avatar will say it here.
The key needs the herald:announce scope. Revoke it any time; nothing else
can address your avatar.
Put it in your own product
No build step required, and the 3D body is optional.
// npm install @axis/herald import { createHerald } from '@axis/herald'; const herald = createHerald({ rules: { minImportance: 70, quietHours: [22, 7] }, voice: 'auto', }); herald.announce({ text: 'Payment received from Acme', from: 'Stripe', importance: 85, url: '/payments/inv_123', });
<!-- or with no bundler at all --> <script type="module"> import { createHerald, pollSource } from '/herald.js'; const herald = createHerald(); // point it at any feed you already have herald.source(pollSource({ url: '/api/alerts', map: (a) => ({ id: a.id, text: a.title, importance: a.severity * 20 }), })); </script>
An interrupt budget
An importance floor, a rate limit, quiet hours, dedupe with a TTL, and a freshness window. Scarcity is what keeps an interruption effective.
Never silently silent
Every message that is not delivered comes back with a reason:
duplicate, rate-limited, quiet-hours,
below-importance-floor.
Works without a GPU
No WebGL, an iframe, reduced motion: the card presenter takes over, in a live region, dismissible with Escape, in your theme.
Any feed
Poll a JSON endpoint, subscribe to SSE, bridge your own event bus, or take the hosted rail. A source is fifteen lines.
The defaults, in full
Chosen so that dropping this into a page cannot make it obnoxious.
| Rule | Default | What it does |
|---|---|---|
minImportance | 50 | The interrupt floor. Below it, nothing is shown. |
freshnessMs | 15 min | Older than this is history, and history belongs in an inbox. |
dedupeTtlMs | 6 h | The same key is said once, however many times a feed repeats it. |
quietHours | off | Local window, wrapping past midnight, e.g. [22, 7]. |
maxPerWindow | 4 / min | The rate limit on interruptions. |
focusOnly | true | Hold while the tab is in the background. |
batchSize | 2 | Said per burst, before the rest collapse into one line. |
Full API, recipes and the rail contract are in the documentation, and AXIS uses this to deliver its own notifications.