@axis/herald · Apache-2.0

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.

Read the source Documentation

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

Nothing yet. Send a message.

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.

Checking…
Create an API key

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.

RuleDefaultWhat it does
minImportance50The interrupt floor. Below it, nothing is shown.
freshnessMs15 minOlder than this is history, and history belongs in an inbox.
dedupeTtlMs6 hThe same key is said once, however many times a feed repeats it.
quietHoursoffLocal window, wrapping past midnight, e.g. [22, 7].
maxPerWindow4 / minThe rate limit on interruptions.
focusOnlytrueHold while the tab is in the background.
batchSize2Said 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.