Private Notify delivers rare, timely Android notifications without keeping a
socket open. A native Android app receives high-priority FCM data messages,
decrypts them locally with ChaCha20-Poly1305, stores them, and displays normal
Android notifications. The standalone nfy CLI, written in Go, creates QR subscriptions and
sends encrypted messages by title.
nfy has two delivery modes. The default server mode sends the already
encrypted envelope through the hosted relay at https://notify.apps.flcl.me, so
the sender needs no Google credential. direct mode keeps the original fully
local sender and uses a Firebase Admin service-account file on the CLI machine.
GitHub releases contain standalone CLI executables and a signed Android APK.
The Windows installer verifies release checksums, installs nfy.exe and the APK
under C:\Programs, adds that directory to the user PATH, and uses ADB to
install the app when an authorized Android device is connected. It removes the
retired rep.exe command after migrating its configuration.
Windows, PowerShell:
irm https://flcl.me/nfy.ps1 | iexDirect-mode users can pass the local Firebase Admin credential during installation:
$i=Join-Path $env:TEMP 'nfy-install.ps1'
Invoke-WebRequest -UseBasicParsing https://flcl.me/nfy.ps1 -OutFile $i
powershell -NoProfile -ExecutionPolicy Bypass -File $i -CredentialPath "D:\path\to\firebase-admin-service-account.json"Linux / macOS, CLI only (no sudo):
curl -fsSL https://flcl.me/nfy | shThis detects x64/ARM64, checks the release SHA-256 checksum, and installs or
updates ~/.local/bin/nfy without replacing nfy.yaml. Open a new terminal if
that directory was not already on your PATH. Set NFY_INSTALL_DIR to an
absolute path to choose another directory. Running the same command updates
the CLI to the latest release.
The short URLs redirect over HTTPS to this repository's latest released
install.sh / install.ps1. These commands execute
remote code: use them only if you trust flcl.me and the flcl42/notify
maintainers. Checksums detect corrupt/mismatched downloads, not a compromised
publisher. To inspect the Unix script before executing it:
curl -fsSL https://flcl.me/nfy -o install-nfy.sh
less install-nfy.sh
sh install-nfy.shAndroid APK only:
$repo='flcl42/notify'; Invoke-WebRequest "https://github.com/$repo/releases/latest/download/private-notify-android.apk" -OutFile .\private-notify-android.apk; adb install -r .\private-notify-android.apkAn existing debug-signed build cannot be updated by a release-signed APK. If
ADB reports a signature mismatch, uninstalling dev.privatenotify removes its
local subscriptions and messages; install the release APK and pair it again.
Create a private key for a notification title and scan the QR with the Android app or a general QR scanner:
nfy create "Build Alerts"In default server mode the QR is a normal
https://notify.apps.flcl.me/pair#payload=... URL, so Android's system scanner
can open it directly in the installed dev.privatenotify app through a verified
App Link. The HTTPS page also offers an app-opening fallback for browsers that
do not hand off the link automatically. The private payload stays in the URL
fragment, which is not sent in the HTTP request, and is removed from browser
history by the fallback page. Android registers silently after the app opens.
Press any key in nfy create to stop waiting; the generated key remains in
nfy.yaml. Use --replace to rotate it.
Send later without maintaining a phone connection:
nfy "Build Alerts" "The build finished."
nfy listThe native Android log groups messages by day and supports search, source and severity filters, unread messages, selectable details, copy/share, and individual deletion. Clearing the log leaves registered sources intact. Existing messages and pairing keys are retained when updating the app in place.
The log follows Android's system light/dark theme, including search, dialogs, menus, and readable severity colors. Changing the theme does not affect saved messages or subscriptions.
Send an optional Unicode icon (including emoji) and/or severity:
nfy "Build Alerts" "Deployment completed" --icon "✅" --severity success
nfy "Build Alerts" "Service unavailable" --icon "🚨" --severity emergency
nfy "Build Alerts" "Disk space low" --severity warningIcons accept up to 16 Unicode code points, including joined emoji. The optional
--severity values are info, success, warning, error, and emergency.
Severity sets the log icon and label color; omitted values use a neutral style.
Both fields are encrypted with the message and work in server and direct mode,
without relay changes or re-pairing. Android system notifications also display
the Unicode large icon and severity accent where supported; the small status-bar
icon stays monochrome, as required by Android. Severity does not change the
notification channel, sound, or interrupt Do Not Disturb.
Choose an optional alert mode independently of the icon or severity:
nfy "Build Alerts" "FYI: backup complete" --alert silent
nfy "Build Alerts" "Review requested" --alert vibrate
nfy "Build Alerts" "Build failed" --alert sound
nfy "Build Alerts" "Service unavailable" --alert sound-vibrate --severity emergencysound requests the default notification sound without vibration; vibrate
requests vibration without sound; sound-vibrate requests both. silent uses
a low-importance channel with neither sound nor vibration. The notification
still appears in the log and system notification shade. Omit --alert (or use
--alert default) to keep the existing channel and its settings. The mode is
encrypted with the message and needs an updated Android app; older apps ignore
the field and retain their previous alert behavior.
Each explicit mode has a separate, fixed Android channel. Android's user channel settings, notification permissions, volume, and Do Not Disturb take precedence; the sender cannot override them. Existing registrations and channel preferences are not reset. Alert mode does not change FCM delivery priority or add polling.
Packaged builds store nfy.yaml next to the executable. On first use, nfy
copies an adjacent legacy rep.yaml when nfy.yaml does not exist. The file
contains private notification keys, any direct-mode push tokens, delivery-mode
settings, and an optional path to the Firebase Admin JSON. Back it up as sensitive data. The
environment overrides are NFY_CONFIG, NFY_MODE, NFY_SERVER_URL, and
NFY_FCM_SERVICE_ACCOUNT; their legacy REP_* names remain supported.
Show or change the persisted mode:
nfy mode
nfy mode server
nfy mode directServer mode is the default for new and existing configurations. It pairs the
phone directly with the relay, then signs every relay request with an Ed25519
identity derived from the QR key. The relay stores the signing public key and
FCM routing token, but never the QR key or notification plaintext. The hosted
relay allows at most 100,000 device deliveries in total per UTC day, 1,000 per
QR subscription per UTC day, and 10 distinct QR subscriptions sending from one
source IP per UTC day. The IP-key counter is charged only by requests with FCM
targets; provisioning unused QR keys does not consume it. IPv6 clients are
grouped by /64, and stored IP identities are truncated SHA-256 hashes rather
than raw addresses.
The hosted CLI endpoint, scanner launch page, and phone registration callback
all use HTTPS. nfy connects directly to the built-in hosted relay instead of
routing it through HTTP_PROXY or HTTPS_PROXY; custom relay URLs retain normal
environment-proxy behavior. Notification title and body contents remain
end-to-end encrypted through either mode.
Override the relay temporarily or persist another relay:
nfy --server-url http://relay.example:17891 "Build Alerts" "Done"
nfy mode server http://relay.example:17891Direct mode sends to FCM from the CLI machine. It requires the Firebase Admin credential and local/LAN or ADB access during pairing:
nfy mode direct
nfy credential "D:\secure\firebase-admin-service-account.json"
nfy create "Build Alerts" --replace--mode server and --mode direct override the stored mode for one command.
This setup is required when building an Android release, operating a relay, or using direct mode. Users of the published APK with the default hosted relay do not need either file. A custom deployment needs two JSON files from one Firebase project:
| File | Used by | Secret | Destination |
|---|---|---|---|
google-services.json |
Android APK | No; Firebase embeds these client identifiers in the APK | android/app/google-services.json |
| Firebase Admin service-account JSON | Relay or direct-mode nfy sender |
Yes; it contains a private key | Keep outside the repository; pass its path to the server or nfy credential |
The Android client and Admin key must have the same Firebase project_id.
- Open the Firebase console.
- Select Create a project, enter a project name, and continue through the project wizard. Google Analytics is optional for Private Notify.
- Wait for provisioning to finish and open the project overview.
These steps follow Firebase's official Android project setup.
-
In Project overview, select Add app, then select Android.
-
Enter this Android package name exactly:
dev.privatenotify -
The nickname is optional. An SHA certificate fingerprint is not required for Firebase Cloud Messaging in this app.
-
Select Register app.
-
Download
google-services.json. Keep the filename unchanged and place it at:android/app/google-services.json
The repository ignores this file. Firebase documents it as client configuration containing non-secret identifiers, but the release workflow still stores it in a GitHub secret to keep project-specific configuration out of the public source tree.
- In Firebase, open Project settings using the gear beside Project overview.
- Open Cloud Messaging.
- Under Firebase Cloud Messaging API (V1), confirm that the API is enabled. If Firebase shows it as disabled, follow its link to enable the API in the Google Cloud API Library, then return to Firebase.
Private Notify uses the OAuth-authenticated HTTP v1 API. It does not use or need a legacy FCM server key. See Firebase's HTTP v1 sending guide and server requirements.
-
In Project settings, open Service accounts.
-
Select Firebase Admin SDK.
-
Select Generate new private key, then confirm Generate key.
-
Move the downloaded JSON to a secure local directory outside this checkout.
-
For direct mode, configure its path on every sender machine:
nfy credential "D:\secure\firebase-admin-service-account.json"
The Admin JSON authorizes sends to FCM and contains a private key. A relay reads
it only on the server. Never commit it, put it in nfy.exe, attach it to a
release, or encode it into a QR. If it is exposed, revoke that service-account
key in Google Cloud IAM and generate a replacement.
Run this before building or pairing:
$client = Get-Content .\android\app\google-services.json -Raw | ConvertFrom-Json
$admin = Get-Content "D:\secure\firebase-admin-service-account.json" -Raw | ConvertFrom-Json
$client.project_info.project_id
$admin.project_idThe two printed project IDs must be identical. A mismatch lets the Android app
register with one Firebase project while nfy tries to send through another.
The release needs the Android client config plus a persistent APK signing key. After the repository exists, run:
.\scripts\configure-github-secrets.ps1 `
-Repository flcl42/notify `
-GoogleServicesJson .\android\app\google-services.json `
-GeneratePasswordsThe helper creates an ignored Android release keystore and uploads the five build secrets documented in docs/RELEASING.md. Generated passwords are saved in an ignored, Windows DPAPI-encrypted recovery file. The helper does not read or upload the Firebase Admin service-account JSON.
The release APK from this repository is tied to the maintainer's Firebase project. Other users should fork the repository, configure their own Firebase and Android signing secrets, and publish releases from that fork.
Prerequisites are Go 1.23, JDK 17, Android SDK 36, and Android platform tools.
Build the CLI binaries for all platforms:
# Windows
.\scripts\build-nfy.ps1 -Version 0.5.0# Linux / macOS / WSL
./scripts/build-nfy.sh 0.5.0Run the Go tests:
cd rep
go test ./...Build the Android app:
npm run android:build
npm run android:installIf google-services.json is absent, the source still compiles but Firebase
registration is disabled.
Build and run the Linux relay:
cd rep
CGO_ENABLED=0 go build -o notify-server ./cmd/notify-server
./notify-server \
--listen :17891 \
--public-url http://your-server:17891 \
--state /var/lib/private-notify/state.json \
--fcm-service-account /secure/firebase-admin-service-account.json \
--ip-subscription-daily-limit 10 \
--trusted-proxies 127.0.0.0/8,::1/128The defaults enforce 100,000 total and 1,000 per-subscription FCM deliveries,
plus 10 distinct sending subscriptions per source IP, per UTC day. Relay state
persists all counters across restarts. Configure --trusted-proxies with only
the CIDRs of reverse proxies that are allowed to supply X-Forwarded-For. See
deploy/private-notify.service for a systemd
unit with automatic restart.
Pairing creates a random 256-bit key and encodes it with the default title and a
registration URL in the QR. The Android app obtains an FCM token and returns it
to either the relay or the direct-mode CLI. For each send, nfy encrypts the
title, body, and metadata locally with ChaCha20-Poly1305. In server mode it signs
the encrypted request; the relay checks the signature and quotas, then submits
the opaque envelope to FCM. Android decrypts the envelope before storing or
displaying it.
The phone keeps no custom network connection open. Android and Google Play services manage push wakeups, which is substantially cheaper at idle than a persistent application socket. Force-stop, missing notification permission, and aggressive OEM battery restrictions can still delay or block delivery.
The QR and nfy.yaml are private. Anyone who obtains a pairing key can decrypt
future messages for that subscription until the title is rotated.
Branch and pull-request workflows test the CLI and Android build. Tags such as
release/0.5.0 build six standalone nfy CLI assets, two static Linux relay
assets, and a signed APK, verify the APK signature, generate SHA-256 checksums,
and publish a GitHub release. Android
signing and Firebase client configuration use repository secrets; GitHub's
built-in token publishes the release.
Android is implemented. iOS is not implemented yet; the encrypted envelope is designed to support a future APNs provider and Notification Service Extension.
MIT.
Android toolbar icons are adapted from Lucide, with the
license bundled in android/app/src/main/assets/licenses/lucide.txt.