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 rep CLI, written in Go, creates QR subscriptions and
sends encrypted messages by title.
The project is self-hosted: the Android APK's Firebase client configuration and the sender's Firebase Admin service account must belong to the same Firebase project.
GitHub releases contain standalone CLI executables and a signed Android APK.
The Windows installer verifies release checksums, installs rep.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.
Windows, PowerShell:
$repo='flcl42/notify'; $i=Join-Path $env:TEMP 'private-notify-install.ps1'; Invoke-WebRequest "https://github.com/$repo/releases/latest/download/install.ps1" -OutFile $i; powershell -NoProfile -ExecutionPolicy Bypass -File $iPass the local Firebase Admin credential during installation when it is already available:
powershell -NoProfile -ExecutionPolicy Bypass -File $i -CredentialPath "D:\path\to\firebase-admin-service-account.json"Linux, bash, CLI only:
repo=flcl42/notify; dir="$HOME/.local/bin"; arch="$(uname -m)"; asset=rep-linux-x64; case "$arch" in aarch64|arm64) asset=rep-linux-arm64;; esac; mkdir -p "$dir"; curl -fsSL "https://github.com/$repo/releases/latest/download/$asset" -o "$dir/rep"; chmod +x "$dir/rep"; grep -qxF 'export PATH="$HOME/.local/bin:$PATH"' "$HOME/.bashrc" || echo 'export PATH="$HOME/.local/bin:$PATH"' >> "$HOME/.bashrc"macOS, zsh, CLI only:
repo=flcl42/notify; dir="$HOME/.local/bin"; arch="$(uname -m)"; asset=rep-macos-arm64; [ "$arch" = "x86_64" ] && asset=rep-macos-x64; mkdir -p "$dir"; curl -fsSL "https://github.com/$repo/releases/latest/download/$asset" -o "$dir/rep"; chmod +x "$dir/rep"; grep -qxF 'export PATH="$HOME/.local/bin:$PATH"' "$HOME/.zshrc" || echo 'export PATH="$HOME/.local/bin:$PATH"' >> "$HOME/.zshrc"Android 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.
Complete Firebase Setup, then store the local Firebase Admin service-account path:
rep credential "D:\path\to\firebase-admin-service-account.json"Create a private key for a notification title and scan the QR with the Android app or a general QR scanner:
rep create "Build Alerts"The QR is an app-specific dev.privatenotify://pair?... URL. Android registers
silently after the link is opened. Press any key in rep create to stop waiting;
the generated key remains in rep.yaml. Use --replace to rotate it.
Send later without maintaining a phone connection:
rep "Build Alerts" "The build finished."
rep listPackaged builds store rep.yaml next to the executable. It contains private
notification keys, push tokens, and the path to the Firebase Admin JSON. Back it
up as sensitive data.
Private Notify needs two JSON files from one Firebase project. They serve very different purposes:
| 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 | rep sender |
Yes; it contains a private key | Keep outside the repository and pass its path to rep 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.
-
Configure its path on every sender machine:
rep credential "D:\secure\firebase-admin-service-account.json"
The Admin JSON authorizes sends to FCM and contains a private key. Never commit
it, upload it as a GitHub Actions secret, put it in rep.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 rep 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-rep.ps1 -Version 0.2.0# Linux / macOS / WSL
./scripts/build-rep.sh 0.2.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.
Pairing creates a random 256-bit key and encodes it with the default title and a
short-lived registration URL in the QR. The Android app obtains an FCM token and
returns it to the CLI. For each send, rep encrypts the title, body, and metadata
with ChaCha20-Poly1305 and submits only the encrypted envelope and routing fields
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 rep.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.1.0 build six standalone CLI 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.