Back to Blog
Available in:
May 20, 20263 min read

Choosing Rails for a Compliance Product

One developer. Eighteen weeks. A B2B compliance product. The wrong stack decision can double the effort without you noticing until week twelve.

Rails won — not because it is "better," but because the context cost for a single developer is lower than any alternative I evaluated. The decision matrix showed that before I wrote a line.

The real constraints

This is the part most stack debates skip. The decision only makes sense against the actual situation:

  • One developer, roughly 290 estimated hours for the first milestone.
  • PostgreSQL with Row-Level Security already decided, and irreversible.
  • A CRUD-heavy product: compliance dashboard, supplier uploads, alerting.
  • The bottleneck is I/O, not CPU — the server calls external search and LLM APIs and waits.

That last point matters more than it looks, and I'll come back to it.

The alternatives, and why they lost

FastAPI scored 3.10. Python's real advantage appears when there's an ML workload running on your server. Here the server calls external APIs — so that advantage is exactly zero. Scaffolding is slower, SQLAlchemy integrates less cleanly with PostgreSQL RLS, and there's no Hotwire equivalent for building a dashboard without adopting React.

NestJS scored 2.45. It demands four distinct contexts for one developer: NestJS, BullMQ, Prisma, React. Worse, Prisma has limited support for PostgreSQL's set_config, which means raw queries for RLS-based multi-tenancy — the core of the product. That cognitive overhead was incompatible with the timeline.

Rails 8 scored 4.95, with ActiveRecord, Hotwire and Sidekiq.

The anchoring point

The decision really hinged on one integration: multi-tenancy. Rails has a native answer.

class Current < ActiveSupport::CurrentAttributes
  attribute :tenant
  attribute :user
  attribute :role
end

CurrentAttributes is thread-local by design and reset at the end of each request or job. There's no risk of tenant context leaking between requests — which, in a compliance product, is the failure mode that ends the company.

Current.tenant gets set in the base controller after authentication and propagates to background jobs through Sidekiq middleware. Without that propagation an async job could run with no tenant context, silently defeating RLS. Silently is the dangerous word: the query still succeeds, it just returns the wrong tenant's rows.

What I gave up

Ruby has a smaller ecosystem for AI/ML. Irrelevant for this product, since the intelligence lives behind an API, but it does narrow future hiring.

Hotwire is the wrong call if the product ever needs to be mobile-first or offline-first. It isn't, today.

And Rails is convention-heavy, which becomes restrictive for complex non-CRUD domain logic. This product is CRUD on every side, so the conventions are pure leverage — but that's a property of this product, not a general truth.

The lesson

The stack question is almost never "which is better." It's "which has the lowest total context cost for the person who has to build and operate this, under these constraints."

Change any one of those constraints — three developers instead of one, a real ML workload on the server, a mobile-first product — and the matrix produces a different winner. That's the point of writing the matrix down rather than arguing from preference.

The score is not the output. The explicit weighting is.

ArchitectureRubyRailsDecisions