Every new Rails app eventually hits the same fork: install Devise in two minutes, or spend a weekend reading the Rodauth docs and pretending I know what I’m doing. I’ve gone both ways on different projects this year, and the choice is less about “which is better” than about which set of trade-offs you’d rather inherit on day 400. This post is the rubric I wish I’d had the third time I made the wrong call. It’s biased toward the kind of Rails app a solo founder or small team actually ships — SaaS, B2B-ish, one or two auth flows, eventually a passkey or magic link bolted on. If you’re building a consumer social app with twelve OAuth providers, your math is different and I’ll flag where.

What we’re actually comparing

Devise is the default. It’s a Warden-based gem, mounted as a Rails engine, with controllers and views you inherit and override. You enable modules — database_authenticatable, recoverable, confirmable, lockable, omniauthable — and most things you’d need on a small SaaS are one or two lines in the user model.

Rodauth is the upstart. It’s a Roda-based authentication framework that you mount into Rails via the rodauth-rails gem. It assumes a separate accounts table (or migrates your users table into one), keeps password hashes in a different table than the account, supports SQL-level constraints for things Devise leaves to Ruby, and gives you a configuration DSL that reads like a strict, opinionated checklist of every auth decision you have to make.

The scope of this post is the green-field decision: a Rails 8 app, Postgres, Hotwire or React, and a founder who wants auth that won’t be a maintenance liability in two years.

The rubric I use

I stopped trying to pick “the best” auth library and started scoring on five axes. Each gets a 1–3, and the higher total wins for that project — not in general.

AxisWhat it measuresDevise tends to scoreRodauth tends to score
Time-to-first-loginHow fast a working signup + login + reset flow shipsHighMedium
Customizability ceilingHow far you can push it before you’re fighting the frameworkMediumHigh
Security defaultsWhat you get without reading the source — token rotation, timing-safe compares, password pepperMediumHigh
Operational footprintSchema sprawl, migrations, what ops has to think aboutLowMedium-high
Ecosystem & hireabilityWill the next dev know it? Are there StackOverflow answers from this decade?HighLow

A typical “ship in six weeks” SaaS MVP for a non-technical founder scores Devise higher on row 1, row 4, and row 5 — and those three matter more than row 2 and row 3 when the app might not exist in six months.

A serious B2B product with audit logging, multi-factor, SAML on the horizon, and a security review in year two scores Rodauth higher on rows 2 and 3, and those eventually dominate the maintenance budget.

The two axes where the table understates the gap are security defaults and customizability ceiling. Rodauth ships with HMAC-signed reset tokens, separate password hash storage, and SQL constraints out of the box. You can replicate most of that in Devise, but you’ll be writing it yourself, and “I wrote auth code myself” is a sentence I try to keep out of post-mortems.

Where each one cuts you

Devise’s failure mode is silent drift. The defaults were fine in 2014. Some still are; some quietly aren’t. Confirmation tokens are not HMAC’d by default — they’re stored as plaintext in the database. If your DB ever leaks, those tokens leak too. You can fix this, but most apps I look at haven’t, because nobody told them to.

The other Devise trap is override sprawl. The moment you need a custom signup flow with a step in between, you’re subclassing Devise::RegistrationsController, overriding create, calling super in a do…end block, and praying the next Devise major version doesn’t rename the callback. I’ve seen apps where the auth code is more custom-controller code than business logic.

Rodauth’s failure mode is upfront pain. The first time you set it up you will be reading three docs tabs and a 40-line config file and wondering if you’ve gained anything. The configuration is explicit — which is the point — but every option you skip is one you’ll regret. The accounts/passwords split is correct security-wise and surprising operationally.

Both gems have a third, shared pitfall: session strategy. Neither one will save you if you store a 30-day session in a cookie, never rotate on privilege change, and let any compromised endpoint elevate to admin. Auth library choice is a small fraction of session security. Pick the gem, then go read OWASP on session management. I keep a checklist for this when I drop AI features into an existing Rails app — same discipline applies here.

How this plays out on a real Rails app

Imagine the project is a B2B SaaS, four-to-eight-week MVP, founder is non-technical, two roles (admin and member), email/password plus Google OAuth, and a “we’ll add SSO later when we have enterprise customers” hand-wave.

With Devise you’d write rails g devise:install, then rails g devise User, enable database_authenticatable, recoverable, confirmable, and omniauthable, add a strong-parameters override for the signup form, mount omniauth-google-oauth2, write a small Users::OmniauthCallbacksController, and the auth chapter of the build is over by Wednesday of week one. You’ll spend the rest of the project on the actual product. When the SSO request lands in year two, you’ll bolt on omniauth-saml per-tenant and probably swear at the multi-tenancy implications.

With Rodauth you’d spend the first two days installing it correctly. You’d configure the accounts table, set up HMAC for tokens, decide whether to use the verify_account feature or assume good-faith signup, write a Roda app that mounts at /auth, and integrate rodauth-oauth for Google. By Friday of week one you’d have a working flow that’s measurably more secure than Devise out of the box. When SSO lands in year two, you’d add the existing OAuth/SAML pieces with the same DSL and a config diff, not a controller archaeology dig.

Which one is right for that project? In my experience, Devise. The MVP needs to ship; the founder needs to find out if anyone wants it; the security ceiling is set by the product not existing yet. I’d write a TODO comment to HMAC the confirmation tokens and move on. If the product survives to year two, the auth migration to Rodauth is a one-week project, not a rewrite. (For more on this trade-off, see Rails SaaS MVP scope.)

The cost of switching auth gems later is real but bounded. The cost of shipping six weeks late because you over-invested in auth on day one is everything.

— Self note

What “done” looks like on either side

Done with Devise means: HMAC’d reset and confirmation tokens, session rotation on password change, paranoid mode enabled, Devise.email_regexp tightened, lockable configured with a sane unlock strategy, OmniAuth callbacks audited for the provider-and-uid lookup race condition, and an integration test that proves a logged-out user can’t reach an authenticated route. If you can’t say yes to all of those, you don’t have working Devise, you have a default Devise.

Done with Rodauth means: every feature you’ve enabled has a corresponding spec, the accounts table schema is reviewed against the gem’s recommended constraints, token HMAC secret is in your secret manager not your credentials.yml, the email templates are yours not the defaults, and the same authenticated-route integration test exists. The Rodauth config file should be readable as a security spec — if a reviewer can’t tell what your auth policy is from the config, you have rope, not a configuration.

Both finish lines share two things: a written threat model, however short, and a test suite that demonstrates the negative cases. Auth without negative tests is theater.

When the rubric breaks

If you’re shipping a consumer app where 80% of signups are OAuth and password auth is a fallback for the edge case, this entire comparison softens. The OAuth layer dominates and Devise + omniauth-* is fine.

If you’re regulated — HIPAA, banking, anything with a real auditor — you may not get to pick. Pick whichever your security team has reviewed before, because the review cost dwarfs the implementation cost.

And if your Rails app is actually a thin frontend to a separate auth service — Auth0, WorkOS, Clerk — you don’t need either gem. You need a session wrapper and the discipline to keep the auth service the source of truth.

A claim I’d defend

Here’s the one I’d bet on: of the Rails apps that pick Devise today and live for three years, fewer than half will have HMAC’d their confirmation tokens by year three. The default is the trap. Whichever library you pick, write down the three security defaults you’re going to verify in week one and post them above your desk. The gem won’t save you. The checklist might.