Plan: Notify and Agents on the Site's Own Host
A phased plan: notify always runs on a site's own origin at /hm/api/notify as a small headless Bun service, and its pages move into the web app. Agents can run at /hm/api/agents on the same host or stay standalone, as production does. It also lists the risks.

This is a plan for the proposal Auxilary Services Same Host. It is based on a reading of the Seed repo at 379c821e3 (2026-10-14) and on the discussion under the proposal.

The goal, as the discussion frames it

The proposal: notify and agents stop needing their own hostnames and run on the same host as the site, the way the vault already does at /vault. Neither service has a frontend. Notify's few pages move into the web (Remix) app, and notify becomes a small headless Bun service, like the vault and agents already are.

Eric's reply in the thread gives the reason. Today the self-hosting story is unclear: someone may want to self-host agents only, run three agent servers, or skip notify. All of that is possible already, but each case needs its own subdomains, certificates and env wiring. Once this is done, each service has a fixed path on the site, /hm/api/notify/* and /hm/api/agents/*, so routing is a few proxy rules. Alex ("long due") and Horacio ("go for it") support it, and nobody raised an objection.

Decisions since the proposal (Eric, 2026-10-14):

    Agents don't have to live on the same host. Same host is supported, and other environments (enterprise, self-hosted, dev) may use it. The production agents server stays standalone.

    Notify has no reason to be hosted separately. It always runs on a site's own origin, and the notify.seed.hyper.media host goes away.

    Links in emails already sent are allowed to break. They are not preserved.

How it works today


vault

notify

agents

Where it runs

<site>/vault (enterprise only)

notify.<dns>, or hosted notify.seed.hyper.media

agents.<dns>, or hosted agentic.seed.hyper.media

Path layout

Serves itself under /vault; the proxy does not strip the prefix

