The notify service
The Seed service that watches a daemon's activity feed, keeps each account's notification inbox and read state, sends email, and answers signed requests from the web app, the desktop app, the vault and the mobile app.

The notify service is how people hear about what happens on the network when they are not looking. It follows a daemon's activity feed, turns new comments and mentions into notifications, keeps an inbox and read state for each account, and sends email for mentions, replies and discussions. The web app, the desktop app, the vault and the mobile app all read their notification inbox from it. The hosted instance is https://notify.seed.hyper.media.

Notifications are centralized on purpose, for now. The desktop app no longer discovers its own notifications over peer-to-peer sync. The notify server is the one source, and read state syncs only when a client is online. The team treats this as temporary. The plan is private peer-to-peer sync of subscriptions and read state.

Where the code is

frontend/apps/notify, package @shm/notify: Remix 2.17 on Node with Vite, better-sqlite3, nodemailer, Sentry. The email templates are a separate package, frontend/apps/emails (@shm/emails, react-email and MJML).

Path

What it holds

app/entry.server.tsx

Boot: loads .env, opens the database, starts the notifier loops.

app/db.ts

The SQLite schema, migrations and statements.

app/email-notifier.ts

The loops that read the activity feed, classify events, write inbox rows and send email.

app/notification-state.ts

The unified state API: snapshot and actions.

app/validate-signature.ts, app/verify-delegation.ts

Request signatures and agent-capability checks.

app/mailer.ts

The SMTP sender.

app/routes/

Every HTTP route, listed below.

frontend/packages/shared/src/models/notification-*.ts

The client transport, payload shape, read-state logic and comment classifier shared with every client.

Read frontend/apps/notify/README.md first, then app/NOTIFICATIONS_SERVICE_ARCHITECTURE.md for the service. The client halves are in frontend/apps/web/app/NOTIFICATIONS_WEB_ARCHITECTURE.md and frontend/apps/desktop/src/NOTIFICATIONS_DESKTOP_ARCHITECTURE.md. NOTIFICATIONS_REVIEW.md is a restructuring memo, docs/notifications/local-first-read-state-edge-cases.md covers offline read state, and email-notification-signing-notes.md at the repository root is historical: it describes an earlier per-feature API that no longer matches the code.

How it talks to the daemon

The service holds one gRPC-web client to DAEMON_HTTP_URL, the daemon's HTTP port. It pages through ActivityFeed.ListEvents for new events, loads comments, documents and accounts through the same typed API handlers the web app uses, and asks AccessControl.ListCapabilitiesForDelegate whether a signing key holds an AGENT capability for the account it claims. The hosted service reads from the hyper.media gateway daemon, so it sees what that node has synced.

The notifier

At startup the service processes once, then runs two loops.

Loop

Every

Produces

Immediate

15 seconds

mention, reply and discussion notifications: an inbox row for the account, then an email if the account has a verified address.

Batch

30 seconds, sending at most every 4 hours (6 minutes in development)

site-new-discussion digest emails for legacy site subscribers.

Each loop keeps a cursor in the notifier_status table. Event ids are derived from the blob, blob-<cid> or mention-<cid>-<type>-<target>, and the same id is the inbox row's key and the read-state key. A page loop is capped at 100 pages per pass.

Some pieces exist only in name. site-doc-update exists in the payload schema and the notifyOwnedDocChange flag exists in the settings, but the notifier has a TODO where document-update delivery should be, so nobody gets document-change email today. Batch-only notifications are not clearly written to the inbox.

Storage

One SQLite file, web-db.sqlite, in DATA_DIR or the working directory. The tables that matter:

Table

Holds

notification_inbox

Persisted notification payloads per account.

notification_read_state, notification_read_events

A "read everything before this time" watermark per account, plus explicit reads above it.

notification_config, notification_email_verifications

An account's notification email and its verification.

inbox_registration

Accounts registered for an inbox, used by the notifier and the vault.

emails, email_subscriptions

Legacy public site subscriptions and one-click unsubscribe tokens.

notifier_status

Loop cursors and the last batch send time.

The watermark design keeps "mark all as read" to one row and makes unread toggling reversible.

