Introduction ##
Uniqush (\ˈyü-nə-ku̇sh\ "uni" pronounced as in "unified", and "qush" pronounced as
in "cushion") is a _free_ and _open source_ software system which provides
a unified push service for server side notification to apps on mobile devices.
The uniqush-push API abstracts the APIs of the various push services used
to send push notifications to those devices. By running uniqush-push on the
server side, you can send push notifications to any supported mobile platform.
### ⚠️ Status: read before deploying
> This project was dormant from 2020 to 2026, and in that time some of the
upstream APIs it depends on were shut down.
> - APNs — repaired, and verified as far as Apple allows without an account.
As of 2.8.0 it uses the HTTP/2 API by default and sends the apns-push-type
header that iOS 13+ requires. Earlier releases defaulted to the binary
protocol, which Apple switched off on 31 March 2021, and could not deliver
at all.
> go test ./srv/apns/ now drives the real HTTP/2 transport against a
simulator that enforces Apple's documented contract, and
go test -tags apns_live ./srv/apns/http_api/ checks reachability and error
parsing against Apple's real sandbox, which answers unauthenticated
requests.
> Token (.p8) authentication is now supported alongside certificates:
/addpsptakesauthkey,keyidandteamid. To move an existing
certificate-based service across, add replace=true; subscriptions survive
it. Do not use /rmpsp for this — see
docs/delivery-point-rebinding.md.
> What remains is delivery to a device, and that needs a paid Apple Developer
Program membership — Apple sells no free route to one. **If you have an
account, a report either way would be genuinely useful**; see
docs/apns-verification-plan.md for exactly
which cases still have no coverage.
- FCM — migrated to HTTP v1, and verified against Google. The legacy
endpoint it used was decommissioned on 20 June 2024. /addpsp now takes
projectidandcredentialsfileinstead ofapikey; devices do not need
to re-subscribe. gcm is now an alias for the same backend.
> Confirmed working end to end on 1 September 2026, against a real Firebase
project: pushes were accepted by FCM and delivered to a browser. Unlike APNs
this needs no paid account, so you can repeat it yourself —
examples/fcm-demo is the ten-minute version, and
go test -tags fcm_live ./srv/fcm/ exercises the send path against Google
directly. Delivery to an Android device has not been checked yet; the
browser path covers the whole server side but not the android block.
- ADM is believed to still work, but has not been re-verified.
- **UnifiedPush / Web Push — new, and the one backend with no vendor
dependency.** See below.
> Building requires Go 1.26 or newer. If you are upgrading,
docs/upgrading.md walks through what changes and what
needs action, release by release.
Supported Platforms ##
- FCM from Google for the Android platform (
gcmis an alias) - APNS from Apple for the iOS platform
- ADM from Amazon for Kindle tablets
- UnifiedPush / Web Push, for de-Googled Android, Linux desktops and browsers
Quickstart with Docker ##
compose.yaml runs the published image with a Redis that keeps its data in a volume. From a checkout:
docker compose up -d
curl http://localhost:9898/health
/health answers {"status":"ok","database":"ok",...} once Redis is
reachable. The API is published on localhost only, for the reason given under
[WebFrontend] addr below.
To send a real push, run the UnifiedPush demo
(needs Go 1.26+). It registers a provider with /addpsp, subscribes an
endpoint on ntfy.sh with /subscribe, calls /push, then fetches the message
back from ntfy.sh and decrypts it:
go run ./examples/unifiedpush-demo
curl http://localhost:9898/psps
To register a service of your own, mint VAPID keys with the image and follow UnifiedPush / Web Push from step 2:
docker compose run --rm --no-deps uniqush-push -generate-vapid-keys
docker compose down stops it; add -v to delete the Redis volume too.
Building and running ##
uniqush-push is a single binary. Building it needs Go 1.26 or newer; running
it needs a Redis server.
git clone https://github.com/uniqush/uniqush-push.git
cd uniqush-push
go build # produces ./uniqush-push
or, without a checkout, go install github.com/uniqush/uniqush-push@master.
Copy conf/uniqush-push.conf to
/etc/uniqush/uniqush-push.conf, the default location, or point at it with
-config. The settings you are most likely to touch:
[WebFrontend] addr— where the REST API listens. It defaults to
localhost:9898, and there is a reason to leave it there: **the API has no
authentication.** Anything that can reach /addpsp can decide where a
service's pushes go. Bind to localhost, or put it behind a reverse proxy that
authenticates.
[Database] host,portandpassword— the Redis server (port=0means
slave_host/slave_port route reads to a replica.
logfile, and aloglevelper section:alert,error,warn,info
standard or verbose) or debug.
Make sure Redis has persistence enabled. Every subscription lives there, and by default Redis only snapshots periodically.
Then:
uniqush-push # or: uniqush-push -config /path/to/uniqush-push.conf
curl http://localhost:9898/version
uniqush-push -version prints the version and uniqush-push -generate-vapid-keys
mints a key pair for a Web Push provider; both exit without starting the
server. go test ./... runs the test suite, including the APNs conformance
suite against a local simulator; the live tests that talk to Apple and Google
are behind build tags and described in
docs/apns-verification-plan.md and
examples/fcm-demo.
Running it as a service ###
The .deb and .rpm install a systemd unit at
/lib/systemd/system/uniqush-push.service, and do not enable it: uniqush needs
a Redis to talk to and at least one push service provider before it is any use,
so starting it the moment the package lands would only produce a failing
service. When the config is ready:
systemctl enable --now uniqush-push
systemctl status uniqush-push
It runs under a transient unprivileged user (DynamicUser), so there is no
account to create and nothing left owned by one afterwards, and systemd creates
/var/log/uniqush for it. Adjust it with systemctl edit uniqush-push rather
than by editing the unit, which a package upgrade replaces.
Only those packages install it. The release tarball does not carry the unit,
because install.sh puts the binary in /usr/local/bin while the unit's
ExecStart names /usr/bin/uniqush-push — a file that needs editing before it
works is worse than one you fetched deliberately. Installing that way, or from
source, copy it out of the source tree at
conf/systemd/uniqush-push.service and
point ExecStart at wherever your binary is.
UnifiedPush / Web Push ##
UnifiedPush is a decentralised push standard: the
user picks the push provider rather than the app developer, and it works on
devices with no Google services at all. Its application-server side is plain
Web Push — RFC 8030 delivery,
RFC 8291 aes128gcm payload
encryption and RFC 8292 VAPID
authentication — so the same backend also drives browser Web Push.
It is registered under two names, webpush and unifiedpush. They behave
identically; pick one per service and stay with it, since the name is part of a
subscription's identity in the database.
Unlike the other backends, this one needs no vendor account and no certificate.
1. Generate a VAPID key pair. These identify your server to push providers. Some providers reject registrations without them.
$ uniqush-push -generate-vapid-keys
vapidpublickey=BIknD72EXwC1CC5WamGPDn4YbTV7o6yE_zMNNJO2xNMGyy4sz6egSmwFhH8lxllQqvqInrkqyKwnuy1Q1vmkevk
vapidprivatekey=NOHiudJNUw6IEf0SN0jYTascVt68R0sQJxMWSbVRWM4
2. Create the push service provider. subscriber is the VAPID contact — a
bare email address or an https:// URL, not a mailto: URI.
curl http://localhost:9898/addpsp \
-d service=myservice \
-d pushservicetype=unifiedpush \
-d vapidpublickey=BIknD72... \
-d vapidprivatekey=NOHiudJ... \
-d [email protected]
3. Subscribe a device. The app's UnifiedPush connector library produces all three values; your app just forwards them to your server.
curl http://localhost:9898/subscribe \
-d service=myservice \
-d subscriber=alice \
-d pushservicetype=unifiedpush \
-d endpoint=https://ntfy.sh/up?id=... \
-d p256dh=BNcRdreALRFXTkOO... \
-d auth=tBHItJI5svbpez7KI4CCXg
4. Push. Payload fields are JSON-encoded and delivered encrypted; the app on
the device decrypts them. Pass uniqush.payload.webpush instead to send a raw
body verbatim.
curl http://localhost:9898/push -d service=myservice -d subscriber=alice -d msg=hello
A note on SSRF ###
For every other backend the destination host is a constant compiled into
uniqush. Here it comes from whoever called /subscribe, so uniqush refuses by
default to POST to addresses that are not globally routable — loopback, RFC 1918,
link-local (including the 169.254.169.254 cloud metadata endpoint), and the
IPv6 equivalents. Redirects are never followed, and the check runs before every
push rather than only at subscribe time, so DNS rebinding does not defeat it.
Self-hosted push servers on a private network are a supported UnifiedPush
setup, so this can be relaxed per service in uniqush-push.conf with
allow_private_addresses, ideally alongside an allowed_hosts list.
Message size ###
Every message is padded to fill one RFC 8188 record, so the POST body is the
same size whatever the payload: a wakeup ping and a full notification are
indistinguishable on the wire. record_size sets it, in the [webpush] or
[unifiedpush] section.
The default is 4096, the largest UnifiedPush permits, which puts the payload
ceiling at 3993 bytes and matches what every other application server emits.
That last part matters more than it sounds: google/tink's apps-webpush
requires the record size in the header to equal the 4096 it expects and
rejects anything smaller, so a smaller record is unreadable to clients built on
it. UnifiedPush's own connector carries a patched fork and is fine from 3.0.4
onwards.
Setting record_size=2048 halves egress, which is worth it for a server sending
mostly wakeup pings to clients you control. It also halves the payload ceiling,
to 1945 bytes.
APNs ##
/addpsp for apns takes the usual cert, key and bundleid. Two optional
settings control where HTTP/2 pushes actually go:
endpoint— the base URL to push to, e.g.https://api.sandbox.push.apple.com.
/3/device/, so it must have no path, query or
fragment. Omitted, pushes go to the environment recorded at /addpsp — the
one sandbox selected, or the one a legacy addr implies for a provider
registered before uniqush recorded it.
cacert— a PEM bundle to verify that endpoint against. Prefer this to
skipverify when testing: the certificate and hostname are still checked, so
a simulator has to present one you actually issued.
Both are cleared if a later /addpsp omits them, the same way bundleid
behaves.
/subscribe accepts a bundleid too, for one device. HTTP/2 requires an
apns-topic on every push and uniqush takes it from the provider, so a
certificate valid for several bundle ids — an app and its enterprise or
release-testing builds — used to need a service per bundle id, with each device
subscribed to the right one. A device that names its own bundle id is pushed to
under that; every other device uses the provider's, so nothing existing
changes.
A note on endpoints ###
An endpoint decides where every push for a service goes. It carries device
tokens and the notification payload, and for a certificate provider it is also
where the APNs client certificate is presented. So unlike the UnifiedPush case
above — where the destination comes from a subscriber and the defence is an
address policy — uniqush refuses any host outside push.apple.com unless
you opt in:
[apns]
allow_non_apple_endpoints=true
The check runs before every push as well as at /addpsp, because a provider
loaded from Redis never passes through the code that validates it. An address
policy would be the wrong tool here: the legitimate destinations are a simulator
on localhost or a relay inside your network, which is exactly what such a policy
blocks, while an attacker-controlled public host is exactly what it permits.
skipverify is refused outright for Apple's own hosts. It predates the HTTP/2
path and was silently ignored there, so operators who set it years ago for the
binary-protocol simulator still have it stored; honouring it now would have
disabled certificate verification on connections to Apple.
Health checks ##
/health answers 200 when redis is reachable and 503 when it is not, with
the reason in a JSON body. Point a readiness probe at it:
readinessProbe:
httpGet:
path: /health
port: 9898
Not a liveness probe. A liveness probe should not depend on another service: during a redis outage this would restart every uniqush over and over, which neither fixes redis nor helps the backlog drain when it returns.
Redis is the only dependency checked, because it is the only one uniqush cannot work without at all. It deliberately does not probe Apple or Google — an endpoint that did would report their outage as this instance being unhealthy, and taking instances out of rotation is the wrong response to a push service being slow.
Timeouts ##
How long one request to a push service has to complete is request_timeout, in
seconds, in that service's section of uniqush-push.conf — [apns], [fcm],
[gcm], [webpush] or [unifiedpush]. The defaults are 20 seconds for APNs
and 30 for the rest.
The value worth changing is the one that does not match your caller. /push
answers when the push services do, so a client that gives up after five seconds
against a uniqush that waits thirty spends twenty-five seconds waiting for an
answer nobody is listening for. Lowering it turns that into a reported failure
you can retry. Raising it suits a slow or congested link, and Web Push most of
all: that endpoint belongs to whoever called /subscribe, so it may be a
self-hosted server rather than a CDN.
A value outside 1–300 falls back to the default rather than being clamped, and so does one that will not parse — as with every other option here, deleting the line restores the default rather than leaving the last value that worked in force.
FAQ ##
- Q: Is this a hosted push service, like OneSignal or Airship?
- A: No.
uniqush-pushis a program that runs on your own server, the way
- Q: Is it a library, then? Do I need to write Go?
- A: No. It is a daemon with a REST API over plain HTTP, so any language with
curl is enough to try it. You don't need Go
to run it either: each release
ships a .deb, an .rpm and a tarball, and the Docker image is on ghcr.io (see
Quickstart with Docker). Go is only needed to build
from source.
- Q: Where is the API documented?
- A: docs/api.md is the reference: every endpoint, the
/addpsp and /subscribe, what /push
accepts, and the response formats. The older reference on uniqush.org
predates this release; its FCM and APNs registration examples will not work
as written.
- Q: I'm upgrading from 2.7.0 or earlier. Will my subscriptions survive?
- A: Yes; no device has to re-subscribe for anything in this release. FCM
/addpsp with projectid and credentialsfile in
place of apikey; APNs providers keep working with their existing
certificate, and can move to a .p8 key with replace=true. Do not use
/rmpsp to change a provider's credentials: in 2.7.0 that silently deleted
every subscription in the service. docs/upgrading.md
has the details.
- Q: My services are registered as
gcm. Do I have to rename them? - A: No, and you should not:
gcmis an alias forfcm, and the name is part
/addpsp with
the new parameters.
- Q: Is the API authenticated?
- A: No. It binds to
localhost:9898by default so that only local processes
/addpsp controls where every push
for a service is sent. The same reasoning is why VAPID keys are generated by
a command-line flag rather than an endpoint, and why a non-Apple APNs
endpoint has to be enabled in the config file.
- Q: What is the difference between Uniqush and
uniqush-push? - A: Uniqush was conceived as a suite of components for mobile messaging;
uniqush-push is the piece that was built, and the names are used
interchangeably.
- Q: Something doesn't work. Where do I ask?
- A: The issue tracker.
Contributing ##
Issues and pull requests are welcome; CONTRIBUTING.md says how to build, test and submit a change. Report security problems privately, as SECURITY.md describes.
Related Links ##
- This story may help you to understand
- API reference, upgrade notes, and the rest of docs/
- Documentation on uniqush.org (older; being updated)
- The Uniqush blog announces releases.
- Redis persistence