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.3.2 - 2026-02-28
Fixed
- Horde compile-time detection now checks
Horde.Registryinstead ofHorde— horde 0.10 has no rootHordemodule, soCode.ensure_loaded?(Horde)always returnedfalse, preventing the Horde backend from being compiled even when the dependency was installed
0.3.1 - 2026-02-26
Fixed
- Generated
Statestructs now deriveJason.Encoder, fixingProtocol.UndefinedErrorwhen encoding state over Phoenix channels or other JSON serialization paths
0.3.0 - 2026-02-26
Fixed
- DSL-configured
shutdown_afterandhibernate_afteroptions were ignored at runtime — processes never shut down even whenshutdown_afterwas set in theoptionsblock. The values were stored for introspection but never read during process startup.Server.init/1andServer.start_link/1now fall back to the module's DSL-configured values when options are not explicitly passed.
Added
- Built-in
state.idfield on every object'sStatestruct, automatically set to the object's ID at init time- Available in handlers,
after_load, andhandle_alarmcallbacks - Not persisted to the database — it's runtime metadata, not domain state
- Defaults to
nilin test helpers likeperform_handler/4(set it yourself if your handler readsstate.id)
- Available in handlers,
- Auto-generated DSL reference documentation via
mix spark.cheat_sheets mix docsalias now chainsspark.cheat_sheets→docs→spark.replace_doc_links- CI check to verify DSL documentation is up-to-date (
mix spark.cheat_sheets --check)
Changed
- Breaking:
field :idis now a reserved name and will raise a compile-time error. If you have an existingfield :idin your state block, rename it (e.g., to:external_idor:resource_id) before upgrading. - State is now returned as a struct (
%MyApp.Counter.State{count: 0}) instead of a plain atom-keyed map (%{count: 0})- The DSL automatically generates a nested
Statestruct module from the declared fields and defaults %{state | field: value}update syntax continues to work unchanged- State is persisted to the database as a plain JSON map (no
__struct__key) - Unknown keys in persisted state are silently dropped on load (forward-compatible with field removal)
get_persisted_state/3inDurableObject.Testingnow returns the module'sStatestruct- Breaking:
state[:field]bracket access no longer works — usestate.fielddot access instead
- The DSL automatically generates a nested
0.2.1 - 2026-02-03
Added
object_keysoption to control how string keys within field values are converted when loading state from JSON:strings(default) - leaves keys as strings:atoms!- converts to existing atoms only (raises on unknown keys):atoms- creates atoms as needed (use with caution)- Configurable per-object in the DSL
optionsblock, or globally viaconfig :durable_object, object_keys: :atoms! - DSL setting takes precedence over application config
DurableObject.Testingmodule with ergonomic test helpersuse DurableObject.Testing, repo: MyApp.Reposets up Ecto sandbox and imports helpers- Unit testing:
perform_handler/4andperform_alarm_handler/3for testing handler logic in isolation - Alarm assertions:
assert_alarm_scheduled/4,refute_alarm_scheduled/4,all_scheduled_alarms/3 - Alarm execution:
fire_alarm/4to bypass scheduler timing,drain_alarms/3for alarm chains - State assertion:
assert_persisted/4for verifying persisted state - Async helper:
assert_eventually/2for polling conditions
Fixed
mix durable_object.gen.migrationnow correctly detects version parameters in existing migrations parsed by Sourceror/Igniter
0.2.0 - 2026-01-30
Upgrading from 0.1.x
If you use the polling scheduler (DurableObject.Scheduler.Polling), the following changes are required. Users of the Oban scheduler are unaffected.
Required migration: Generate and run an upgrade migration before deploying:
mix durable_object.gen.migration
mix ecto.migrate
The task automatically detects your current migration version and generates the appropriate upgrade migration.
Idempotent handlers: The polling scheduler now uses at-least-once delivery. If a node crashes mid-handler, the alarm will be retried after claim_ttl expires (default: 60 seconds). Ensure your handle_alarm/3 callbacks are idempotent.
Added
mix durable_object.gen.migrationtask to generate upgrade migrations automaticallybaseoption forDurableObject.Migration.up/1anddown/1to support incremental upgrades- Crash recovery for polling scheduler alarms: if the server crashes or restarts while executing an alarm handler, the alarm is automatically retried
- New
claim_ttloption for polling scheduler (default: 60 seconds) - controls how long before a claimed alarm becomes eligible for retry. Lower values reduce recovery latency but increase risk of duplicate delivery if handlers are slow. - Migration version 3 adds
claimed_atcolumn todurable_object_alarmstable
Changed
- Polling scheduler now uses at-least-once semantics (handlers should be idempotent)
- Alarms are claimed before firing and only deleted on success
- Failed or interrupted alarm handlers will retry after the claim TTL expires
0.1.5 - 2026-01-28
Added
- Lifecycle guide with Mermaid diagrams covering all phases from startup through shutdown
- Mermaid diagram rendering support in ExDoc
usage-rules.mdfor LLM agent guidance via the usageRules ecosystem
Changed
- Oban scheduler
oban_instanceoption now defaults toOban, matching the common case where apps use a single default Oban instance
Fixed
- Documentation in
DurableObject.Schedulernow uses correct option names (oban_instanceandoban_queue) to match the implementation - Oban scheduler documentation now shows simple default configuration first, with customization options explained separately
0.1.4 - 2026-01-27
Fixed
- Documentation now uses correct
registry_modeconfig key instead ofcluster - README migration example now uses latest migration version instead of hardcoding
version: 1 - Installer generates migrations using latest version for
up/0 - Polling scheduler documentation shows
repoat top-level config (canonical location)
0.1.3 - 2026-01-27
Fixed
- Oban scheduler
schedule/4now passes arguments toOban.insert/2in the correct order for named instances
0.1.2 - 2026-01-27
Fixed
- Oban scheduler
cancel/3andcancel_all/2now pass an Ecto query toOban.cancel_all_jobs/2instead of a function, fixing a crash withProtocol.UndefinedErrorforEcto.Queryable
0.1.1 - 2026-01-27
Fixed
- Oban scheduler now uses correct config keys (
oban_instanceandoban_queue) to match what the installer generates
0.1.0 - 2026-01-27
Added
- Initial release
- Core Durable Object functionality with GenServer-backed instances
- Spark DSL for declarative object definitions
statesection for defining fields with types and defaultshandlerssection for defining RPC methodsoptionssection for lifecycle configuration
- Automatic client API generation from handler definitions
- Ecto-based persistence with JSON blob state storage
- Versioned migrations for database schema (v1 creates tables, v2 removes unused locking columns)
- Alarm scheduling with two backends:
DurableObject.Scheduler.Polling- Database-backed polling (default)DurableObject.Scheduler.Oban- Oban integration (optional)
- Distribution support via Horde (optional)
- Telemetry instrumentation for storage operations
- Igniter-based installation task (
mix igniter.install durable_object) - Object generator task (
mix durable_object.gen.object)
Configuration Options
repo- Ecto repo for persistenceregistry_mode-:local(default) or:hordefor distributionscheduler- Alarm scheduler backendscheduler_opts- Backend-specific options