The HTTP API

All routes live under /hm/api and /hm on the notify host. The signed routes take a POST body of DAG-CBOR: the client builds {action, signer, time, accountUid?}, signs the CBOR encoding, and sends it with sig. The service re-encodes the unsigned payload to verify the signature. If accountUid differs from the signer, the signer must hold an AGENT capability for that account, which is how a browser session key acts for the account held in the vault. The service accepts Ed25519 and compressed P-256 signers.

Route

Kind

Used by

/hm/api/notifications

signed: get-notification-state, apply-notification-actions

web, desktop, mobile; returns inbox, email config and read state in one snapshot

/hm/api/notification-inbox, /hm/api/notification-config

signed

the vault: inbox registration, email config, trusted email prevalidation

/hm/api/notification-read-state

signed

compatibility only; no first-party client calls it

/hm/api/public-subscribe/*

unsigned

legacy site email subscriptions

/hm/api/email-notif-token, /hm/email-notifications?token=

token

the settings page linked from emails

/hm/api/unsubscribe

token

one-click unsubscribe, RFC 8058

/hm/notification-email-verify

link

email verification

/hm/notification-read-redirect

link

marks an email's notification read, then redirects to the comment

Email prevalidation. A vault that has already verified a person's email signs {email, signer, host} with its daemon's key. The notify service accepts that instead of sending a verification email only when the host is in NOTIFY_TRUSTED_PREVALIDATORS, and only after fetching the host's /hm/api/config and checking that signerAccountUid matches the signer.

Configuration

Variable

Meaning

DAEMON_HTTP_URL, DAEMON_HTTP_PORT

The daemon to read.

PORT

Listen port; the Docker image uses 3000, the hosted deployment 3560.

DATA_DIR

Where web-db.sqlite lives.

SEED_BASE_URL

The public site used to build links in emails.

NOTIFY_SERVICE_HOST

This service's own public URL, used in email links.

NOTIFY_SMTP_HOST, NOTIFY_SMTP_PORT, NOTIFY_SMTP_USER, NOTIFY_SMTP_PASSWORD, NOTIFY_SENDER

Outgoing mail. Without host, user and password, emails are logged and dropped.

NOTIFY_TRUSTED_PREVALIDATORS

Vault origins whose email prevalidation is trusted.

NOTIFY_SENTRY_DSN

Error reporting.

Running it

pnpm notify # Remix dev server on :3060, against the desktop dev daemon on 58001 pnpm notify:standalone # its own daemon on 54000-54002 and notify on :3061 pnpm --filter @shm/notify test

./dev up starts it as the notify pane and points the vault pane at it. Images are seedhypermedia/notify:dev from main and seedhypermedia/notify:latest for releases. The hosted services are notify.seed.hyper.media and notify-dev.seed.hyper.media.

Working with it

In the Seed app

The desktop app keeps a local optimistic notification store and syncs it with /hm/api/notifications, signing each request through the daemon's SignData RPC. The notify host comes from VITE_NOTIFY_SERVICE_HOST, https://notify.seed.hyper.media in release builds. Account settings can override it, and the override is saved in the synced vault state.

Web API

A site advertises its notify service as notifyServiceHost in /hm/api/config (see The web API), set from NOTIFY_SERVICE_HOST on the web app. The web app's /hm/notifications page signs requests in the browser with the session key and stores the notify host it learned at sign-in. The mobile app reads the same field and falls back to the hosted service.

SDK

The SDK has no notify client. The transport lives in @shm/shared (models/notification-service.ts). A third party can reproduce it with the SDK's CBOR and signing helpers.

Agents

Seed Agents do not use the notify service. They watch the network themselves through the activity feed. See triggers.

Where this is going

As of September 2026, the team plans three changes. None of them is built.

    Move subscriptions and read state out of this central server into private peer-to-peer sync once private documents mature.

    Restore the link between an email and the account that subscribes. It was dropped in 2025 and later called a mistake.

    Replace the legacy site subscriptions with per-site options for document changes, discussions, comments and mentions.

Do you like what you are reading? Subscribe to receive updates.

Unsubscribe anytime