Conversation
This was referenced Aug 19, 2026
Adds an optional AppleOptions struct (critical, sound, volume) to the message model, persisted as a JSON TEXT column following the "actions" precedent. Persistence matters because delayed messages are re-sent from the cache and must not silently lose the critical flag. Part of the X-Apple-(Critical|Sound|Volume) header support discussed in binwiederhier#1235.
An explicit X-Apple-Critical header (bool) creates the AppleOptions on the message; sound and volume are only read for critical messages, since they have no effect otherwise. Unlike the lenient cache/firebase bool params, invalid values return HTTP 400: a silently dropped critical flag on an alarm is worse than a rejected publish. The JSON publish endpoint accepts the same options as a nested "apple" object. Relates to binwiederhier#1235.
…ream forwards Sets the critical sound (with configurable name and volume, defaulting to "default"/1.0), apns-priority 10 and interruption-level "critical" in the APNs payload. Precedence: an explicit X-Apple-Critical value always wins; without it, max priority (5) messages are critical for backwards compatibility. Two paths would otherwise silently lose the flag: - toPollRequest: topics without anonymous read access turn messages into poll requests, which must keep the iOS options - forwardPollRequest: self-hosted servers forward poll requests to an upstream server, which delivers the APNs alert and needs the flag Relates to binwiederhier#1235.
Fixes found in review, squashed into one commit: - handlePublishInternal replaced a poll request publish (X-Poll-ID) with a fresh message, discarding the parsed iOS options; the receiving server now keeps them, so forwarded poll requests stay critical - forwardPollRequest only forwarded an explicit critical flag; it now uses appleCritical(), so the priority-based fallback reaches the upstream server too (appleCritical moved to server.go for nofirebase builds) - The JSON publish path used the model struct directly, so "volume":0 and an empty "apple" object silently bypassed validation and the priority fallback; a dedicated input struct with pointer fields fixes both - X-Apple-Critical is now case-insensitive like other bool parameters, and sound names reject control characters (MIME-decoded headers) - Error texts say "parameter" instead of "header", matching the codebase; test names follow the TestServer_*/TestStore_* conventions Relates to binwiederhier#1235.
…t for iOS critical alerts - docs/publish.md: note the new iOS default behavior in the max priority row - docs/config.md: describe the forwarded critical flag in the iOS instant notification setup, including the behavior with older upstream servers - TestStore_MessageFieldRoundTrip: populate and verify the new apple field Relates to binwiederhier#1235.
tobehn
force-pushed
the
apple-critical-headers
branch
from
August 30, 2026 14:53
2d4d7db to
749599b
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Implements the server side of iOS critical alerts using the header approach from the Jun 30 discussion in #1235: the publisher decides whether a message breaks through Focus, Do Not Disturb and the mute switch. Supersedes #1778. This is the server contract for the iOS work in #1680 (2A).
Behavior — an explicit
X-Apple-Criticalalways wins; without it, max priority stays critical, matching the already-merged app behavior (binwiederhier/ntfy-ios#44):X-Apple-Critical1/yes/true0/no/false51–4X-Apple-SoundandX-Apple-Volume(0.0–1.0, enforced by iOS) are optional. Also available as query params and as a nestedappleobject in JSON publishing; subscribers see the options in the message JSON (for the NSE).What the commits cover:
sounddict withcritical/name/volume,interruption-levelinside theapsdict,apns-priority: 10toPollRequest(topics without anonymous read),forwardPollRequest(self-hosted → upstream, including the priority-based fallback) and the receiving side inhandlePublishInternalOpen question: the priority-5 fallback changes behavior for existing publishers without a server config gate. Should it ship enabled, behind a config option, or header-only? Happy to adjust.
Limitations: delivery is verified down to the exact APNs payload (unit + two-server relay tests, real v15→v16 migration, race detector, sqlite/postgres), but I cannot verify on-device behavior — that needs an app build with the critical alerts entitlement. Behavior on non-entitled installs (current App Store app) should degrade to a regular alert, but is untested. CLI (
ntfy publish) and Go client support can follow in a separate PR if needed.