Routes at /hm/api/notification*, /hm/api/unsubscribe, /hm/api/public-subscribe/*, /hm/api/email-notif-token, plus three HTML pages under /hm/*

/api/message, plus everything again under /agents/api/* and /agents/ws

Frontend

React SPA

Remix app with 3 pages

none

Own state

sqlite

sqlite web-db.sqlite, plus in-process email loops

sqlite agents.sqlite, /data/agents/*, uploads, microVMs (/dev/kvm)

Talks to the daemon

BACKEND_GRPC_BASE_URL

DAEMON_HTTP_URL (gRPC-web) + SEED_BASE_URL

SEED_AGENTS_HM_SERVER_URL (Seed API) + IPFS URL

How the services and the site find each other

flowchart LR subgraph site[Site host] web[seed-web] daemon[seed-daemon] vault[seed-vault /vault] end notify["notify.DNS"] agents["agents.DNS"] web -- "/hm/api/config advertises notifyServiceHost (env NOTIFY_SERVICE_HOST)" --> clients[desktop / web browser / mobile] vault -- "auth callback returns notifyServerUrl (SEED_VAULT_DEFAULT_NOTIFY_SERVER), stored in IndexedDB" --> clients notify -- "prevalidation fetches SITE/hm/api/config and checks signerAccountUid (NOTIFY_TRUSTED_PREVALIDATORS)" --> web notify -- "gRPC-web ListEvents, capabilities, domain checks" --> daemon agents -- "Seed API polling + IPFS" --> daemon clients -- "signed CBOR" --> notify clients -- "signed CBOR + WebSocket" --> agents


    Notify knowing the site. Notify gets DAEMON_HTTP_URL and SEED_BASE_URL. For email prevalidation it fetches <site>/hm/api/config and trusts only hosts listed in NOTIFY_TRUSTED_PREVALIDATORS.

    The site knowing notify. The web app's NOTIFY_SERVICE_HOST goes into window.ENV and /hm/api/config.notifyServiceHost. Desktop reads that field for each site, and the same host is baked into desktop builds via VITE_NOTIFY_SERVICE_HOST. The vault separately hands signed-in web users a notifyServerUrl, which browsers keep in IndexedDB.

    The site knowing agents. Web uses SEED_AGENT_SERVER_URL, defaulting to agentic.seed.hyper.media. Desktop and mobile hardcode the same default. A site can also advertise one through its home document's agentServerUrl / spaceAgents metadata. Desktop users keep a list of servers.

    Deploy wiring. ops/deploy.ts builds https://notify.<dns> and https://agents.<dns>, gives them Caddy site blocks, and passes them to web, vault and notify. Only the enterprise compose file runs notify or agents at all. The standard stack and every home-server template (umbrel, casaos, coolify, caprover, dokploy, easypanel, fly, railway) point at hosted notify and run no agents.

Useful facts found while reading

    Hosted notify already runs on the hyper.media machines. In SeedInfra/seed_dns/main.tf, notify.seed.hyper.media is a CNAME to hyper.media and notify-dev.seed.hyper.media is a CNAME to dev.hyper.media. (The two Terraform keys are swapped, seed_notify_dev names the prod record, which is worth fixing when the records are deleted.) Moving hosted notify to hyper.media/hm/api/notify is a proxy rule on the same machine: no data move and no new machine.

    No path clashes. The web app has no routes named like notify's (hm.api.notification*, hm.api.unsubscribe, hm.api.public-subscribe, hm.api.email-notif-token, hm.email-notifications, hm.notification-*). Notify's current API paths can therefore also be routed on a site's own origin, which gives older clients a cheap compatibility path (see Phase 2). New code uses the /hm/api/notify/* prefix so routing is one rule.

    The agents client is inconsistent about the base URL. It posts to ${base}/api/message but uses ${base}/agents/ws, ${base}/agents/api/health and ${base}/agents/api/webhooks/..., and normalizeAgentServerUrl strips a trailing /agents. A base of https://site/hm/api/agents would produce /hm/api/agents/api/message and /hm/api/agents/agents/ws. The base-URL contract has to be fixed first (frontend/packages/ui/src/agents/client.ts:156-197,311, agents/src/main.ts:150-163).

    Notify's pages are mostly redirects. hm.notification-read-redirect and hm.notification-email-verify each do one database write and then redirect. They can become notify API endpoints that answer with a 302, with no UI at all. Only the email-settings page (hm.email-notifications) needs real UI in the web app.

    The old notify host is stored in several places.

      Desktop release builds bake in VITE_NOTIFY_SERVICE_HOST=https://notify.seed.hyper.media.

      A desktop user's override is saved in the synced vault state.

      Browsers keep the vault's notifyServerUrl in IndexedDB.

      The vault's SEED_VAULT_DEFAULT_NOTIFY_SERVER defaults to it.

      Mobile and every home-server template use it as the fallback.

      Links in emails already sent also use it. Per the decision above, those links are allowed to break. The stored client values are handled in Phase 2.

Target design

    Notify always lives on a site's origin, at /hm/api/notify. There is no standalone notify hostname any more. A site that runs notify serves it at <site>/hm/api/notify. A site that doesn't uses another site's notify, normally https://hyper.media/hm/api/notify. notify.seed.hyper.media and notify-dev.seed.hyper.media are retired.

    Agents are wherever the operator wants. Same host is supported and is the default for self-hosted and enterprise nodes that run agents (<site>/hm/api/agents). A standalone agents server is still a first-class setup, and production stays standalone at agentic.seed.hyper.media on its own machine. Nothing in this plan moves or changes the prod agents deployment. The only thing it needs is the server to also answer at the new canonical paths.

    Each service is a base URL, and the site advertises it. /hm/api/config gains notifyServiceUrl and agentServerUrl (full base URLs; empty means "none"). notifyServiceHost stays for clients already in the field.

    Services serve themselves under their prefix, as the vault does. The proxy forwards /hm/api/notify/* and /hm/api/agents/* unchanged and never strips the prefix. A standalone agents server is then https://agentic.seed.hyper.media/hm/api/agents, the same shape as a co-located one. The agents server keeps its current root routes so every stored https://agentic.seed.hyper.media value keeps working.

    The reverse proxy does the routing, not the web app. Proxying through the web app's Node process would add load to the process that is already the bottleneck under crawler traffic, and Remix is a poor WebSocket proxy.

    Notify becomes a light Bun service. Once its pages are in the web app, nothing in notify needs Remix. It becomes a Bun.serve route table plus its email loops, on the same runtime and image base as the vault and agents.

    Multiple agent servers remain a client feature. The site advertises its default. Desktop keeps its list. Site metadata agentServerUrl still overrides. Nothing here limits a user to one.

Steps

Phase 0: settle the conventions (small, first)

    [ ] Write the base-URL contract for both services in hypermedia/ (apps/notify.md, agent/operations.md). Cover the base URL, the path suffixes, the meaning of an empty value, and how legacy origin-only values are read.

    [ ] Add one shared helper per service in @shm/shared that turns a base URL into endpoint URLs. Everything that builds a notify or agents URL goes through it: notification-service.ts, email-notifications.ts, the vault's notification-api.ts, desktop app-sites.ts / app-notifications.ts, mobile, and the agents client.ts.

    [ ] The notify helper also rewrites the retired hosts: https://notify.seed.hyper.media → https://hyper.media/hm/api/notify and https://notify-dev.seed.hyper.media → https://dev.hyper.media/hm/api/notify. That one mapping fixes every stored value the new clients will read: desktop overrides in the synced vault state, browser IndexedDB, and any notifyServiceHost still advertised.

Phase 1: agents at /hm/api/agents, with standalone still supported

    [ ] Server: register the canonical routes POST /hm/api/agents/message, GET /hm/api/agents/ws, POST /hm/api/agents/webhooks/:id[/:secret] and GET /hm/api/agents/{health,version,perf,...}. Keep /api/message, /agents/api/* and /agents/ws. They are what the prod server and existing webhooks use, and there is no reason to remove them.

    [ ] Client: when the base path ends in /hm/api/agents, use the new suffixes. An origin-only base keeps the legacy suffixes, so https://agentic.seed.hyper.media and every stored server list keep working unchanged. Fix normalizeAgentServerUrl so it never rewrites the new path.

    [ ] Webhooks: new triggers get <base>/webhooks/.... On prod nothing changes because the host and legacy routes stay.

    [ ] Discovery: /hm/api/config advertises agentServerUrl. The web /hm/agents page uses it before the env default. Desktop's per-site lookup reads it, with an agreed priority against the agentServerUrl in site metadata (open question below). Hosted hyper.media advertises https://agentic.seed.hyper.media.

    [ ] Tests: client URL-building unit tests (legacy origin, /agents suffix, new prefix) and a server route test for both shapes.

Phase 2: notify at /hm/api/notify on the site's origin

    [ ] Server: move API routes to /hm/api/notify/{notifications,config,inbox,read-state,public-subscribe/*,unsubscribe,email-notif-token}.

    [ ] Keep the current /hm/api/notification* route names as aliases for now, routed on the site's origin. Older desktop builds read a site's notifyServiceHost and call ${host}/hm/api/notifications. If the site advertises its own origin there, and the proxy also forwards those few legacy paths to notify, older desktops keep working against the new layout with no client update. It costs a handful of proxy rules, which can be removed when the old builds are gone.

    [ ] Redirect pages become API endpoints: add GET /hm/api/notify/read-redirect and GET /hm/api/notify/verify-email, which write and then answer 302. Email templates switch to these.

    [ ] Email links come from notify's own origin. Because notify and the web app now always share an origin, NOTIFY_SERVICE_HOST is the site origin. Settings and welcome-email links point at <origin>/hm/email-notifications?token=… (the web app's page, Phase 3a). List-Unsubscribe and the redirect links point at <origin>/hm/api/notify/.... No second "web URL" setting is needed. Links in emails sent before the switch are not preserved.

    [ ] Discovery:

      /hm/api/config advertises notifyServiceUrl, plus notifyServiceHost set to the origin for older clients.

      The vault's SEED_VAULT_DEFAULT_NOTIFY_SERVER, the desktop release build's VITE_NOTIFY_SERVICE_HOST, mobile's fallback and ops/deploy.ts's NOTIFY_SERVICE_HOST all become https://hyper.media/hm/api/notify.

    [ ] Prevalidation: with notify on the site's origin, NOTIFY_TRUSTED_PREVALIDATORS defaults to the site. Let notify fetch the site's /hm/api/config over the internal network (seed-web:3000) instead of through the public hostname.

    [ ] Tests: the existing notify route tests against the new paths and the aliases. Email snapshot tests for the new link shapes.

Phase 3: notify's pages go into the web app, and notify becomes a light Bun service

3a. Pages into the web (Remix) app.

    [ ] Add /hm/email-notifications to frontend/apps/web, reusing the notify page's components (they already use @shm/ui). It calls /hm/api/notify/email-notif-token on the same origin. The token is a bearer credential: set Referrer-Policy: no-referrer on that route and keep the query string out of SSR and access logs.

    [ ] The read-redirect and email-verify pages don't move. They are notify API endpoints now (Phase 2), and email-verify redirects to the web settings page.

    [ ] The web app's existing hm.notifications page and site-settings emails pages stay as they are. Only the token-link page is new.

3b. Notify as a headless Bun service.

    [ ] Replace Remix with a Bun.serve route table, the same pattern as vault/src/main.ts and agents/src/main.ts. Each hm.api.* route's loader or action becomes a handler. The code that validates signatures, verifies delegation and writes state is already plain TypeScript and moves unchanged. Start the email loops from main.ts instead of entry.server.tsx.

    [ ] SQLite: better-sqlite3 is a native Node addon and Bun doesn't support it. Port db.ts to bun:sqlite. The API is close (prepare/run/get/all, transaction), but named-parameter prefixes, pragma calls and returned row and change shapes differ. The on-disk format doesn't change, so existing web-db.sqlite files open as they are. Port db.test.ts to bun test first and use it as the gate.

    [ ] Other dependencies:

      gRPC to the daemon: notify-request.ts uses createGrpcWebTransport from @connectrpc/connect-node. Switch to the same function from @connectrpc/connect-web, as vault/src/daemon-client.ts already does.

      Error reporting: @sentry/remix becomes @sentry/bun.

      Email rendering: mjml / @faire/mjml-react and nodemailer are plain JS. Check they render and send under Bun with the existing email snapshot tests.

      Images: sharp (and node-fetch) are only imported by app/utils/image-processor.ts, which nothing imports. Delete it and the native dependency goes too.

    [ ] Remove the rest: Vite, Tailwind, Remix, @shm/ui, react-dom/client build, isbot, tsx, and the HTML handling in entry.server.tsx. The Dockerfile moves to oven/bun and copies only what the server needs, like the agents image.

    [ ] Behaviour stays the same: same port (3000 in the container), same /data volume and database file, same env vars, same routes. A deployment swaps the image with no data migration.

Phase 4: ops

    [ ] Hosted hyper.media and dev.hyper.media:

      In the prod and dev proxies on those machines, route /hm/api/notify/* (plus the legacy notify API paths) to the notify container that already runs there.

      Exempt these paths from the crawler throttle. They are signed API calls, not pages.

      Switch notify's NOTIFY_SERVICE_HOST to https://hyper.media (dev: https://dev.hyper.media).

      Once Phase 2 clients are released, delete the notify.seed.hyper.media and notify-dev.seed.hyper.media DNS records and proxy blocks (SeedInfra/seed_dns/main.tf).

    [ ] Hosted agents: no change. agentic.seed.hyper.media stays standalone on its own machine. Optionally make the agents image's new canonical paths reachable there too, so https://agentic.seed.hyper.media/hm/api/agents works as a base URL. It comes for free once Phase 1 ships.

    [ ] Enterprise Caddyfile (generateEnterpriseCaddyfile):

      Add handle /hm/api/notify/* (plus legacy notify API paths) → seed-notify:3000.

      When agents are co-located, add handle /hm/api/agents/* → seed-agents:3050, with the agents request_body max_size and WebSocket upgrade.

      Put both before the forward_auth gate, next to /vault, and add them to ENTERPRISE_OPEN_PATHS. Desktop clients, webhook senders and mail clients doing one-click unsubscribe never carry a site session.

      Remove the notify.<dns> block.

      Keep an agents.<dns> block only for operators who choose a standalone agents host (a config option), not by default.

    [ ] Standard stack: add an optional agents profile and the path rule to generateCaddyfile. Notify isn't added there: standard sites keep using hosted notify, now https://hyper.media/hm/api/notify. A standard-stack notify profile can come later if someone wants their own email sending.

    [ ] Home-server templates (umbrel, casaos, coolify, caprover, dokploy, easypanel, fly, railway, website_deployment.sh): change the notify URL env to https://hyper.media/hm/api/notify. They need no proxy changes, because they keep calling a remote notify.

    [ ] deploy.ts doctor and invites: probe /hm/api/notify/... and, when co-located, /hm/api/agents/health. Update deploy.test.ts snapshots.

    [ ] Monitoring: add health checks for notify and agents. There are none today.

Phase 5: clean up (after a few desktop releases)

    [ ] Remove the legacy notify API path aliases and the retired-host rewrite once hyper.media's logs show the old shapes have stopped.

    [ ] Keep the agents legacy routes, because standalone prod still uses them as its base.

    [ ] Update hypermedia/ docs (apps/notify.md, apps/vault.md, apps/web.md, build/web-api.md, agent/operations.md, self-hosting guides).

Risks

Risk

Impact

Mitigation

Clients that stored notify.seed.hyper.media (desktop builds, desktop overrides in the synced vault, browser IndexedDB, mobile) after the host is deleted

Notification inboxes and subscribe boxes fail for those users

Ship the retired-host rewrite (Phase 0) and the new defaults in a desktop release before deleting the DNS records. Older desktops still work through a site's advertised origin plus the legacy API aliases. Only the baked-in account-level default on old builds breaks, and updating fixes it

Links in emails already sent stop working

Old unsubscribe and settings links 404

Accepted by decision. New emails use the new links from the day Phase 2 deploys

The enterprise sign-in gate (forward_auth) intercepts the new paths

Desktop, webhooks and one-click unsubscribe get redirected to sign-in

Route both prefixes before the gate and add them to the open paths. Both APIs authenticate every request with signed envelopes or tokens. Add a deploy test that asserts this

hyper.media's crawler throttle or proxy rules catch notify traffic

Notification calls get rate-limited or slow

Exempt /hm/api/notify/* in the prod proxy. Notify already runs on that machine, so this adds proxy rules, not load

Agents microVMs compete with the daemon and web on a co-located node

A busy code-exec queue slows the site itself

Prod stays standalone, so this only affects self-hosters who opt in. Lower SEED_AGENTS_EXEC_* concurrency when co-located (the CPU−2 default assumes a dedicated host). Document memory sizing. /dev/kvm is still needed for code execution

Base-URL ambiguity in the agents client (/api/message vs /agents/*, /agents stripping)

Wrong URLs once a path prefix is involved, and stored server lists get rewritten

Fix the contract first (Phase 1) with unit tests for every shape

Same-origin raises the stakes of an XSS bug in any service

Script on one service could act on the site's origin

Notify becomes headless and agents already is, so neither returns HTML. Keep CSP on the web app

Notify's email-settings token appears in a web-app URL

Token leaks via Referer, analytics or SSR logs

Referrer-Policy: no-referrer on that route. Keep the query out of logs. Consider moving the token to the URL fragment

Notify's port to Bun (better-sqlite3 → bun:sqlite, connect-node → connect-web, Sentry)

Subtle query or row-shape differences corrupt or drop notification state. Email loops send duplicates or stall

Port the DB layer behind db.test.ts first. Keep the same database file and event cursors (notifier_status). Run the Bun image side by side on dev against a copy of the prod database with sending disabled, and compare which emails it would send

Two things changing at once in notify (routes and runtime)

A failure is hard to attribute

Ship Phase 2 on the current Remix app first. Then swap the runtime with identical routes

/hm/api/config now matters more

A wrong advertised URL sends every client to the wrong place

The doctor checks that each advertised URL answers its health endpoint

Open questions

    Agents server precedence: when both are set, should a site's advertised /hm/api/config.agentServerUrl or the home-document agentServerUrl metadata win? (The metadata is signed and travels with the site. The config endpoint reflects what is actually running on the host.)

    Should /hm/api/config advertise more than one agent server (for the "3 agent servers" case), or does that stay entirely in client and site metadata?

    When to delete the notify.seed.hyper.media DNS record: right after the Phase 2 desktop release, or after one more release so the rewrite has reached most users?

Suggested order and size

    Phase 0 and Phase 1 (contract, helpers, agents canonical paths). Small and contained. Prod agents are untouched. One PR.

    Phase 2 (notify paths, redirect endpoints, new defaults, retired-host rewrite). One PR, released with a desktop build.

    Phase 4 hosted and enterprise proxy changes. One PR in this repo plus the prod and dev proxy config. Ship once 1 and 2 are released, then delete the old notify DNS records.

    Phase 3a (settings page into web). One PR. It has to land with or before the first emails that link to <origin>/hm/email-notifications, so in practice it ships together with Phase 2.

    Phase 3b (notify on Bun). One PR, deployed to dev.hyper.media first with a shadow run against a copy of the prod database.

    Phase 4 standard stack and templates, then Phase 5 cleanup, over later releases.

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

Unsubscribe anytime