Rename listen-admin-unix to experimental-listen-admin-unix and drop the
/run/ntfy/admin.sock default: the CLI side (#2089, #2135) is not merged,
so nothing uses the socket yet. The startup warning no longer points at a
CLI fallback, and the docs say the commands do not use it yet.
Other systems keep two things apart: draining before a shutdown (ephemeral, cleared by the restart: RabbitMQ drain, CockroachDB node drain) and an operator's maintenance flag (persistent cluster state: Kubernetes cordon, Nomad eligibility, Consul maint). This PR is the former, so it is named that way: Server.Drain (synchronous again; the on/off API is gone until the persistent maintenance flag exists), Cluster.SetDraining/Draining, the read-only phase flag readOnly, /v1/ready reports "draining": true, error 50301 says the node is shutting down, and docs/config.md has a "Draining on shutdown" section. A drain duration of 0 now closes all streams at once instead of disabling the drain.
Maintenance mode is now a state that can be entered and left without a restart: Server.SetMaintenance(on) runs the phases in the background and returns at once; off stops them wherever they are and takes the node back into service (ready, writable, active in the cluster). WaitDrained waits for the drain. On SIGTERM, Drain is SetMaintenance(true) plus WaitDrained, as before. SetMaintenance(true) is idempotent and refuses with drain duration 0, on a passive node, or without another active node. This is what a later 'ntfy cluster maintenance on|off' needs.
Maintenance mode now has two phases. Entering (experimental-cluster-drain-delay, default 15s): /v1/ready fails and the node acts as passive in the cluster, but it still serves everything, so the load balancers can stop sending it requests without any request failing. Draining (experimental-cluster-drain-duration, default 1m): read-only, i.e. new subscriptions and all publishes get 503 (email gets SMTP 451, so mail servers retry instead of bouncing), and the streams are closed evenly. The defaults together fit systemd's 90s stop timeout. docs/config.md gets a maintenance mode section with the behavior table.
A node draining before a shutdown is in maintenance mode: /v1/ready reports "maintenance": true (was "draining"), and the 503 says the node is in maintenance mode. Maintenance is a runtime state next to the configured role, not a role: a later admin command can enter and leave it without a shutdown, with the same semantics. server_drain.go is now server_maintenance.go.
On SIGTERM, an active cluster node with another healthy active node first drains: new subscriptions (HTTP streams and WebSockets) get 503, which a load balancer retries on another node, /v1/ready fails with "draining": true, and the open streams are closed evenly over experimental-cluster-drain-duration (default 1m, fits systemd's 90s stop timeout; 0 disables). Clients reconnect a few at a time and resume with since=, instead of all at once. Publishes and polls are still served, since a load balancer does not retry a POST. A second signal skips the rest of the drain.
A stream registry on the server tracks open subscriber connections once each, however many topics they subscribe to.
A load balancer connects to each upstream address from one source IP, so one address caps it at ~63k connections to a node. listen-http now takes a comma-separated list (whitespace trimmed, empty and duplicate entries rejected, "-" still unsets it); Config.ListenHTTP becomes a slice, Run starts one HTTP server per address on the same handler, and stop closes all of them.
The server listens on admin-socket (default /run/ntfy/admin.sock, none
on Windows) when auth is configured. Requests on it run as a synthetic
admin, skip rate limiting and the ban feed, and may do what the network
API refuses (roles, changing or deleting admins). File mode 0660 in a
0750 directory is the authorization; peer credentials go into the audit
log on Linux and the BSDs.
A stale socket file is replaced, a socket another server answers on is
left alone, and failing to listen only logs a warning so the CLI can
fall back to the database. The systemd unit gets RuntimeDirectory=ntfy.
Move the unreleased server notes to v2.30.0 (released October 13, 2026), bump install.md to 2.30.0,
add the experimental clustering release note and a downgrade note for the message schema change
(15 to 16), and update the stale cluster comments/docs now that the mesh cluster ships.
Five option names and their validation, ahead of the implementation, so the
config surface is settled and the cluster change is only code. The
experimental- prefix is there because the names may still change.
The options parse into server.Config and the cross-field rules are enforced:
a listen address needs database-url, a secret and a node id; a secret without
a listen address is an error rather than clustering being silently off; a
wildcard bind needs an explicit advertise URL; a negative linger is rejected.
Setting experimental-cluster-listen then refuses to start the server, since
accepting the options and running single-node would look like a working
cluster. The cluster change deletes that one line.