A shared Rails template gave three internal tools a common foundation - authentication, tenancy and permissions solved once, argued about once, then simply inherited.
When I wrote about Elysium building our own HR system rather than buying one, I skipped over some detail that deserves its own article: that HR build didn’t start from a blank Rails app. It started from a template - one we hadn’t originally set out to build at all.
The origin is less deliberate than it sounds in hindsight. We had three teams working independently on three new internal tools at roughly the same time. Within the first couple of days, it was obvious what would happen if we let all three carry on separately: three different baselines, three sets of decisions about authentication and permissions and tenancy, three lots of the same boring problems solved three slightly different ways. So we stopped, pulled everyone into the same conversation, and agreed to extract a shared core first - then build each tool on top of it, so that improvements to the foundation could benefit all three at once instead of being duplicated three times over.
That decision is the subject of this article. Not the tools built on top of it - the core application that sits underneath them.
What’s actually in it
The template is a Ruby on Rails SaaS boilerplate, running on PostgreSQL. The philosophy behind it is simple: everything that every internal tool needs, and that has no business being reinvented per project, gets solved once, here, and inherited everywhere else.
Authentication is built on authentication-zero rather than Devise - signed-cookie sessions, email verification, password reset, sudo mode for sensitive actions, and full 2FA support including TOTP, WebAuthn, and recovery codes. Multi-tenancy runs through acts_as_tenant, with an Organization model that users can belong to via a join model, and a Current.organization scope set per request so any model that needs tenant isolation just inherits from OrganizationRecord and gets it for free. Roles and permissions are a custom implementation rather than a gem like rolify - permissions are named, scopable capabilities; roles are containers that group them, scoped to a specific resource instance; and authorization is enforced through ActionPolicy, with policies living in one predictable place.
Underneath all of that, we deliberately avoided adding Redis to the stack: background jobs, caching and WebSockets all run through Solid Queue, Solid Cache and Solid Cable, all PostgreSQL-backed. One less moving part to provision, monitor and pay for on every single project.
None of this is exotic, and frankly, that’s the point. It’s the set of decisions every Rails SaaS app ends up making anyway, made once, argued about once, and then simply inherited.
Where AI fits into this
It would be easy to assume the template is the “AI story” here - that we prompted our way to a boilerplate - but this template is pure, conventional engineering: decisions made deliberately, by people, about what good defaults look like. But there is a place for AI - in the development that happens after the fork.
Every new tool starts from the template, which runs in a devcontainer with Ruby, Node, PostgreSQL and AI harnesses such as Claude Code pre-configured, git identity and SSH forwarding already wired up, commit signing already working. A developer opens the project, builds the container, and is running a complete baseline application with working login, SaaS framework, authorisation system, and AI harnesses in just a few minutes. From there, the pattern described in the HR piece takes over: what’s left for the developer to build is the interesting 30% - the workflow logic specific to each tool. For us this has been a far better development pattern than asking AI to regenerate the same boring 70% from scratch on each project. Do it once, do it well.
Keeping it honest
A shared core only stays useful if it stays shared, which is the part that’s easy to get wrong. Ours lives in a single Gitea repository, and each team that builds on it is also responsible for maintaining it - when one team adds or refines something in the core, they communicate that back to the others so the shared understanding doesn’t quietly drift out of sync. In a small team like ours, this is best done over our internal comms; a quick @mention to the template-author role when there’s something worth discussing or syncing to the child projects, a written change log for detail, and all knowledge documented into the codebase. It’s not automatic, which means the people behind the project talk whenever there’s something to talk about; ideas get shared, refinements get discussed, problems get solved together.
The payoff
We now have three internal tools running on this core, the HR system among them, and I’m excited to write about the others soon. I don’t have a precise number on the time it’s saved, but my experience tells me that it’s weeks of effort avoided so far, if not months, and that number only grows the more we use it. No duplicated code, no reinventing the wheel, just a solid foundation that stays current by design.
How much of what your team rebuilds from scratch each time is actually a foundation in disguise?
See the original post on Matt's website: tiltedsky.net(opens in new tab)