Active Chronicle
Chronicle connects Datomic to Ruby on Rails Active Record. It maps Active Record models to Datomic facts and exposes immutable history through normal Rails query patterns. Check Datomic page
Requirements
- Ruby 3.4.5 or newer.
- Rails Active Record 7.0 or newer.
- Datomic Pro for the Docker examples and JRuby integrations.
- Java 17 for the Datomic container.
- Java 21 for JRuby 10 workloads.
Capabilities
- Active Record integration through
Chronicle::Model. - Datomic attributes with types and schema options.
- Datalog compilation for equality, comparison,
IN, projection, and pull queries. - Association preloading through Datomic pull results.
- Time travel with
.as_ofand.since. - Datomic schema and migration helpers.
- Cross-database transactions with compensating Datomic retractions.
- Retry handling with exponential backoff and jitter.
- Circuit breaker support.
- Fiber-aware backoff when a Ruby scheduler is active.
- CRuby transport through Datomic REST using EDN.
- JRuby Client API transport through a Datomic peer server.
- JRuby Peer API transport with direct access to the Datomic transactor.
- Rails initializer and migration generators.
Installation
Add Active Chronicle to the application Gemfile:
gem "active_chronicle"Then install the bundle:
bundle installConfiguration
Generate an initializer:
bin/rails generate chronicle:initializerConfigure a Datomic connection in config/database.yml.
CRuby and REST
Chronicle's CRuby transport uses Datomic's REST service. Datomic marks REST as a legacy interface, but it remains useful for existing integrations and supports the Ruby transport.
datomic:
adapter: datomic
uri: datomic:dev://localhost:4334/app_dev
client_endpoint: https://localhost:8001
rest: trueJRuby Client API
The Client API connects to a Datomic peer server. It requires the peer-server endpoint, access key, secret, and database name.
datomic:
adapter: datomic
uri: datomic:dev://localhost:4334/app_dev
client_endpoint: localhost:8998
access_key: chronicle-dev
secret: chronicle-dev-secretJRuby Peer API
The Peer API connects directly to the transactor. Select it with transport: peer.
datomic:
adapter: datomic
uri: datomic:dev://localhost:4334/app_dev
transport: peerThe included Compose examples mount the Datomic distribution at /opt/datomic.
Chronicle loads peer-*.jar from the distribution root and its dependencies from
lib/*.jar. Set DATOMIC_HOME when the distribution is installed elsewhere.
The Peer jar is not inside lib: for Datomic Pro 1.0.7705 it is
/opt/datomic/peer-1.0.7705.jar, corresponding to Maven artifact
com.datomic:peer:1.0.7705. Alternatively, resolve that artifact and its runtime
dependencies onto the application JVM classpath. Startup raises an error if
datomic.Peer cannot be resolved or Peer.connect returns no connection.
Model Integration
Include Chronicle::Model and declare the attributes stored in Datomic:
class DatomicRecord < ApplicationRecord
self.abstract_class = true
connects_to database: { writing: :datomic, reading: :datomic }
end
class HistoricalRecord < DatomicRecord
include Chronicle::Model
datomic_attribute :event_name, :string
datomic_attribute :user_id, :integer, index: true
datomic_attribute :payload, :string
endconnects_to must be declared on an abstract Active Record class. Rails 8 rejects it on a concrete model. Keep SQLite-backed models on ApplicationRecord and inherit Datomic-backed models from the abstract Datomic base.
Chronicle::Model also provides to_datoms, datomic_entity_id, and model-level .as_of and .since query entry points.
Schema and Migrations
Generate a Datomic migration:
bin/rails generate chronicle:migration create_historical_records \
event_name:string user_id:integer:indexA generated migration uses the Chronicle table definition:
class CreateHistoricalRecords < ActiveRecord::Migration[8.1]
def change
create_datomic_schema :historical_record do |table|
table.string :event_name
table.integer :user_id, index: true
table.timestamps
end
end
endRun it with:
bin/rails db:migrateTime Travel
Datomic never overwrites a fact. Each transaction produces a new database value and a transaction time. Chronicle exposes that history through relation scopes:
past = HistoricalRecord.as_of(2.hours.ago).where(user_id: 42)
recent = HistoricalRecord.since(10040).where(event_name: "login")The transport applies as_of and since to the Datomic database snapshot before it executes the query.
Cross-Database Transactions
Chronicle::TransactionCoordinator coordinates a Datomic write and a relational write. If the relational operation fails, it sends compensating retractions to Datomic and raises Chronicle::TransactionError.
Chronicle::TransactionCoordinator.transaction do |transaction|
transaction.datomic(record.to_datoms)
transaction.postgres do
AuditLog.create!(datomic_basis_t: transaction.basis_t)
end
endExamples
The repository contains three Rails applications. They are excluded from the published gem.
| Example | Ruby | Datomic API | Port | Purpose |
|---|---|---|---|---|
news_feed |
CRuby | REST | 3001 | Publish stories and inspect revision history. |
wallet |
JRuby 10 | Client API | 3000 | Record deposits and withdrawals over time. |
animal_tracker |
JRuby 10 | Peer API | 3002 | Record coordinates and draw the historical path on a map. |
cross_store |
CRuby | REST + SQLite | 3003 | Reference a Datomic customer from a SQLite purchase and benchmark coordinated writes. |
The Compose stack downloads and installs Datomic inside the Datomic container. It runs the transactor, peer server, REST service, and all four applications:
cd examples
docker compose up --buildThen open:
-
http://localhost:3000for the wallet. -
http://localhost:3001for the news feed. -
http://localhost:3002for the animal tracker. -
http://localhost:3003for the cross-store example.
The Datomic peer server listens on port 8998. The REST service listens on port 8001. The transactor uses ports 4334 and 4335.
Run the cross-store benchmark with:
docker compose -f examples/docker-compose.yml exec cross_store \
bundle exec ruby benchmark/cross_store_benchmark.rbDevelopment
Run the full local validation:
bundle exec rake qualityThis runs RuboCop and RSpec. The tracked pre-commit hook runs the same checks:
git config core.hooksPath .githooksThe CI workflow runs RuboCop, RSpec, and uploads SimpleCov results to Codecov.
Release Contents
The gem contains only lib/, README.md, CHANGELOG.md, and LICENSE.txt. It excludes specs, CI configuration, examples, the Gemfile, the Rakefile, and the gemspec.
Build the package with:
gem build chronicle.gemspecSupport Notes
- The CRuby REST transport and the JRuby Client API transport use different Datomic endpoints.
- The JRuby Peer API requires the Datomic distribution jars and a JVM.
- The Docker examples use Datomic Pro distribution downloads. Review Datomic licensing and distribution terms before use.
- JRuby and Datomic containers are not required to run the CRuby unit test suite.
Special Thanks
- Thanks to the creators of Diametric, it was the spark of inspiration for this project.
License
Chronicle is available under the MIT License.