Skip to content

ntfy auth dual-run and Play Internal testing

Last updated: 13 September 2026

This page is the operator how-to for the interim ntfy dual-run and the Play Internal track. The first Play Internal install is done (10 September 2026, OA-124-play-internal). Parts A–D (ntfy auth, users, limits, SafeCall config) are still owed.

Owed rows live in operator-actions.md. Do not close #119 or #124 from this page. Closed-app FCM/APNs is a parallel track: ntfy-fcm-apns.md. Soak, Closed testing, and store listings remain on those issues.

Do not put passwords, ntfy tokens, keystores, or Play service-account JSON in this file or in git. Record only where they live.

Official ntfy knobs: Configuration — access control and rate limiting.

Purpose and non-goals

Purpose

  1. Enable ntfy user/token auth on the client notify host.
  2. Keep anonymous read/write so existing official ntfy app subscriptions keep working (dual-run).
  3. Give authenticated SafeCall publish and Keryx subscribe their own, larger buckets so large sites stop sharing the anonymous visitor 429 limiter.
  4. Upload the recorded 1.0.0+3 AAB to Play Internal testing, add testers, and install from Play on a physical Android device.

Non-goals

  • Setting auth-default-access: deny-all (that is the later cutover, after the official ntfy app is retired).
  • Pasting Play listing copy, Data Safety, screenshots, or privacy URLs (#125).
  • Starting a Play production rollout or changing Czech Republic–only availability.
  • Play Closed testing, App Review, or public store launch (#126 / #127).
  • Firebase / FCM / APNs host setup — that is ntfy-fcm-apns.md, not this dual-run page.
  • Rebuilding with bun run release:keryx unless the recorded 1.0.0+3 AAB is gone or you are shipping real Firebase client files.
  • Inventing delivery SLOs. The numbers below are starting knobs.

This dual-run is an accepted interim. The launch protocol still requires deny-anonymous on the production host before public launch (provisioning-protocol.md). Do not treat this page as that cutover.

Already true

ItemValue
SafeCall QRsafecall.keryx.ntfy.v2 with subscribe_auth. Without token-manager credentials the server mints a local tk_local_… so the QR stays scannable; that token does not raise ntfy limits
SafeCall publishSends Authorization when server_ntfy_publish_token is set
Token mintPOST /v1/account/token as server_ntfy_token_manager_user / password. 404 falls back to tk_local_…
Typical hostCompany notify host used by SafeCall installs (often behind nginx/Caddy)
Play appKeryx for SafeCall, com.sourcectl.keryx, Czech Republic only
Play signingPlay App Signing enrolled. Upload cert SHA-256 0D:AC:61:B1:5F:85:91:3F:2B:21:20:5E:46:14:81:90:20:13:85:8B:76:D2:00:91:36:FD:30:9D:18:D8:0D:AA
Play track todayInternal testing: operator installed from Play on 10 September 2026 (OA-124-play-internal). First upload was a 1.0.0+1 draft
Working-loop AAB1.0.0+3, SHA-256 dff47ac927687950c25c574ee9ecd1283cbc3355dbcc0d6c902f74a93e488601
NASUnlock first if the share is empty: nas-unlock.md

Do not create a second Play app record.

Order of work

Do Parts A–D on the notify host and each SafeCall install. Part E is recorded for the next AAB; the first Play Internal install of 1.0.0+3 is already done.


Part A — Enable ntfy authentication (keep anonymous)

Row: OA-119-ntfy-auth.

Work on the ntfy server, not on a SafeCall box.

A1. Confirm proxy accounting

If ntfy sits behind nginx, Caddy, or another reverse proxy, server.ymlmust have behind-proxy: true (or NTFY_BEHIND_PROXY=true). Without it, every SafeCall install and every phone share one visitor bucket and you will keep seeing HTTP 429.

Restart ntfy after the change. Confirm the service sees client IPs in its logs, not only the proxy address.

A2. Turn on the auth database

In /etc/ntfy/server.yml (paths may differ on Docker):

yaml
auth-file: "/var/lib/ntfy/user.db"
auth-default-access: "read-write"

Leave auth-default-access at read-write. That is the dual-run: anonymous official-ntfy-app clients keep working. Do not set deny-all in this phase.

Restart ntfy. The user.db file is created if it does not exist.

A3. Prove anonymous still works

From a laptop, without credentials:

bash
curl -sS -o /dev/null -w "%{http_code}\n" \
  -d "dual-run anonymous probe" \
  "https://NOTIFY_HOST/PROBE_TOPIC"

Expect 200. Then confirm an existing official ntfy app subscription on a phone still receives a test publish. If either fails, stop and fix proxy/TLS before creating users.


Part B — Create SafeCall users and tokens

Row: OA-119-ntfy-users.

Commands edit auth-file and must run on the ntfy host (or in the ntfy container). Do not paste passwords or tk_… values into git, Slack, or this page. Store them in Apple Passwords and write only usernames / host / path notes under NAS secrets/keryx/ntfy/ (secret-store.md).

ntfy access tokens inherit the whole user account (topic ACL of that user). They are not yet per-topic. Dual-run still allows anonymous read/write; the users below exist so SafeCall and Keryx can authenticate and leave the anonymous visitor bucket.

B1. Users

Suggested names (change only if they already exist):

ntfy userRoleTopic ACLUsed by
safecall-publisheruserwrite (or read-write) on *SafeCall server_ntfy_publish_token
safecall-token-manageruserread-only on *SafeCall Basic auth for POST /v1/account/token
bash
ntfy user add safecall-publisher
ntfy user add safecall-token-manager
ntfy access safecall-publisher "*" read-write
ntfy access safecall-token-manager "*" read-only
ntfy user list

Do not make the token-manager an admin unless you have no other way to mint tokens. Tokens it issues inherit its permissions; read-only is what Keryx should have.

B2. Publish token

bash
ntfy token add --label="safecall-publish" safecall-publisher
ntfy token list safecall-publisher

Copy the tk_… value into Apple Passwords as the SafeCall publish token. This becomes server_ntfy_publish_token on each SafeCall install (Part D).

B3. Token-manager password

The password you set in ntfy user add safecall-token-manager is server_ntfy_token_manager_password. Username is server_ntfy_token_manager_user. SafeCall uses HTTP Basic against /v1/account/token. It does not need a token-manager access token.

B4. Prove the account API exists

bash
curl -sS -o /tmp/ntfy-token.json -w "%{http_code}\n" \
  -u 'safecall-token-manager:PASSWORD' \
  -H 'Content-Type: application/json' \
  -d '{"label":"probe","expires":4102444800}' \
  "https://NOTIFY_HOST/v1/account/token"

Expect 200 and a JSON token field. Delete that probe token afterward (ntfy token remove safecall-token-manager tk_… or DELETE /v1/account/token). A 404 means this ntfy build has no token API — SafeCall will keep issuing tk_local_… and authenticated limits will not apply to Keryx. Upgrade ntfy before continuing.


Part C — Larger limits for authenticated traffic

Row: OA-119-ntfy-limits.

Anonymous visitors stay on ntfy defaults (60-request burst, one request / 5s, 30 open subscriptions). Authenticated users without a tier still share those IP buckets. A tier is what gives SafeCall its own visitor identity and higher message headroom.

These starting knobs are for a shared company host with several large SafeCall sites. Tune after you watch 429s; do not treat them as product SLOs.

C1. Create and assign a tier

bash
ntfy tier add \
  --name="SafeCall" \
  --message-limit=100000 \
  --message-expiry-duration=24h \
  safecall

ntfy user change-tier safecall-publisher safecall
ntfy user change-tier safecall-token-manager safecall
ntfy user list

Every Keryx subscribe token minted by safecall-token-manager counts as that user. One shared token-manager for the whole company therefore shares one authenticated subscription bucket across all Keryx devices. If that becomes tight, create one token-manager user per SafeCall install instead of raising anonymous limits.

C2. Subscription / connection headroom

visitor-subscription-limit defaults to 30 open connections per visitor. A site with many official-ntfy-app topics, plus many Keryx devices on the same NAT, can hit it.

Current Keryx joins every QR category into ntfy's comma-separated subscription endpoint, so each foreground Keryx instance consumes one persistent SSE connection rather than one per category. Official ntfy clients remain outside SafeCall's control and may have different connection behavior.

Prefer not to raise the global anonymous cap unless phones are already failing to subscribe. First:

  • Confirm behind-proxy (A1) so phones are not one visitor.
  • Keep Keryx on the token-manager identity (tier) so it does not sit in the anonymous bucket.
  • Verify multiplexing with one open Keryx connection before increasing the limit; reducing published messages does not reduce subscriber connections.

If authenticated devices still hit the cap, raise only what you must, for example:

yaml
visitor-subscription-limit: 200

Restart ntfy.

C3. Exempt SafeCall server IPs from the request limiter

SafeCall publishes many POSTs from each install’s egress IP. Even with a publish token, a missing tier or a shared proxy IP will 429.

In server.yml:

yaml
visitor-request-limit-exempt-hosts: "SAFE_CALL_EGRESS_IP_1,SAFE_CALL_EGRESS_IP_2"

Use each SafeCall host’s public egress IPv4 (and IPv6 if used). Hostnames are resolved once at ntfy start. Restart ntfy after edits.

Do not globally raise visitor-request-limit-burst for anonymous traffic as the first fix.

C4. Prove the split

  1. Publish without auth from a non-exempt laptop — still 200 (dual-run).
  2. From a SafeCall install, dashboard Recheck ntfy (or a test-topic send). Delivery should be green and must not sit on HTTP 429 after a quiet minute.
  3. Publish with the publish token from an exempted SafeCall host in a burst that used to 429. Expect 200.
  4. Leave an official ntfy app subscription in place. It must still receive the same test topic.

If Recheck is 429, stop clicking it (it burns the bucket) and re-check behind-proxy, tier assignment, and exempt IPs.


Part D — Wire each SafeCall install

Row: OA-119-safecall-config.

On each SafeCall server that should publish authenticated and mint real Keryx tokens. The CLI restarts the service on config set.

bash
sudo ./util.sh config set server_ntfy_publish_token 'tk_…'
sudo ./util.sh config set server_ntfy_token_manager_user 'safecall-token-manager'
sudo ./util.sh config set server_ntfy_token_manager_password '…'

Or sudo ./util.sh config edit. These keys are restricted and are not exposed on the HTTP API.

Confirm server_ntfy_server_url is the HTTPS notify host from Part A (https://…/, including the trailing slash SafeCall already normalizes).

D1. Prove mint is no longer local

  1. Sign in to that SafeCall admin UI.
  2. Open the Ntfy / Keryx card and generate a QR (any category is enough).
  3. The payload must be safecall.keryx.ntfy.v2 with subscribe_auth of the form Bearer tk_… and not Bearer tk_local_….
  4. Do not photograph that QR into chat. Treat it as a subscribe secret.

If you still get tk_local_…, the token-manager call failed or returned 404. Check URL, Basic auth, and Part B4.

D2. Prove both clients on one host

ClientHow it authenticatesMust still work
Official ntfy appAnonymous (dual-run)Existing topics keep updating
SafeCall publisherserver_ntfy_publish_tokenRecheck / SOS / test topic
KeryxPer-QR subscribe_auth from token-managerScan QR, receive the test alert

Scan the new QR with a sideloaded or Play Internal Keryx build (Part E). Reset App first if that phone already had a tk_local_… config.


Part E — Google Play Internal testing

Row: OA-124-play-internal (done 10 September 2026). Use these steps again for the next AAB.

This is “publish to Play so we can install from Play and test,” not a Czech Republic production listing. Policy, screenshots, and Data Safety stay on #125. Closed testing and production stay later.

Signing how-to: android-signing.md. Release policy: Publishing Keryx.

E1. AAB in hand

Prefer the GLaDOS 1.2.0+7 FCM-candidate bundle. Do not upload the installed-loop 1.0.0+3 AAB (dff47ac927687950c25c574ee9ecd1283cbc3355dbcc0d6c902f74a93e488601) or the superseded 1 September 1.0.0+1 AAB (a9a4db25474a7c8ddfb1cb8811e1f087b57b9b9cb0eec10f065b47486c5bf756).

bash
ls -l keryx/build/app/outputs/bundle/release/app-release.aab
shasum -a 256 keryx/build/app/outputs/bundle/release/app-release.aab
bun run verify:keryx-aab -- --expect-fingerprint=0D:AC:61:B1:5F:85:91:3F:2B:21:20:5E:46:14:81:90:20:13:85:8B:76:D2:00:91:36:FD:30:9D:18:D8:0D:AA

SHA-256 must be f3a93e6e806a46096503b4160c0411693c26487c42dc69a0aece656ea8b90c88.

Copy keryx/build/release-evidence/1.2.0+7.txt to NAS evidence/keryx/ if that is not already done.

If the AAB is gone, rebuild on GLaDOS with bun run release:keryx -- --contracts (no bump/tag/upload) and record the new checksums in the operator log before uploading.

E2. Play Console release

  1. Open Play Console as a user who can upload to Keryx for SafeCall.
  2. Test and release → Testing → Internal testing.
  3. Create new release. Upload keryx/build/app/outputs/bundle/release/app-release.aab.
  4. Confirm the upload-key certificate still matches 0D:AC:61:B1:…:0D:AA. Do not request an upload-key reset.
  5. Version must show 1.2.0 (7) (version code 7).
  6. Release name / notes can stay internal (1.2.0+7 closed-app FCM). Do not paste public listing copy.
  7. Save and roll out to Internal testing (this is not Production).

If Create new release is disabled, finish only the Dashboard tasks that unblock testing. Do not fill Store presence, Data Safety, or production countries beyond the existing Czech Republic–only setting.

E3. Testers

  1. On the Internal testing page, open the testers tab / email list.
  2. Add the Google accounts that should install from Play (your Android phone, plus anyone in this working loop).
  3. Copy the opt-in URL (Play Console shows it on that page). Open it on the device while signed into an added account, then install Keryx for SafeCall from Play.

Internal testing is limited to the people you add. It is not a public listing.

E4. Device check (enough to tick this row)

On a Play-certified Android 12+ phone:

  1. Install from the Internal track (not a sideload of a different SHA).
  2. Grant camera and notifications when asked.
  3. Scan a QR minted after Part D (real tk_…, not tk_local_…).
  4. From SafeCall, send the test category. The alert must appear in Keryx Active while the app is open or freshly resumed.

Killed-state / Doze delivery can still fail until ntfy-fcm-apns.md (OA-119-firebase / ntfy-fcm) is done and a binary with real google-services.json is installed. That does not block OA-124-play-internal. Record the gap; do not claim production delivery.

E5. Do not do in this phase

  • Promote the release to Closed testing or Production.
  • Change countries/regions.
  • Paste #125 listing copy, screenshots, or Data Safety answers.
  • Close #124.

Later (named so they are not missed)

After this pageWhere
auth-default-access: deny-all once official ntfy app clients are goneOA-119-ntfy-cutover
Keryx Firebase, notify-host firebase-key-file, APNs .p8, swipe-away proofntfy-fcm-apns.md (OA-119-firebase / ntfy-fcm / ntfy-apns / closed-push-proof)
Play Closed testing, soak, listings, production#124 / #125 / #126 / #127