AuthTrail records every Devise login attempt as a queryable row
Track Devise login activity
At a glance
- What is it?
- AuthTrail is a Ruby gem for Rails applications using Devise, and it creates a LoginActivity record on every login attempt whether it succeeded or failed, with the identity, IP address, user agent and geolocation attached. It suits a team that wants an audit trail and a basis for detecting suspicious sign-ins, and it does nothing to block those sign-ins on its own.
- Who is it for?
- Adopt AuthTrail if you run a Rails application with Devise and need a durable record of who tried to sign in, from where and with what outcome, because the gem gives you that table and the hooks to shape it. Do not adopt it expecting it to stop the attacks it records, since the documentation recommends pairing it with Devise's Lockable module and Rack::Attack rather than treating it as a defence.
- Can I use it commercially?
- Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Activity is slowing. The repository last received commits 6 months ago.
- What is it written in?
- Mainly Ruby, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.
Editorial analysis
One row per attempt, successful or not
AuthTrail is described in one line as a tool to track Devise login activity, and the mechanism is a record rather than a service. A LoginActivity record is created every time a user tries to log in, and the point of the library is that this includes the failures. The data captured for each event is listed explicitly, and the list is what makes the table useful for detection. The scope is the Devise scope and the strategy is the Devise strategy. The identity is the email address. The success flag records whether the login worked, and the failure_reason records why it did not. The user is attached if the login succeeded. The context is the controller and action. The IP address comes from the request, and the user agent and referrer come from the browser. The city, region, country, latitude and longitude come from the IP, and created_at records the time of the event. The library is described as battle-tested at Instacart, and it is MIT licensed, with the last push to the repository on 2026-04-04 and no GitHub releases, so the changelog is the version record.
Installing the gem and choosing an encryption strategy
The gem goes into your Gemfile like any other dependency.
gem "authtrail"The install generator then asks a question that matters more than it first appears, which is how email and IP addresses are encrypted. With Lockbox, you install Lockbox and Blind Index and run the generator with that option.
rails generate authtrail:install --encryption=lockbox
rails db:migrateWith Active Record encryption, the option is activerecord.
rails generate authtrail:install --encryption=activerecord
rails db:migrateAnd if you prefer not to encrypt data, the option is none.
rails generate authtrail:install --encryption=none
rails db:migrateThe three choices are worth weighing before you pick one. A login activity table is a table of email addresses and IP addresses, which is exactly the shape of data that causes trouble under a privacy request, so the choice of no encryption is one you should make deliberately. The migrate step creates the login_activities table, and the generated initializer at config/initializers/authtrail.rb is where the rest of the configuration lives.
The four configuration hooks, and what each one changes
The library exposes four lambdas, and together they are the whole extension surface. The first is the exclude method, which decides which attempts are not tracked at all.
AuthTrail.exclude_method = lambda do |data|
data[:identity] == "[email protected]"
endThe documentation gives the reason, which is that this is useful if you run acceptance tests, since without it every Capybara session would fill the table with noise. The second is the transform method, which lets you add or modify data, and also add new fields to the login_activities table if needed.
AuthTrail.transform_method = lambda do |data, request|
data[:request_id] = request.request_id
endThe third is the track method, which writes the data somewhere other than the login_activities table, so if you want the events in a different store you can send them there instead. The fourth is the identity method, which lets you use a custom identity rather than assuming the email address.
AuthTrail.identity_method = lambda do |request, opts, user|
if user
user.email
else
request.params.dig(opts[:scope], :email)
end
endThat last one matters for a specific case. On a failed attempt there is no user object, so the default identity source has to come from the request parameters, and if your sign-in form does not use an email field at all, the library needs to be told where to look.
Keeping the user on failed attempts, and the polymorphic association
One feature deserves emphasis because it changes what the table can answer. By default a failed attempt has no user attached, since the identity was not resolved to an account. The transform method can change that.
AuthTrail.transform_method = lambda do |data, request|
data[:user] ||= User.find_by(email: data[:identity])
endWith that in place, a failed attempt against a known email address is linked to that user, which is what turns the table from a list of strangers into a list of failed attempts against your real accounts. The association itself is a plain has_many on your user model.
class User < ApplicationRecord
has_many :login_activities, as: :user # use :user no matter what your model name
endThe comment is the important part, because the polymorphic association uses :user as the association name regardless of what your model is actually called, so a rename of the class does not break the relation. The LoginActivity model uses a polymorphic association so it can be associated with different user models, which is what lets the same table serve an application with a User model and an Admin model, or a multi-tenant setup with one model per tenant.
Geocoding without shipping IP addresses to a third party
Geocoding is optional and off by default, which is the right default for a table full of IP addresses. To turn it on you add the geocoder gem.
gem "geocoder"And set the flag in the initializer.
AuthTrail.geocode = trueGeocoding is performed in a background job so it does not slow down web requests, and the queue it runs on is configurable.
AuthTrail.job_queue = :low_priorityThe library uses Geocoder for the work, and the documentation recommends configuring local geocoding or load balancer geocoding so IP addresses are not sent to a third party service, with a GDPR note that if you do use a third party service you should add it to your subprocessor list. Local city-level geocoding uses the MaxMind GeoLite2 City database through the maxminddb gem, configured with the path to the .mmdb file, and country-level geocoding uses the geoip-database package, which the README notes is preinstalled on Heroku, with the apt command given for Ubuntu. The load balancer route is the one that scales best: several load balancers can add geocoding information to request headers, and the transform method then copies those headers into the data.
Load balancer headers, retention, and cleaning up old rows
The load balancer approach turns the geocoding step into a header copy, which is both faster and private, and it requires geocoding to be turned off so the library does not also geocode.
AuthTrail.geocode = false
AuthTrail.transform_method = lambda do |data, request|
data[:country] = request.headers["<country-header>"]
data[:region] = request.headers["<region-header>"]
data[:city] = request.headers["<city-header>"]
endThe placeholders in that example are meant to be replaced with the header names your own load balancer sets, and nginx, Google Cloud and Cloudflare are all listed as capable of adding them. Data retention is handled with ordinary Active Record queries, and the two examples cover the two cases you have. To delete older data, you delete in batches by created_at.
LoginActivity.where("created_at < ?", 2.years.ago).in_batches.delete_allAnd to delete data for a specific user you match both the id and the type, because of the polymorphic association.
LoginActivity.where(user_id: 1, user_type: "User").in_batches.delete_allThe in_batches call is what keeps a large table from locking. The library itself is not a retention policy, so a table that records every failed login attempt against a public endpoint will grow quickly, and something has to prune it.
What AuthTrail does not do
The limits are explicit in the documentation and they are the difference between an audit trail and a security control. The recommendation is to use this in addition to Devise's Lockable module and Rack::Attack, with links to hardening guides for Devise and for Rails generally. That is the library saying plainly that it records and does not respond. A table of failed logins tells you afterwards that an account was probed; it does not slow the probe, does not lock the account, and does not challenge the source. If your requirement is to block an attacker, Rack::Attack and Lockable are the tools for that, and AuthTrail is what you layer underneath to understand what happened after the fact. The second limit is scope. It tracks Devise logins, so a password reset flow, an OAuth callback or a magic link is outside what it records unless you add a transform to capture it, and the last push to the repository was on 2026-04-04, so an application on a very old version of Devise may need checking against the changelog before you rely on the recorded fields.
Editorial conclusion
Adopt AuthTrail if you run a Rails application with Devise and need a durable record of who tried to sign in, from where and with what outcome, because the gem gives you that table and the hooks to shape it. Do not adopt it expecting it to stop the attacks it records, since the documentation recommends pairing it with Devise's Lockable module and Rack::Attack rather than treating it as a defence. Verify two things before you wire it in: that you have decided how email and IP addresses are encrypted, since the install generator takes an encryption option and the default choice of none leaves identity data in plain columns, and that your geolocation runs locally or through your own load balancer, because the default Geocoder setup would send IP addresses to a third party service. The practical first step is to add the gem, run the install generator with your chosen encryption, and confirm that a LoginActivity row appears for a failed sign-in as well as a successful one.
Frequently asked questions
What does AuthTrail do?
It creates a LoginActivity record every time a user tries to log in, whether the attempt succeeded or failed, and the data includes the Devise scope and strategy, the email identity, a success flag, the failure reason, the IP address, the user agent and referrer, the geolocation fields and the timestamp.
How do I install AuthTrail in a Rails application?
Add gem "authtrail" to your Gemfile, then run the install generator with the encryption option you want, either lockbox, activerecord or none, followed by rails db:migrate to create the login_activities table.
How do I stop AuthTrail recording my acceptance test logins?
Set AuthTrail.exclude_method to a lambda that returns true for attempts you want to skip. The documented example excludes the identity [email protected], which is useful if you run acceptance tests.
How does AuthTrail geocode IP addresses without a third party service?
The documentation recommends local geocoding with the MaxMind GeoLite2 City database or the geoip-database package, or load balancer geocoding where the transform method copies country, region and city from request headers your own load balancer adds. Geocoding itself runs in a background job so it does not slow down requests.
Can AuthTrail link a failed login to a known user?
Yes. A transform method that sets data[:user] ||= User.find_by(email: data[:identity]) attaches the user on failed attempts. The association is polymorphic, so it is added to your model as has_many :login_activities, as: :user, using :user whatever your model is named.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/ankane-authtrail)Was this useful?