rpushd is a reusable realtime push daemon for application integrations.
It keeps long-lived HTTP stream connections outside PHP-FPM and accepts lightweight publish events from trusted application code.
- accepts signed browser subscriptions for named channels
- accepts authenticated publish requests from trusted server-side code
- streams framed realtime messages to subscribed clients
- exposes health and internal statistics endpoints
rpushd is intended for Linux systems where you can run an additional long-lived
service next to your application stack.
Required:
- a modern Linux distribution
- an application that:
- publishes events to
rpushd - mints signed subscribe tokens for browser clients
- publishes events to
- a reverse proxy in front of the daemon
- nginx
- Apache 2.4
- HAProxy
- or an equivalent proxy
- a browser-facing public URL for the stream endpoint
Recommended:
systemdfor service management
Choose one of these installation paths:
- install from a precompiled release archive
- build from source
Release archives are published on GitHub Releases.
Each archive already contains:
rpushdrpushd.servicenginx-location.confREADME.md
Current release variants:
rpushd-linux-x86_64-gnu.tar.gz- for
x86_64Ubuntu, Debian, Arch, and other glibc-based Linux distributions
- for
rpushd-linux-x86_64-musl.tar.gz- for
x86_64Alpine Linux
- for
rpushd-linux-aarch64-gnu.tar.gz- for
aarch64/arm64Ubuntu, Debian, and other glibc-based Linux distributions
- for
rpushd-linux-aarch64-musl.tar.gz- for
aarch64/arm64Alpine Linux
- for
Example installation:
curl -LO https://github.com/SoftCreatRMedia/rpushd/releases/latest/download/rpushd-linux-x86_64-gnu.tar.gz
curl -LO https://github.com/SoftCreatRMedia/rpushd/releases/latest/download/rpushd-linux-x86_64-gnu.tar.gz.sha256
sha256sum -c rpushd-linux-x86_64-gnu.tar.gz.sha256
tar -xzf rpushd-linux-x86_64-gnu.tar.gz
mkdir -p /opt/rpushd
cp rpushd-linux-x86_64-gnu/rpushd /opt/rpushd/rpushd
cp rpushd-linux-x86_64-gnu/rpushd.service /opt/rpushd/
cp rpushd-linux-x86_64-gnu/nginx-location.conf /opt/rpushd/
cp rpushd-linux-x86_64-gnu/README.md /opt/rpushd/
cd /opt/rpushdReplace rpushd-linux-x86_64-gnu.tar.gz with the archive that matches your
platform.
After that, continue with Configuration, Service Setup, and Reverse Proxy.
Clone the repository:
git clone https://github.com/SoftCreatRMedia/rpushd.git
cd rpushdInstall a Rust toolchain and basic build dependencies.
Ubuntu / Debian:
apt update
apt install -y build-essential pkg-config curl ca-certificates
curl https://sh.rustup.rs -sSf | sh -s -- -y --profile minimal
. "$HOME/.cargo/env"
rustup default stable
rustup component add rustfmtAlpine:
apk add --no-cache alpine-sdk pkgconf curl ca-certificates rustup
rustup-init -y --profile minimal
. "$HOME/.cargo/env"
rustup default stable
rustup component add rustfmtArch:
pacman -Sy --needed base-devel pkgconf curl ca-certificates rustup
rustup default stable
rustup component add rustfmtBuild:
. "$HOME/.cargo/env"
cargo build --releaseDeploy the built files:
mkdir -p /opt/rpushd
cp target/release/rpushd /opt/rpushd/rpushd
cp rpushd.service /opt/rpushd/
cp nginx-location.conf /opt/rpushd/
cp README.md /opt/rpushd/
cd /opt/rpushdAfter that, continue with Configuration, Service Setup, and Reverse Proxy.
Choose the update path that matches how you installed rpushd.
Download and verify the new archive, extract it, stop the service, replace the deployed files, then start the service again.
Example:
cd /root
curl -LO https://github.com/SoftCreatRMedia/rpushd/releases/latest/download/rpushd-linux-x86_64-gnu.tar.gz
curl -LO https://github.com/SoftCreatRMedia/rpushd/releases/latest/download/rpushd-linux-x86_64-gnu.tar.gz.sha256
sha256sum -c rpushd-linux-x86_64-gnu.tar.gz.sha256
tar -xzf rpushd-linux-x86_64-gnu.tar.gz
systemctl stop rpushd
install -m 755 rpushd-linux-x86_64-gnu/rpushd /opt/rpushd/rpushd
install -m 644 rpushd-linux-x86_64-gnu/rpushd.service /opt/rpushd/rpushd.service.dist
install -m 644 rpushd-linux-x86_64-gnu/nginx-location.conf /opt/rpushd/nginx-location.conf.dist
install -m 644 rpushd-linux-x86_64-gnu/README.md /opt/rpushd/README.md
systemctl start rpushd
systemctl status rpushdRecommended after each update:
- compare
/opt/rpushd/rpushd.service.distwith your active/etc/systemd/system/rpushd.service - compare
/opt/rpushd/nginx-location.conf.distwith your active/etc/nginx/snippets/rpushd.conf - merge relevant changes instead of overwriting local customizations blindly
- if you changed the systemd unit, run:
systemctl daemon-reload
systemctl restart rpushd- if you changed the nginx snippet, run:
nginx -t
systemctl reload nginxYour /etc/rpushd.env file is not replaced by this process.
Pull the new source, rebuild, stop the service, replace the deployed binary, then start the service again.
Example:
cd /path/to/rpushd
git pull --ff-only
. "$HOME/.cargo/env"
cargo build --release
systemctl stop rpushd
install -m 755 target/release/rpushd /opt/rpushd/rpushd
install -m 644 rpushd.service /opt/rpushd/rpushd.service.dist
install -m 644 nginx-location.conf /opt/rpushd/nginx-location.conf.dist
install -m 644 README.md /opt/rpushd/README.md
systemctl start rpushd
systemctl status rpushdThe same review guidance applies here too:
- compare the shipped
.distfiles with your active service and proxy config - merge changes intentionally
- reload
systemdor nginx only if you actually updated their active config files
rpushd uses two different secrets:
RPUSHD_SECRET- used to verify signed browser subscribe tokens
RPUSHD_PUBLISH_SECRET- used to authenticate privileged publish and stats requests
Optional environment variables:
RPUSHD_LISTEN- default:
127.0.0.1:45831
- default:
RPUSHD_HEARTBEAT_SECS- default:
15
- default:
RPUSHD_CHANNEL_IDLE_TTL_SECS- default:
3600
- default:
Generate strong random secrets before starting the daemon.
Tip:
- if Python 3 is available, this works on Linux, macOS, and Windows:
python -c "import secrets; print(secrets.token_urlsafe(64))"- run it twice and use different values for:
RPUSHD_SECRETRPUSHD_PUBLISH_SECRET
Recommended:
- keep the daemon bound to
127.0.0.1or another private interface - store secrets in a dedicated environment file instead of hardcoding them into the unit file
- treat
RPUSHD_PUBLISH_SECRETas especially sensitive because it authorizes event injection
Example environment file:
install -m 600 -o root -g root /dev/null /etc/rpushd.env
editor /etc/rpushd.envExample /etc/rpushd.env contents:
RPUSHD_LISTEN=127.0.0.1:45831
RPUSHD_SECRET=replace-with-a-long-random-secret
RPUSHD_PUBLISH_SECRET=replace-with-a-different-long-random-secret
RPUSHD_HEARTBEAT_SECS=15
RPUSHD_CHANNEL_IDLE_TTL_SECS=3600The repository ships a hardened systemd unit.
Install it:
cp /opt/rpushd/rpushd.service /etc/systemd/system/rpushd.service
editor /etc/systemd/system/rpushd.serviceReplace the inline Environment= lines with:
EnvironmentFile=/etc/rpushd.envThen enable and start the service:
systemctl daemon-reload
systemctl enable --now rpushd
systemctl status rpushdIf your system does not use systemd, run the same binary with the same
environment variables through your native service manager.
Only expose these endpoints publicly:
/healthz/api/stream/
Do not expose these endpoints publicly:
/api/publish/api/stats
Trusted application code or internal admin tooling should call those endpoints directly through the internal daemon address, for example:
http://127.0.0.1:45831/api/publishhttp://127.0.0.1:45831/api/stats
Install the shipped snippet:
mkdir -p /etc/nginx/snippets
cp /opt/rpushd/nginx-location.conf /etc/nginx/snippets/rpushd.conf
editor /etc/nginx/sites-enabled/your-site.confInside the relevant server { ... } block, add:
include snippets/rpushd.conf;Validate and reload:
nginx -t
systemctl reload nginxThe shipped nginx snippet intentionally documents rate limiting and only exposes the public stream and health endpoints.
Enable the required modules:
a2enmod proxy proxy_http headers ssl
systemctl reload apache2Then add something like this to the relevant VirtualHost:
ProxyPreserveHost On
ProxyTimeout 75
ProxyPass /push-daemon/healthz http://127.0.0.1:45831/healthz timeout=15 keepalive=On
ProxyPassReverse /push-daemon/healthz http://127.0.0.1:45831/healthz
ProxyPass /push-daemon/api/stream/ http://127.0.0.1:45831/api/stream/ timeout=75 keepalive=On
ProxyPassReverse /push-daemon/api/stream/ http://127.0.0.1:45831/api/stream/
<Location "/push-daemon/api/stream/">
Header always set Cache-Control "no-cache, no-store, must-revalidate, no-transform"
Header always set Pragma "no-cache"
Header always set Expires "0"
</Location>Keep /api/publish and /api/stats internal-only here as well.
A typical frontend/backend split looks like this:
frontend https_in
bind *:443 ssl crt /etc/haproxy/certs alpn h2,http/1.1
mode http
acl path_push_daemon path_beg /push-daemon/
use_backend push_daemon if path_push_daemon
backend push_daemon
mode http
option forwardfor
http-reuse safe
timeout server 75s
timeout tunnel 75s
server local_push 127.0.0.1:45831 checkAgain, expose only the public stream and health paths.
Your application should be configured so that:
- browsers use the public stream base URL
- for example:
https://your-domain.tld/push-daemon
- for example:
- privileged publish requests target the internal daemon URL
- for example:
http://127.0.0.1:45831
- for example:
- subscribe tokens are signed with
RPUSHD_SECRET - privileged publish and stats requests use
RPUSHD_PUBLISH_SECRET
Available endpoints:
GET /healthzPOST /api/publishGET /api/statsPOST /api/stream/{channel}
Request body:
{
"channel": "example-channel",
"message": {
"foo": "bar"
}
}Required header:
Authorization: Bearer <publish-secret>
Request body:
{
"token": "<signed-subscribe-token>"
}The response is an application/octet-stream body using a two-byte big-endian
length prefix. Zero-length frames are heartbeats.
Required header:
Authorization: Bearer <publish-secret>
Supported output modes:
- no
modeparameter: plain text ?mode=json?mode=xml- add
?verbose=1to include per-channel details
Example plain text request:
curl -sS \
-H 'Authorization: Bearer replace-with-the-publish-secret' \
http://127.0.0.1:45831/api/statsExample plain text response:
rpushd stats
version: 1.0.2
repository: https://github.com/SoftCreatRMedia/rpushd
started_at: 1776181200
uptime_seconds: 842
active_channels: 3
retained_channels: 9
active_subscribers: 7
active_stream_connections: 7
stream_connections_total: 24
publish_requests_total: 18
published_bytes_total: 2914
auth_failures_total: 0
memory_rss_bytes: 7348224
channels: hidden (use ?verbose=1 to include channel details)
By default, /api/stats only returns aggregate counts. Detailed channel
listings are hidden unless verbose is enabled.
Example JSON request:
curl -sS \
-H 'Authorization: Bearer replace-with-the-publish-secret' \
'http://127.0.0.1:45831/api/stats?mode=json' | jqExample JSON response:
{
"active_channels": 3,
"active_stream_connections": 7,
"active_subscribers": 7,
"auth_failures_total": 0,
"channels": [],
"memory_rss_bytes": 7348224,
"publish_requests_total": 18,
"published_bytes_total": 2914,
"repository_url": "https://github.com/SoftCreatRMedia/rpushd",
"retained_channels": 9,
"started_at": 1776181200,
"stream_connections_total": 24,
"uptime_seconds": 842,
"version": "1.0.2"
}Example verbose JSON request:
curl -sS \
-H 'Authorization: Bearer replace-with-the-publish-secret' \
'http://127.0.0.1:45831/api/stats?mode=json&verbose=1' | jqExample XML request:
curl -sS \
-H 'Authorization: Bearer replace-with-the-publish-secret' \
'http://127.0.0.1:45831/api/stats?mode=xml'Example XML response:
<stats>
<version>1.0.2</version>
<repositoryUrl>https://github.com/SoftCreatRMedia/rpushd</repositoryUrl>
<startedAt>1776181200</startedAt>
<uptimeSeconds>842</uptimeSeconds>
<activeChannels>3</activeChannels>
<retainedChannels>9</retainedChannels>
<activeSubscribers>7</activeSubscribers>
<activeStreamConnections>7</activeStreamConnections>
<streamConnectionsTotal>24</streamConnectionsTotal>
<publishRequestsTotal>18</publishRequestsTotal>
<publishedBytesTotal>2914</publishedBytesTotal>
<authFailuresTotal>0</authFailuresTotal>
<memoryRssBytes>7348224</memoryRssBytes>
<channels></channels>
</stats>Example verbose XML request:
curl -sS \
-H 'Authorization: Bearer replace-with-the-publish-secret' \
'http://127.0.0.1:45831/api/stats?mode=xml&verbose=1'Useful operational checks:
systemctl status rpushdjournalctl -u rpushd -f- reverse-proxy access and error logs for
/push-daemon/ GET /api/statsfrom trusted internal tooling
At minimum, watch these signals:
- active stream count
- reconnect rate
- publish request rate
- daemon restarts or crashes
401,403, and429responses at the proxy layer
For a strong production setup:
- bind the daemon only to
127.0.0.1or another private interface - never expose the raw daemon port directly to the internet
- proxy only
/healthzand/api/stream/publicly - keep
/api/publishand/api/statsinternal-only - call
/api/publishonly from trusted application code - call
/api/statsonly from trusted internal admin tooling - store secrets outside the service unit if possible
- rotate secrets during a planned deployment window
Suggested rotation order:
- rotate the publish secret
- update the application publish side
- verify publish requests still work
- rotate the subscription secret
- allow old subscribe tokens to expire
If publish traffic ever has to cross hosts, prefer a private network, VPN, IP allowlisting, or mTLS rather than exposing privileged daemon traffic publicly.
If an application currently uses rpushd, disable that integration first.
Then remove the service and reverse-proxy configuration:
systemctl disable --now rpushd
rm -f /etc/systemd/system/rpushd.service
systemctl daemon-reload
editor /etc/nginx/sites-enabled/your-site.conf
rm -f /etc/nginx/snippets/rpushd.conf
nginx -t
systemctl reload nginxFinally remove the deployed files:
rm -rf /opt/rpushd
rm -f /etc/rpushd.envCopyright by SoftCreatR.dev.
License terms: