Version-to-version migration notes. ash_onetime follows semantic versioning: breaking DSL or contract changes bump the minor version (the library is pre-1.0), and each breaking change lands here with the exact edit to make.

The published package is v0.6.0. Set your dependency to the minor range to pick up patches automatically and review this page on each minor bump:

{:ash_onetime, "~> 0.6"}

v0.6.0 — enhancements (no upgrade action)

v0.6.0 is an additive minor bump: two new capabilities (a mix ash_onetime.doctor upgrade-preflight task and a Phoenix integration guide) plus internal perf/cleanliness gap closures. No breaking change, no DSL/contract change, no upgrade action for existing consumers.

  • mix ash_onetime.doctor --repo MyApp.Repo — a read-only preflight that checks the Ash security floor, Oban queue configuration (advisory), and prefix validity. Run it after install and after each upgrade to catch the silent-failure modes (e.g., a missing :ash_onetime_partitions queue that strands the retention-safety path).
  • Phoenix integration guide — a runnable Phoenix controller recipe wiring the Plug, the replayed?/1 signal, and the error-code → HTTP-status mapping into a complete controller pattern.
  • Cleanup delete-guard probe partition-scoped — the :complete-branch cleanup probe (install.exs) now constrains partition_date = OLD.response_partition, turning an O(partitions) scan into a point lookup (the H1 read-path tail). Same property shift as H1: partition pruning and cross-partition duplicate detection are mutually exclusive; the write path remains the authoritative guard.
  • Dead trusted_context parameter removed from the internal scope/key resolution path (admission.ex) — cleanliness; no behavior change.

v0.5.1 — internal patch (no upgrade action)

v0.5.1 is a patch: test-only hardening of the v0.5.0 closeout's five documented test gaps, plus a behavior-identical lint cleanup. No consumer-visible change, no DSL/contract change, no upgrade action.

  • Three internal helpers are now @doc false public callables for deterministic contract testing (Store.Postgres.roll_advisory_key/1, Cache.key/1, Resource.Verifier.verify_required_shape/2). They are undocumented test seams, not a supported API — do not depend on them.

v0.5.0 — security hardening from the independent code review

v0.5.0 lands the sixteen findings (M1–M5, L1–L11) from the v0.4.0 independent code review. It is a minor bump because four changes are consumer-visible and require action on upgrade; the rest are internal hardening (no consumer action). Read the four items below before upgrading.

  • CONSUMER-VISIBLE — PartitionWorker moved to a dedicated :ash_onetime_partitions Oban queue (L4). Forward partition creation is the retention-safety path; it previously shared :ash_onetime_cleanup with routine cleanup. Add the queue to your Oban config or PartitionWorker jobs sit unscheduled and bounded retention silently degrades past the install window:

    config :my_app, Oban,
      queues: [ash_onetime_cleanup: 1, ash_onetime_reap: 1, ash_onetime_partitions: 1]

    The discard-alert SQL and the partition-discard triage in operations.md now name the new queue.

  • CONSUMER-VISIBLE — protected resources declaring a reserved-named attribute now fail to compile (M2). A protected resource that declares an attribute named :key, :issued_at, :expires_at, :verification_state, or :algorithm — even with no accept on any action — now fails compilation (it previously compiled and was caught only at runtime by reject_reserved/1). If a protected resource has such an attribute, rename it (e.g. :idempotency_key instead of :key); reserved names are trusted local facts the verification path derives itself and may not come from caller input.

  • CONSUMER-VISIBLE — the :clock verify-option override is off by default in every build (M5). The gate changed from Mix.env() == :test (which a MIX_ENV=test mix deps.compile consumer could accidentally ship live) to Application.compile_env(:ash_onetime, :allow_clock_override, false). The override is now disabled unless explicitly configured, regardless of MIX_ENV. If your test suite pins verification time via the :clock option, set it in config/test.exs:

    config :ash_onetime, allow_clock_override: true

    (set BEFORE compiling ash_onetime — the gate is read at build time).

  • CONSUMER-VISIBLE — Oban worker error tuples now carry the inner reason (L5). {:error, :reap_failed} became {:error, {:reap_failed, reason}} (and likewise for :roll_partitions_failed, :cleanup_failed). Oban serializes the tuple into job.errors; the distinguishable store cause (:lock_timeout / :disconnected / :store_invariant / …) now survives exhaustion. If you pattern-match the old bare atom, update to the 2-tuple inner shape. Retry/discard semantics are unchanged.

The remaining twelve findings (M1, M3, M4, L1, L2, L3, L6, L7, L8, L9, L10, L11) are internal hardening with no consumer action — the bounded callback context, the consolidated constant-time comparator, the per-tenant partition-roll lock, the reap floor, the store telemetry event, the cache-key framing, the roll-forward namespace scoping, the change/generic_action dedup, the compile-cycle diagnostic, and the dropped dead spec arm. See the CHANGELOG for the full list.

v0.4.0 — hardening, ops-readiness, and enhancement

