Infrastructure Plan · Approved Design

Self-Hosted ntfy — Private Bidirectional Comms

One private push server replaces public ntfy.sh topic-secrecy. Sessions page the phone as before — and the phone now steers any live tmux pane from anywhere. One new component; everything else is configuration.

Spec: docs/superpowers/specs/2026-06-12-ntfy-selfhost-bidirectional-design.md Commit: 5be84fc Date: 2026-06-12

1The Argument — two lanes, one server, least privilege

OPERATOR · anywhere VPS srv908592 · proteus SESSION MAC · atlas ntfy app default server = ntfy.boas.dev operator tok ntfy server binwiederhier/ntfy auth-default-access: deny-all cache on → offline catch-up for daemon OUTBOUND · alerts-<room> · today's lane, made private fleet-pub · write-only INBOUND · cmd-<room> · the new capability operator · write fleet-sub · read-only ntfy-bridge launchd · long-poll out since= catch-up on wake tmux panes bare → coordinator hades: … → that pane stamp: Operator via ntfy: war-room-send

2Auth — three principals, no topic secrecy anywhere

PrincipalAccessLivesIf it leaks
operatorread + write, all topicsphone app onlyrotate; the only token that can steer panes
fleet-pubwrite-onlyalerts-*, hitlnono profiles / Keychainspam to your phone, nothing readable
fleet-subread-onlycmd-*bridge daemon envread your commands, can't publish or page
Rule #19 predicate: pane injection is MUTATING + PUBLIC → compensating control is real authentication (deny-all default, per-principal tokens, TLS). The old model — public ntfy.sh + secret topic names — is path obscurity and dies with this migration.

3What you type vs. what happens

PHONE → SESSION (new)
# in ntfy app, topic cmd-infra:
"merge it and close the room"
→ lands in wr-infra coordinator pane:
Operator via ntfy: merge it and close the room

# prefix targets an agent pane:
"hades: rotate the cal webhook secret"
→ lands in wr-infra:0.1
SESSION → PHONE (migrated, now private)
# hitl-notify + watchdog, config-only change:
HITL_NTFY_SERVER=https://ntfy.boas.dev
Authorization: Bearer <fleet-pub>

# blocker pushes, tap-to-respond,
# watchdog stall pages — same code paths

4Delegation — four legs, one new component

LegOwnerWorkClass
U1 serverproteusDocker on srv908592, ntfy.boas.dev via existing proxy + TLS, deny-all, cache onC new standing service
U2 authhades3 users + ACLs, tokens → vault + Keychain + profiles, phone onboardingC credential surface
U3 bridgeatlasthe only new code — ~100-line launchd daemon: subscribe cmd-*, route via panes.json, inject via war-room-sendC injection surface
U4 migrationatlashitl-notify + watchdog → private server + token header (env only)S
All Class C legs → heph review per rule #14. Rule #18: ntfy IS the prior art — adopt, not rebuild.

5E2E gate before wired=true

1Anonymous read and write on any topic → denied (negative test first)
2Session publish via fleet-pub → push arrives on phone
3Phone → cmd-infra bare → coordinator pane injection, provenance-stamped
4Phone → hades: <text> → hades pane injection
5Daemon restart → missed message replays exactly once
6Full hitl loop (notify → tap → poll) on the private server
hades flips fleet_credentials.wired=true only on this evidence.
atlas · wr-infra · 2026-06-12 · sources: spec 5be84fc · existing plumbing: hitl-notify skill, war-room-watchdog (HITL_NTFY_SERVER already externalized)