All notable changes to this project will be documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
[Unreleased]
[0.1.1] — 2026-07-06
Changed
- Relaxed the Elixir requirement from
~> 1.20to~> 1.18(the floor is the built-inJSONmodule, introduced in 1.18); CI now tests Elixir 1.18/OTP 26 through 1.20/OTP 29
Added
- Testing guide: stubbing ExNtfy's HTTP layer with
Req.Testplugs, simulating errors, and how to approach subscription testing - Subscriptions guide: pool-sizing guidance for many concurrent
subscriptions (dedicated Finch pool via
req_options: [finch: ...]) - README: scope and versioning statement;
SECURITY.mdwith a private reporting channel; Dependabot for Mix and GitHub Actions dependencies
[0.1.0] — 2026-07-05
Added
format: :ws— WebSocket subscriptions (GET /<topics>/ws) via the optional:mint_web_socketdependency, with identical semantics to the HTTP stream formats (reconnect/resume, watchdog, all consumption styles);ExNtfy.Subscriptionnow drives connections through a transport boundaryExNtfy.subscribe/2,unsubscribe/1, andExNtfy.Subscription— long-lived streaming subscriptions over/json(plus/sseand/rawviaformat:), with automatic reconnect (exponential backoff + jitter,since=<last id>resume), a keepalive watchdog (idle_timeout:), owner monitoring, and[:ex_ntfy, :subscription, ...]telemetryExNtfy.Handler— optional behaviour for callback-style consumption inside the subscription process (supervision-tree friendly)ExNtfy.stream/2— a lazyEnumerableof messages that blocks the caller and halts cleanly- Pure incremental stream parsers (ndjson, SSE, raw) with partial-line buffering across arbitrary chunk boundaries
ExNtfy.poll/2andpoll!/2— one-shot retrieval of cached messages (GET /<topics>/json?poll=1) with the fullsince/scheduled/filter surface, multi-topic support, ndjson parsing (unparsable lines skipped with a warning), and[:ex_ntfy, :poll, ...]telemetry spansExNtfy.Subscribe.Options— subscribe option schema and URL builder (path/2,topics_segment/1,to_query/1), shared infrastructure for polling and the upcoming streaming subscriptionsExNtfy.publish_file/3andpublish_file!/3— binary attachment uploads (PUT /<topic>) accepting iodata, chunk streams, or{:file, path}(streamed from disk, filename defaulting to the basename), with all publish options riding along as headersExNtfy.update/4,clear/3, anddelete/3— the sequence-ID notification lifecycle: republish with the same sequence ID,PUT /<topic>/<seq>/clear, andDELETE /<topic>/<seq>- Topic names and sequence IDs are percent-escaped in request paths
ExNtfy.publish/3,publish!/3,publish_raw/3, andtrigger/2— publishing with full option coverage (title, priority, tags, markdown, delay, click, icon, attach, filename, actions, email, call, sequence_id, cache, firebase, unified_push, template, poll_id), delegating toExNtfy.PublisherExNtfy.Publish.Options— one NimbleOptions schema validating publish options and encoding them as JSON body fields, canonicalX-headers (RFC 2047 for non-ASCII values), or query parametersExNtfy.Action.to_json_map/1andto_short/1— outgoing action-button encoding (JSON and ntfy short format), round-tripping withfrom_map/1Telemetry:
[:ex_ntfy, :publish, :start | :stop | :exception]span events with%{topic, base_url}metadataExNtfy.Client— Req-based HTTP client withnew/1andrequest/2, Basic/Bearer authentication via header or?auth=query parameter, and areq_optionsescape hatchExNtfy.Config— option resolution with per-call > application config > defaults precedence, validated with NimbleOptionsExNtfy.Error— exception type covering ntfy JSON errors, plain-text/empty error bodies, and transport failuresExNtfy.Message,ExNtfy.Action,ExNtfy.Attachment— lenient parsing of the ntfy message schema (from_map/1,from_json/1), retaining the raw map- Project scaffolded