v0.4.0 is a hardening + ops-readiness + enhancement release. No breaking contract change for existing consumers — the minor bump carries new public observability surfaces (the telemetry default attach helper, the :worker_timeout result class, the ETS cache reference adapter) and the span-events-out-of-scope decision (H22). Existing consumers are unchanged; all new surfaces are opt-in.

  • External-recovery adversarial-absence proof + normative doc (H10): a test proving the re-execution invariant against a lying-:absent adapter, and a normative section in documentation/external-effects.md stating the adapter MUST prove absence and the peer MUST enforce idempotency by operation key. No runtime guard — the trust is inherent to the design (ADR-0001).
  • Worker timeout distinguished from disconnect (H11): the committed-claim worker's 30s timeout now surfaces as a distinct :worker_timeout result_class on [:ash_onetime, :store_uncertainty], separate from :disconnected and :unknown. All three fail closed; the distinction is operational triage (pool/lock contention vs network partition).
  • Oban worker backoff + discard alert (H20, ADR-0005): the three maintenance workers (Cleanup, Partition, Reap) declare a bounded, jittered backoff/1 (30–120 s) instead of the default exponential, so transient failures retry within the retention window. A documented discard-alert SQL names the operational signal for a stranded partition roll.
  • Telemetry default attach handler (H21): AshOnetime.Telemetry.attach/0 — an opt-in helper that routes the closed event surface into a downstream :metric stream for a consumer's own aggregator. No telemetry_metrics dependency.
  • Telemetry span structure (H22): documented that the library emits point events only (never span events), with the reason (span/3 cannot preserve the value-free invariant) and a recommended consumer-applied :telemetry.span/3 wrapper.
  • Operations runbook (H23): three named procedures (backlog-stuck, partition-discard-detected, pool-saturated) with exact SQL/telemetry queries.
  • ETS cache reference adapter (H30): AshOnetime.Cache.Ets — bounded, TTL-aware, supervised, no third-party dependency. Makes the cache-degradation path reachable.
  • Admission unit tests (H31), key_source/claim property tests (H32): direct test coverage for the pure decision functions and the security-boundary invariants.
  • Runtime security-surface docs (H33): @doc on token.ex, key_source.ex, fingerprint.ex, telemetry.ex public functions.
  • CI-matrix-asserted compatibility documented (H34): CONTRIBUTING names the CI matrix as the guard against transitive semantic drift (not the dep bounds).

Ash floor raised to 3.31.1 (v0.3.0, security-driven)

v0.3.0 tightens the Ash requirement from >= 3.29.3 and < 4.0.0 to >= 3.31.1 and < 4.0.0. Two Ash advisories published during the v0.2.0 window affect Ash below 3.31.1, both patched in 3.31.1: EEF-CVE-2026-70395 (predicate injection in manage_relationship belongs_to lookup disclosing secret lookup keys) and EEF-CVE-2026-69659 (memory exhaustion via unbounded keyset-cursor deserialization). A security library must not admit a vulnerable floor. Bump Ash to ≥ 3.31.1, then bump ash_onetime:

{:ash_onetime, "~> 0.3"}

See ADR-0004 (Security-driven Ash floor). The CI compatibility matrix narrows from [3.29.3, 3.30.1, latest] to [3.31.1, latest].

Forward response-partition maintenance (apply once to existing installs)

Two data-layer fixes shipped for the response store: the response_partition index (cleanup's partition-empty check was a full scan) and forward monthly partition creation (payloads past the install window routed to _default and were never dropped, silently defeating bounded retention). Greenfield installs on the current version get both automatically from the install migration. Existing installs should generate and run the forward migration once:

mix ash_onetime.gen.roll_forward --repo MyApp.Repo --months 18
mix ecto.migrate

It adds the index, back-fills the elapsed+forward partitions, and drains past-retention payloads stranded in _default. After that, schedule mix ash_onetime.roll_partitions (or AshOnetime.Oban.PartitionWorker) on a cadence ahead of your retention horizon — see Operations. This is additive (no DSL/contract change) and does not require a dependency version bump.

DPoP replay fence (commit: :independent)

Additive: a new opt-in commit: option on :one_time_nonce protections (default :with_action). No migration, no schema change, no new error code. Existing nonce consumers are unchanged. Adopt by adding commit :independent to a nonce protect block whose proof should survive action-body failure (RFC 9449 §11.1). See ADR-0003 (Independent-commit nonce) and the operational characteristics for pool-sizing notes. Declaring commit: on :idempotency is now a compile error (idempotency commits with its effect).

Single limits surface (shipped in v0.1.0)

The dual limits surface collapsed to one protect-level vocabulary in v0.1.0. Response-size limits are declared on the protect block, not the response entity:

protect :charge do
  strategy :idempotency
  # ...
  response MyApp.ChargeCodec, fields: [:id, :status], classify: MyApp.ChargeClassifier
  limits max_response_bytes: 8_388_608
end

The protect limits: option accepts the full 11-key union — the key/verification/cache keys (max_key_bytes, max_token_bytes, max_scope_components, max_fingerprint_bytes, verifier_timeout_ms, max_cache_entry_bytes) and the response-payload keys (max_response_bytes, max_response_depth, max_response_nodes, max_response_entries, max_response_scalar_bytes). All keys are validated at compile time. If you are upgrading from a pre-v0.1.0 snapshot, move any response ..., limits: [max_response_*: ...] onto protect ..., limits: [...].

This change is additive in coverage (no limit is lost) and removes a redundant configuration surface where two places could spell overlapping limits.

Between releases

Non-breaking additions (new optional integrations, new introspection helpers, new guides) ship in patch or minor releases and need no migration. Check the CHANGELOG for the full list per release.

If you depend on a private (non-documented) module or function, it may change in any release — the public contract is the documented DSL, the modules in the API reference, and the behaviours (AshOnetime.Codec, the classify/2 contract on AshOnetime.ResponseClassifier, verification callbacks returning AshOnetime.Verified).