Audit trails and record versioning in Rails: PaperTrail, Audited and Logidze

Sooner or later someone asks who changed a price, when an account was closed, or what a record looked like last Tuesday. Rails keeps no history of its own, so the answer comes from a gem. Three cover most applications, and the real choice between them is where the history is written: by Ruby callbacks into a separate table, or by the database itself.
Release figures are from RubyGems.org, November 2026. The latest Rails release at the time of writing is 8.1.4, from September 2026.
Audit trail or versioning?
The two needs overlap but are not the same. An audit trail answers "who did what, and when", and is read by support staff, compliance or a security review. Versioning answers "what did this record look like before", and is used to show a diff, undo a mistake or restore a deleted row. All three gems below store enough to do both; they differ in how much effort each side takes.
PaperTrail: a versions table and reify
PaperTrail (17.0.0, October 2025) is the most widely installed of the three. Its README states compatibility explicitly: version 17 requires Ruby 3.2 or later and Active Record 7.1 to 8.1. Installation adds one polymorphic versions table shared by every tracked model:
bundle exec rails generate paper_trail:install --with-changes bundle exec rails db:migrate class Product < ApplicationRecord has_paper_trail ignore: [:updated_at], skip: [:internal_notes] end class ApplicationController < ActionController::Base before_action :set_paper_trail_whodunnit end
Each version stores the event (create, update, destroy), the whodunnit (by default the ID returned by current_user) and the state of the record before the change. That last design choice is what makes PaperTrail strong at versioning: version.reify rebuilds the old object, product.paper_trail.previous_version walks back through history, and a destroyed record can be restored from its last version. The --with-changes option adds an object_changes column with the diff of each update.
version = product.versions.last version.event # "update" version.whodunnit # "42" old = version.reify # the product as it was before the update old.save! # revert
Points to check: objects are serialized as YAML by default, and the README recommends json or jsonb columns if you want to query changes with where_object_changes. Tracking associations was extracted into a separate gem, paper_trail-association_tracking, and is not part of the core. A version_limit setting caps how many versions each record keeps.
Choose it when you need to show or restore earlier versions of records, and want the most documented option.
Audited: an audits table with comments and associations
Audited (5.8.0, November 2024) began life as the acts_as_audited plugin and is published from the collectiveidea organisation on GitHub. Its gemspec accepts Active Record from 5.2 up to, but not including, 8.2, so it installs on Rails 8.1; the README's compatibility line, written for 5.6, still lists Rails 7.2 as the newest tested version, so run your own test suite before relying on it.
rails generate audited:install --audited-changes-column-type jsonb rails db:migrate class Account < ApplicationRecord audited except: [:password_digest], associated_with: :company end class Company < ApplicationRecord has_associated_audits end
Each audit stores the action and a hash of changes, so it reads naturally as a log: account.audits.last.audited_changes returns {"plan" => ["basic", "pro"]}. Within a request, the current user is recorded automatically through current_user; outside one, a block such as Audited.audit_class.as_user(admin) { ... } sets it. Audited's distinctive features are on the audit side: an audit_comment attribute, with an option to require a comment on every change, and associated_with, which lets you list every audit of every account belonging to one company. Previous states are available through revisions and revision_at(date).
Releases have slowed: the latest is two years old. That is not a problem for a mature gem with a narrow job, but it is the kind of signal the gem evaluation checklist asks you to weigh, especially before a Rails upgrade beyond 8.1.
Choose it when the history is mainly read by people, who need to see who changed what, with a reason, grouped by parent record.
Logidze: history kept by PostgreSQL triggers
Logidze (1.4.1, June 2025), built by Evil Martians, takes a different route. It works only with PostgreSQL (version 10 or later, per its README) and installs database triggers that write each change into a log_data JSONB column on the row itself, so no extra table is needed. The 1.x line requires Rails 6.0 or later.
bundle exec rails generate logidze:install
bundle exec rails generate logidze:model Post --only=title,body,status
bundle exec rails db:migrate
post.at(time: 2.days.ago) # the post as it was then
post.diff_from(time: 1.hour.ago) # what changed since
post.undo! # step back one version
Logidze.with_responsible(current_user&.id) { post.save! }
Because the trigger runs in the database, changes made by update_all, raw SQL or another application sharing the database are recorded too; the README even documents Logidze.without_logging for the bulk updates you do not want logged. Logidze uses SQL functions and triggers, so you either switch to structure.sql for the schema dump or add the fx gem, which keeps schema.rb working. If a growing log_data column worries you, a --limit option caps the number of versions per row, and a detached mode stores history in a separate logidze_data table instead.
Choose it when you run PostgreSQL, write data outside Active Record callbacks, or care about the write overhead of an extra INSERT per change.
The changes callbacks never see
PaperTrail and Audited hook into Active Record callbacks. The Rails guide to callbacks lists the methods that skip callbacks entirely, among them update_all, update_column, update_columns, delete, delete_all, insert_all and upsert_all. Any of those, a data migration in SQL, or a console session with tracking switched off, leaves no trace in a callback-based audit log. If the log has to be complete, for example for a regulatory requirement, either forbid those methods on audited models in code review or choose a trigger-based approach.
Practical advice for any of the three
- Exclude secrets and noise. Leave password digests, tokens and
updated_atout of the log withignore,skip,exceptor--except. An audit table full of credential hashes is a liability. - Record the actor everywhere. Controllers are covered, but jobs, rake tasks and the console are not. Wrap them with
PaperTrail.request(whodunnit: ...),Audited.audit_class.as_userorLogidze.with_responsible. - Plan for growth. History tables become the largest in the database. Index the columns you search by, cap versions where the law allows, and decide on a retention period before the table reaches millions of rows.
- Show it in the admin. Support staff use history far more than developers do; most of the Rails admin panels can display a versions or audits relation on a record page.
- Test it. A model test that updates a record and checks the new version or audit catches a missing
has_paper_trailorauditedline after a refactor.
Summary
| Gem | Storage | Latest release | Best fit |
|---|---|---|---|
| PaperTrail | Shared versions table, callbacks | 17.0.0, October 2025 | Versioning, reverting and restoring records |
| Audited | Shared audits table, callbacks | 5.8.0, November 2024 | Human-readable audit logs with comments |
| Logidze | JSONB column via PostgreSQL triggers | 1.4.1, June 2025 | Complete history, including SQL and bulk updates |
Category guidesAdmin panels
How ActiveAdmin, Avo, Administrate, RailsAdmin and Madmin approach back-office interfaces for Rails, their dependencies and licences, and when to build your own admin.
Category guidesAuthorization
How Pundit, CanCanCan and Action Policy model permissions in a Rails application, how each handles scopes and testing, and when a hand-written approach is enough.
GuidesEvaluating a Rails gem
A practical checklist for judging a Ruby gem: maintenance signals, compatibility with Rails 7 and 8, dependencies, licence, and security advisories with bundler-audit.