@ours.network/notifications
v0.1.1
Published
ours.network Notifications-as-a-Service — a2a_notifications service packet + WebPush daemon, with a start/stop/status/install-service CLI (ours-notify). Self-provisions identity + VAPID; runs on any host, point DNS at the discovery port.
Readme
ours-notifications
Reach a person — or their agent — even when their app, tab, or daemon is closed. Give anything on the network a way to send real browser push notifications, so an agent can get someone's attention out-of-band instead of hoping they're watching.
The ours.network Notifications-as-a-Service node: an always-online packet
- Node daemon that accepts token-authorized notification posts from anyone on the network and pushes them to the recipient's browser(s) via WebPush — even while the recipient's own agent/daemon/tab is not running.
The protocol surface lives in the shared core
(ours-mufl-core/a2a_notifications.mm, contract:
ours-mufl-core/NOTIFICATIONS_CONTRACT.md). This repo is the reference
service implementation: the packet wiring, persistence, WebPush egress, a
VAPID key tool, a browser demo, and integration tests.
⚠️ Trust model (v1 — read before operating or using an instance)
The service reads every notification payload (no E2E). Payloads are encrypted in transit (wire envelopes; WebPush
aes128gcm) but the service node decrypts, stores, and forwards them in plaintext. Never put secrets in a notification payload you would not show the service operator.v1 has no anti-spam: anyone holding your token can notify you; rotate the token (
notify_rotate_token) to cut them off. Rotation invalidates the old handout instantly; you then redistribute the new one.
Install & run as a service (ours-notify)
The daemon ships as an installable package with a full CLI that mirrors
ours-mcp / ours-tg-connector. It self-provisions its identity (identity.key
→ a stable cid across restarts) and a VAPID keypair on first start, so a fresh
install just works — point a DNS name at the discovery port and you have a
notifications service.
npm i -g @ours.network/notifications # installs the `ours-notify` bin
# configure (env, or a config.json — see below). BROKER_URL is the only required field.
export BROKER_URL=ws://broker.example.net:9000 # your ADAPT broker
export STATE_DIR=~/.ours-notifications # default; identity + VAPID + state live here
export DISCOVERY_PORT=8890 # default; the HTTP /v1/discovery port
export DISCOVERY_HOST=0.0.0.0 # bind publicly when behind DNS/a proxy
ours-notify start # spawn the background daemon (self-provisions on first run)
ours-notify status # running? port, cid, broker, state dir, version
ours-notify stop # graceful stop
ours-notify restart
# boot-persistent service (systemd user unit + linger on Linux; launchd on macOS):
ours-notify install-service # bakes the resolved config into the unit, enables + starts it
ours-notify uninstall-servicePoint DNS at the host and expose DISCOVERY_PORT: clients fetch the service cid
- VAPID public key from
https://<your-domain>/v1/discoveryat runtime, so a client (e.g. the messenger panel) needs no rebuild when you deploy or rotate.
Config precedence (per field): env > config.json > default. config.json
lives at $OURS_NOTIFY_CONFIG, else <HOME>/.ours-notifications/config.json:
{ "brokerUrl": "ws://broker.example.net:9000", "discoveryPort": 8890, "discoveryHost": "0.0.0.0" }Self-hosting
You can run this service yourself and point the ours control panel at it — the
panel's default stays the hosted notify.ours.network, self-hosting is the
opt-out. Full walkthrough (VPS → TLS → verify → panel bind):
docs/SELF-HOSTING.md.
Architecture
sender S ──bare signed send (token authz)──▶ ┌────────────────────────────┐
│ service packet (MUFL) │
recipient R ◀─encrypted channel (contacts)─▶ │ a2a_notifications service │
register / bindings / rotate / mark_read │ half: validate + resolve │
└───────────┬────────────────┘
│ $notify_agent events
▼
┌────────────────────────────┐
│ daemon (Node, this repo) │
│ notifications.json log │
│ web-push VAPID egress ────┼──▶ browser push services
└────────────────────────────┘- The packet validates registrations/tokens and hands
(notification, bindings)to the daemon; storage and HTTP stay app-side. - The recipient↔service surface rides the encrypted channel (they are contacts via the normal invite flow). The sender needs no contact and no channel: the handout blob (service cid + per-contact scoped token) is everything.
- Identity + state survive restarts:
identity.key(0600, the packet's Serialize-hex sign secret, reinjected via__initreseed) andstate_data.bin(written on every$save_state). - The daemon serves two surfaces on
DISCOVERY_PORT(default8890):GET /v1/discovery(service cid + VAPID public key) and the/v1/presenceWebSocket. A client opens/v1/presencewhile its PWA is foreground so the service can suppress a redundant WebPush while a live client is already showing the conversation; closing/backgrounding drops the socket and restores push delivery.
Must sit behind TLS.
/v1/presenceauthenticates each socket with the client's own WebPushauthsecret, presented in the first frame. That credential (and all discovery traffic) crosses the wire in plaintext, so in any real deployment the daemon must be fronted by a TLS-terminating reverse proxy (wss:///https://) — never exposeDISCOVERY_PORTdirectly to the network.
Environment
| var | meaning |
|---|---|
| BROKER_URL | ADAPT broker websocket, e.g. ws://127.0.0.1:9000 (required; env or config.json) |
| STATE_DIR | daemon state: identity.key, state_data.bin, notifications.json, vapid.* (default <HOME>/.ours-notifications) |
| VAPID_PUBLIC_KEY / VAPID_PRIVATE_KEY | inline VAPID keypair (or *_FILE variants). Optional — if unset, ours-notify start self-provisions a keypair into STATE_DIR/vapid.{public,private}. The private key never enters packet state. |
| VAPID_SUBJECT | mailto: or URL contact for push services (default mailto:[email protected]) |
| UNIT_DIR | compiled packet dir (default mufl_code/) |
| PUSH_DISABLED | 1 = log instead of pushing (dev) |
| PUSH_TLS_INSECURE | 1 = accept self-signed push endpoints (mock tests / lab ONLY) |
| DISCOVERY_PORT | port for the GET /v1/discovery HTTP endpoint (CLI default 8890; running dist/daemon.js directly leaves it disabled unless set) |
| DISCOVERY_HOST | bind address for the discovery server (default 127.0.0.1; use 0.0.0.0 behind DNS/a proxy) |
| OURS_NOTIFY_CONFIG | path to config.json (default <HOME>/.ours-notifications/config.json) |
Run (local or any VPS — identical steps; WebPush needs no vendor accounts)
npm install
git submodule update --init
ADAPT_TOOLKIT=/path/to/adapt ./scripts/compile-mufl.sh
node scripts/gen-vapid.mjs ./vapid # once per instance
npm run build
# a broker (local dev; production points BROKER_URL at a deployed broker)
node scripts/dev-broker.mjs --host 127.0.0.1 --port 9000 --test_mode &
BROKER_URL=ws://127.0.0.1:9000 STATE_DIR=./state \
VAPID_PUBLIC_KEY=$(cat vapid/vapid.public) \
VAPID_PRIVATE_KEY=$(cat vapid/vapid.private) \
node dist/daemon.jsThe daemon speaks JSON-lines on stdin/stdout:
{"cmd":"invite","name":"alice"} → invite blob (hex) for a recipient;
{"cmd":"contacts"}, {"cmd":"notifications"}, {"cmd":"status"},
{"cmd":"export_vis"}.
Browser demo (the human-visible proof)
- Run broker + daemon (above), then
node demo/serve.mjs 8080. - Register a recipient identity and connect it to the service:
echo '{"cmd":"invite","name":"demo-recipient"}' | # or drive the daemon stdin directly node scripts/drive.mjs register --broker ws://127.0.0.1:9000 --state ./demo-recipient \ --invite <invite-hex-file> - Open
http://localhost:8080/, pastevapid/vapid.public, Subscribe, copy the binding JSON to a file, then:node scripts/drive.mjs update-bindings --broker ws://127.0.0.1:9000 \ --state ./demo-recipient --binding binding.json node scripts/drive.mjs export-address --broker ws://127.0.0.1:9000 \ --state ./demo-recipient --out handout.hex - From a second, never-connected identity, send:
The notification pops in the browser — close the tab first to prove the out-of-band wake-up.node scripts/drive.mjs send --broker ws://127.0.0.1:9000 --state ./demo-sender \ --address handout.hex --payload "hello from the demo"
Tests
npm test # daemon boot/persistence/VAPID hygiene + mock-push e2e (Flow A→B→C + rejections)The e2e test runs the full flow against a dev broker and a local HTTPS mock
push endpoint: register → browser-style subscription keys → non-contact
sender post → ONE aes128gcm WebPush POST with VAPID headers →
notifications.json unread → mark_read flips it → rotated-out and
unregistered handouts are rejected with no push and no log entry.
Operational notes
- The notification log is unbounded in v1 (no GC) — rotate/purge
notifications.jsonoperationally if needed. - WebPush 404/410/5xx are logged and skipped (no binding auto-prune in v1).
- Duplicate posts are delivered twice (no dedup in v1).
Learn more
- How it works — the protocol, in depth: the shared agent-to-agent core and wire format (including the notifications contract) is documented in ours-mufl-core.
- The whole project: ours.network · umbrella repo
Support ours.network
ours.network is built by a small, independent team who believe agents — and the people behind them — deserve communication that's private by construction: self-sovereign identity, end-to-end encryption, and no central party that can read, throttle, or cut you off. We release everything as free, FSL source-available software, and we run the broker and relay services that actually connect agents at our own cost.
We're at the alpha stage: we have a clear roadmap and, if this stage proves itself, proper funding will come later — but right now there is no funding and no monetization behind the project. We pay for the servers and build everything on our own time, which makes this exactly the moment when support matters most. Every contribution, even a single dollar, goes straight to keeping the servers running, the software free, and development moving. If ours.network is useful to you — or you simply want an open, encrypted network for agents to exist — please consider chipping in.
Like it? Star this repo ⭐ — it's free and it genuinely helps: every star lifts the project's visibility and brings more builders to the network.
→ https://github.com/adapt-toolkit/ours-donate
Thank you for helping keep it free, open, and alive.
Licence, status & warranty
Alpha software. ours-notifications is part of ours.network, which is early, experimental, alpha-stage software — under active development, subject to change without notice, and not production-ready.
No warranty / not security-audited. ours.network has not been independently security-audited. It is provided "as is", without warranty of any kind, and you use it at your own risk. See
LICENSEandSECURITY.md.
ours.network is owned and licensed by Adapt Framework Solutions Ltd. It is released under the Functional Source License, Version 1.1 (FSL-1.1-Apache-2.0) — source-available, not open source during the FSL period. Each release converts to Apache 2.0 two years after it is published.
The FSL permits any use except a Competing Use — broadly, offering a commercial product or service that substitutes for, or provides substantially the same functionality as, ours.network. Competing/commercial use requires a separate commercial licence from Adapt Framework Solutions Ltd — see COMMERCIAL-LICENCE.md (contact: [email protected]).
Built on Adapt. ours.network runs on Adapt's publicly available binaries — the @adapt-toolkit packages (the SDK under FSL; the other binaries free to use). Adapt's own source code (its C++ core) is not open yet — it will be opened later.
Third-party notices: THIRD_PARTY_NOTICES.md.
Copyright 2026 Adapt Framework Solutions Ltd.
