Reading Time: 14 minutes | Series: Rails Renaissance #2
When we built Heis Soma in 2021, we made a decision that seemed crazy: build our own OAuth2 Single Sign-On system instead of using Devise. Three years and three Rails versions later, that “crazy” decision looks like prophecy.
“Just use Devise” is the standard Rails authentication advice. It’s battle-tested, well-documented, and used by thousands of applications. For 95% of Rails apps, it’s the right choice. But sometimes—just sometimes—the 5% edge case is exactly where you are.
This is the story of why we built Heis Soma (Greek: Εἷς Σῶμα, “One Body”), our custom OAuth2 authentication service that now powers Prayer Nook and positions us to serve the broader Christian ministry ecosystem. It’s a story about architectural decisions, technical tradeoffs, and the surprising ways that following standards can set you free.
Whether you’re considering custom authentication, evaluating Devise alternatives, or just curious about what it takes to build production-grade OAuth2, grab your favorite beverage and let’s dive deep.
Ember’s Opening: “Sometimes the path less traveled leads to undiscovered territory—and sometimes it just leads to more work. Here’s how we made sure it was the former.” 🐧🔥
The Context: Why Authentication Mattered So Much
Before we get into the technical details, you need to understand what we were building and why authentication was so critical.
The Christian Chain Ecosystem
The Christian Chain isn’t just one application—it’s a growing ecosystem of ministry tools:
Prayer Nook (launched 2022) – A prayer request platform where community members share needs and pray for each other. Think social network meets spiritual support group.
Unity Accord (in development) – A platform for churches to affirm core Christian beliefs while respecting denominational differences. Digital signatures, theological discussion, community building.
Future Applications – We had plans for:
- Small group management tools
- Bible study platforms
- Event coordination systems
- Giving management
- Volunteer scheduling
Each application needed:
- User authentication – Who are you?
- Permission management – What can you do?
- Profile consistency – One identity across all tools
- Privacy controls – Ministry-specific visibility settings
- Shared sessions – Login once, access everything
The Theological Dimension
The name “Heis Soma” comes from 1 Corinthians 12—Paul’s teaching about the church being “one body” despite many different members with different gifts. This wasn’t just a cute theological reference; it shaped our entire architecture:
One Body, Many Members:
- Centralized identity (one body)
- Distributed applications (many members)
- Unified purpose (ministry)
- Respect for differences (different gifts)
This theological framework led us toward SSO as a technical solution. Users shouldn’t need separate accounts for prayer requests, church events, and small groups any more than your hand needs a separate brain from your foot.
The Devise Decision: Why Not?
Let’s be clear: Devise is excellent software. It’s mature, well-maintained, secure, and solves authentication for thousands of Rails apps. So why didn’t we use it?
Limitation #1: Single Application Focus
Devise is designed for authentication within an application, not across applications. While you can share a Devise setup across apps with some gymnastics, it’s not the tool’s primary use case.
# What Devise does great:
class User < ApplicationRecord
devise :database_authenticatable, :registerable,
:recoverable, :rememberable, :validatable
end
# What we needed:
# Users authenticate ONCE in Heis Soma
# Then access Prayer Nook, Unity Accord, Events, etc.
# Each app trusts Heis Soma for authentication
We could have run Devise in each app with a shared database, but:
- Session management gets complex
- Cookie domain issues
- No standard protocol for trust
- Harder to add third-party apps later
Limitation #2: OAuth2 Provider Not Built-In
Devise handles OAuth2 clients well (login with Google, Facebook, etc.) through OmniAuth, but it’s not designed to be an OAuth2 provider. You can add this with the Doorkeeper gem, but then you’re combining two complex authentication systems.
We evaluated the Devise + Doorkeeper combination:
Pros:
- Both are well-maintained
- Good documentation
- Community support
Cons:
- Two separate authentication layers to maintain
- Conceptual overlap and potential conflicts
- Heavier dependency footprint
- More “magic” to understand and debug
Limitation #3: Lock-in to Rails Ecosystem
This was the subtle one. Devise is deeply Rails-integrated (which makes it powerful within Rails), but what if we wanted to:
- Build mobile apps with native authentication?
- Allow partner ministries to integrate?
- Add authentication to non-Rails services?
- Provide SSO to church’s existing tools?
OAuth2 is a standard protocol that works across languages, frameworks, and platforms. A properly implemented OAuth2 provider can serve:
- Rails apps (with gems)
- React Native apps (with libraries)
- iOS/Android native apps
- Third-party integrations
- Even non-web services
Limitation #4: Future-Proofing Concerns
In 2021, we were starting a nonprofit ministry with limited resources. We couldn’t afford to paint ourselves into a corner architecturally. Questions haunted me:
- What if Rails authentication patterns shift radically?
- What if we need to migrate away from Rails someday?
- What if partner churches want to integrate their tools?
- What if we need enterprise-grade features later?
OAuth2 is an IETF standard (RFC 6749). It’s not going anywhere. Building on a standard meant our authentication would outlive any particular framework’s trends.
The Decision: Custom OAuth2 with Standards Compliance
So we decided to build our own. Not because we thought we were smarter than the Devise team (we’re definitely not), but because our needs aligned better with OAuth2 principles than monolithic authentication.
Core Principles
1. Standards Over Cleverness
We would implement OAuth2 exactly by the spec, not invent our own variant. This meant:
- Following RFC 6749 to the letter
- Implementing standard grant flows
- Using JWT for tokens (RFC 7519)
- Supporting PKCE for mobile (RFC 7636)
- Following OAuth2 security best practices
2. Rails-Native Implementation
While OAuth2 is framework-agnostic, our implementation would be deeply Rails-integrated:
- ActiveRecord models
- Rails routing conventions
- ActionController inheritance
- Rails caching
- ActiveJob for async tasks
Best of both worlds: standard protocol, idiomatic Rails code.
3. API-First Design
Everything would be API-accessible from day one:
- RESTful endpoints
- JSON responses
- Versioned API (v1, v2, etc.)
- Comprehensive documentation
- Client libraries for common cases
This would make integrations easier and force us to think about the contract between Heis Soma and client applications.
4. Security-First Mentality
Authentication is security-critical. We would:
- Hash all tokens before storage
- Implement rate limiting
- Audit log everything
- Regular security reviews
- Stay current on CVEs
- Encrypt sensitive data at rest
The Architecture: How Heis Soma Works
Let me walk you through the actual implementation. This isn’t theoretical—this is production code serving 1,000+ users across multiple applications.
The OAuth2 Flow (Simplified)
Here’s what happens when you click “Login” in Prayer Nook:
1. User clicks "Login with Christian Chain"
Prayer Nook → https://heissoma.org/oauth/authorize?
client_id=prayer_nook&
redirect_uri=https://prayernook.org/callback&
response_type=code&
scope=basic+email+prayer_wall
2. Heis Soma shows login screen
User enters email/password
3. Heis Soma validates credentials
User sees: "Prayer Nook wants to access..."
User clicks "Authorize"
4. Heis Soma redirects back with authorization code
→ https://prayernook.org/callback?code=ABC123XYZ
5. Prayer Nook exchanges code for access token
POST to https://heissoma.org/oauth/token
{
"grant_type": "authorization_code",
"code": "ABC123XYZ",
"client_id": "prayer_nook",
"client_secret": "[secret]",
"redirect_uri": "https://prayernook.org/callback"
}
6. Heis Soma returns tokens
{
"access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...",
"refresh_token": "dGhpcyBpcyBhIHJlZnJlc2ggdG9rZW4...",
"token_type": "Bearer",
"expires_in": 3600,
"scope": "basic email prayer_wall"
}
7. Prayer Nook uses access token for API requests
GET https://heissoma.org/api/v1/user
Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...
8. Heis Soma validates token, returns user info
{
"id": 123,
"email": "user@example.com",
"name": "John Doe",
"username": "jdoe",
"permissions": ["pray", "post_requests", "comment"]
}
This is the Authorization Code Grant flow—the most secure OAuth2 flow for web applications.
The Database Schema
Clean, focused tables that do one thing well:
# app/models/user.rb
class User < ApplicationRecord
has_many :oauth_access_tokens, dependent: :destroy
has_many :oauth_access_grants, dependent: :destroy
has_many :authorizations, dependent: :destroy
has_many :permissions, dependent: :destroy
# BCrypt password hashing
has_secure_password
validates :email, presence: true, uniqueness: true,
format: { with: URI::MailTo::EMAIL_REGEXP }
validates :username, presence: true, uniqueness: true,
format: { with: /\A[a-zA-Z0-9_]+\z/ }
# Soft delete for GDPR compliance
acts_as_paranoid
end
# app/models/oauth_application.rb
class OauthApplication < ApplicationRecord
has_many :oauth_access_tokens, dependent: :destroy
has_many :oauth_access_grants, dependent: :destroy
validates :name, presence: true
validates :uid, presence: true, uniqueness: true
validates :secret, presence: true
validates :redirect_uri, presence: true
# Trust levels for different app types
enum trust_level: {
first_party: 0, # Our own apps
partner: 1, # Verified ministries
third_party: 2 # Public applications
}
# Generate secure UID and secret on creation
before_validation :generate_credentials, on: :create
private
def generate_credentials
self.uid ||= SecureRandom.hex(16)
self.secret ||= SecureRandom.hex(32)
end
end
# app/models/oauth_access_token.rb
class OauthAccessToken < ApplicationRecord
belongs_to :user
belongs_to :oauth_application
# Store hashed token, not plaintext
before_create :generate_token
before_create :set_expiration
# Scopes as array
serialize :scopes, Array
scope :active, -> { where('expires_at > ?', Time.current).where(revoked_at: nil) }
def expired?
expires_at < Time.current
end
def revoked?
revoked_at.present?
end
def valid_token?
!expired? && !revoked?
end
private
def generate_token
# Generate JWT
payload = {
user_id: user.id,
app_id: oauth_application.id,
scopes: scopes,
exp: 1.hour.from_now.to_i,
iat: Time.current.to_i
}
self.token = JWT.encode(payload, Rails.application.credentials.jwt_secret, 'RS256')
self.token_digest = Digest::SHA256.hexdigest(token)
end
def set_expiration
self.expires_at = 1.hour.from_now
end
end
The Controllers (Where OAuth2 Happens)
Authorization Endpoint:
# app/controllers/oauth/authorizations_controller.rb
module Oauth
class AuthorizationsController < ApplicationController
before_action :authenticate_user!
before_action :validate_client_application
before_action :validate_redirect_uri
# GET /oauth/authorize
def new
# Check if user has already authorized this app
existing_grant = current_user.oauth_access_grants
.find_by(oauth_application: @application)
if existing_grant && @application.trusted?
# Skip authorization screen for trusted apps
create_and_redirect
else
# Show authorization screen
@scopes = parse_requested_scopes
render :new
end
end
# POST /oauth/authorize
def create
if params[:authorize] == 'true'
create_and_redirect
else
redirect_to oauth_error_url('access_denied',
'User denied authorization')
end
end
private
def create_and_redirect
# Create authorization code
auth_code = create_authorization_code(
user: current_user,
application: @application,
redirect_uri: params[:redirect_uri],
scopes: parse_requested_scopes,
code_challenge: params[:code_challenge] # PKCE support
)
redirect_to build_callback_url(auth_code.code)
end
def validate_client_application
client_id = params[:client_id]
@application = OauthApplication.find_by(uid: client_id)
return if @application
redirect_to oauth_error_url('invalid_client',
'Unknown client application')
end
def validate_redirect_uri
requested_uri = params[:redirect_uri]
allowed_uris = @application.redirect_uri.split("\n")
return if allowed_uris.include?(requested_uri)
redirect_to oauth_error_url('invalid_request',
'Invalid redirect URI')
end
def build_callback_url(code)
uri = URI.parse(params[:redirect_uri])
uri.query = URI.encode_www_form(
code: code,
state: params[:state] # Anti-CSRF
)
uri.to_s
end
end
end
Token Endpoint:
# app/controllers/oauth/tokens_controller.rb
module Oauth
class TokensController < ApplicationController
skip_before_action :verify_authenticity_token # API endpoint
before_action :validate_client_credentials
# POST /oauth/token
def create
case params[:grant_type]
when 'authorization_code'
handle_authorization_code_grant
when 'refresh_token'
handle_refresh_token_grant
else
render json: { error: 'unsupported_grant_type' }, status: :bad_request
end
end
# POST /oauth/revoke
def revoke
token = OauthAccessToken.find_by(token_digest: token_digest(params[:token]))
if token
token.update(revoked_at: Time.current)
head :ok
else
head :not_found
end
end
private
def handle_authorization_code_grant
code = OauthAuthorizationCode.find_by(code: params[:code])
unless code&.valid_code?
return render json: { error: 'invalid_grant' }, status: :bad_request
end
# Validate PKCE if present
if code.code_challenge.present?
unless validate_pkce(code)
return render json: { error: 'invalid_grant' }, status: :bad_request
end
end
# Exchange code for tokens
access_token = create_access_token(
user: code.user,
application: code.oauth_application,
scopes: code.scopes
)
refresh_token = create_refresh_token(
user: code.user,
application: code.oauth_application
)
# Mark code as used (one-time use only)
code.destroy
render json: {
access_token: access_token.token,
refresh_token: refresh_token.token,
token_type: 'Bearer',
expires_in: access_token.expires_in,
scope: access_token.scopes.join(' ')
}
end
def handle_refresh_token_grant
refresh_token = OauthRefreshToken.find_by(
token_digest: token_digest(params[:refresh_token])
)
unless refresh_token&.valid_token?
return render json: { error: 'invalid_grant' }, status: :bad_request
end
# Create new access token
access_token = create_access_token(
user: refresh_token.user,
application: refresh_token.oauth_application,
scopes: refresh_token.scopes
)
# Rotate refresh token (security best practice)
new_refresh_token = create_refresh_token(
user: refresh_token.user,
application: refresh_token.oauth_application
)
refresh_token.destroy # Invalidate old one
render json: {
access_token: access_token.token,
refresh_token: new_refresh_token.token,
token_type: 'Bearer',
expires_in: access_token.expires_in,
scope: access_token.scopes.join(' ')
}
end
def validate_pkce(code)
verifier = params[:code_verifier]
challenge = code.code_challenge
method = code.code_challenge_method || 'plain'
case method
when 'plain'
verifier == challenge
when 'S256'
Base64.urlsafe_encode64(
Digest::SHA256.digest(verifier),
padding: false
) == challenge
else
false
end
end
def validate_client_credentials
client_id = params[:client_id]
client_secret = params[:client_secret]
@application = OauthApplication.find_by(uid: client_id)
unless @application&.secret == client_secret
render json: { error: 'invalid_client' }, status: :unauthorized
end
end
def token_digest(token)
Digest::SHA256.hexdigest(token)
end
end
end
Security Measures (The Non-Negotiables)
1. Token Hashing
We NEVER store plaintext tokens:
# When creating a token
token = SecureRandom.hex(32)
token_digest = Digest::SHA256.hexdigest(token)
# Store only the digest
OauthAccessToken.create(
token_digest: token_digest,
# other attributes...
)
# Return the plaintext token ONCE to the client
{ access_token: token }
# When validating
submitted_digest = Digest::SHA256.hexdigest(submitted_token)
OauthAccessToken.find_by(token_digest: submitted_digest)
2. Rate Limiting
Protect against brute force and DoS:
# config/initializers/rack_attack.rb
Rack::Attack.throttle('login attempts', limit: 5, period: 15.minutes) do |req|
if req.path == '/oauth/token' && req.post?
req.ip
end
end
Rack::Attack.throttle('token requests', limit: 10, period: 1.minute) do |req|
if req.path == '/oauth/token'
req.params['client_id']
end
end
3. Audit Logging
Track everything for security and compliance:
# app/models/concerns/auditable.rb
module Auditable
extend ActiveSupport::Concern
included do
after_create :log_creation
after_update :log_update
after_destroy :log_destruction
end
private
def log_creation
AuditLog.create(
auditable: self,
action: 'create',
changes: attributes,
user_id: Current.user&.id,
ip_address: Current.ip_address
)
end
# Similar for update and destroy...
end
# Usage
class OauthAccessToken < ApplicationRecord
include Auditable
# Now all token operations are logged
end
4. HTTPS Enforcement
OAuth2 requires HTTPS in production:
# config/environments/production.rb
config.force_ssl = true
# And validate redirect URIs
def validate_redirect_uri
uri = URI.parse(params[:redirect_uri])
unless uri.scheme == 'https' || Rails.env.development?
redirect_to oauth_error_url('invalid_request',
'Redirect URI must use HTTPS')
end
end
How It Survived Three Rails Versions
Remember from Post #1 that we upgraded through Rails 6.1 → 7.0 → 7.1 → 8.0? Here’s the remarkable thing: Heis Soma’s OAuth2 implementation barely changed.
Rails 6.1 → 7.0
Changes required: Minimal
# Updated session handling slightly
# Old (Rails 6.1)
session[:oauth_state] = SecureRandom.hex(16)
# New (Rails 7.0) - same code, just works better
session[:oauth_state] = SecureRandom.hex(16)
# Rails 7's improved session handling made this more secure automatically
Why so smooth?
- We followed Rails conventions
- No Webpacker dependencies (API-only for OAuth endpoints)
- Token generation was pure Ruby, not framework-specific
- Database schema was simple and standard
Rails 7.0 → 7.1
Changes required: None for core OAuth flow
Improvements we adopted:
# Async queries for dashboard
def index
@applications = current_user.oauth_applications.load_async
@active_tokens = current_user.oauth_access_tokens.active.load_async
# Both queries run in parallel now
end
Rails 7.1 → 8.0
Changes required: Still minimal
New features we’re evaluating:
- Solid Queue for token cleanup jobs
- Better connection pooling for high-traffic periods
The lesson? Standards-compliant code ages gracefully. Because we implemented OAuth2 by the spec rather than inventing our own approach, Rails upgrades didn’t break our authentication.
The Multiple Database Connection Win
This deserves special attention because it’s where our architecture really paid off.
The Problem (Rails 6.1)
When Prayer Nook needed user information:
# SLOW: HTTP request on every page load
def current_user_info
response = HTTP.get(
"https://heissoma.org/api/v1/user",
headers: { "Authorization" => "Bearer #{session[:token]}" }
)
JSON.parse(response.body)
rescue
nil
end
Performance cost:
- 50-100ms for HTTP request
- Additional DNS lookup
- SSL handshake
- Network latency
- JSON parsing
- Potential timeout/retry logic
Reliability issues:
- Network failures
- Heis Soma downtime affects Prayer Nook
- Extra point of failure
The Solution (Rails 7+)
Direct database read access:
# config/database.yml (Prayer Nook)
production:
primary:
database: prayer_nook_production
host: <%= ENV['PRIMARY_DB_HOST'] %>
heis_soma:
database: heis_soma_production
host: <%= ENV['HEIS_SOMA_DB_HOST'] %>
replica: true # Read-only
# app/models/heis_soma/user.rb
module HeisSoma
class User < ApplicationRecord
self.abstract_class = true
connects_to database: { writing: :heis_soma, reading: :heis_soma }
# Read-only enforcement
def readonly?
true
end
end
end
# app/models/heis_soma/user_profile.rb
module HeisSoma
class UserProfile < User
self.table_name = "users"
has_many :permissions
has_many :prayer_groups
end
end
# Now in controllers
def current_user_full_info
@current_user ||= HeisSoma::UserProfile
.includes(:permissions, :prayer_groups)
.find_by(id: session[:user_id])
end
Performance improvement:
- 50-100ms → 5-10ms (10x faster!)
- No network calls
- No HTTP overhead
- Direct PostgreSQL query
- Eager loading with
includes
Results:
- API calls to Heis Soma: -70%
- Average page load: 245ms → 180ms
- Fewer points of failure
- Better caching possible
- Simpler error handling
The Security Model
“Wait,” you might be thinking, “isn’t giving Prayer Nook direct database access to user data a security risk?”
Great question! Here’s how we handle it:
1. Read-Only Access
# Database user permissions
GRANT SELECT ON heis_soma.users TO prayer_nook_user;
GRANT SELECT ON heis_soma.permissions TO prayer_nook_user;
-- No INSERT, UPDATE, DELETE
# Enforced in Rails too
def readonly?
true
end
# Any attempt to modify raises an error
user = HeisSoma::UserProfile.find(123)
user.email = "hacked@example.com"
user.save # ActiveRecord::ReadOnlyRecord exception!
2. Network-Level Security
# Database firewall rules
# Only Prayer Nook servers can access Heis Soma DB
iptables -A INPUT -p tcp --dport 5432 -s <prayer-nook-ip> -j ACCEPT
iptables -A INPUT -p tcp --dport 5432 -j DROP
3. Separate Credentials
# Different database users for different apps
prayer_nook: read-only user with limited table access
unity_accord: read-only user with limited table access
heis_soma: full access (only Heis Soma itself)
4. Sensitive Data Still via API
Some operations still go through the OAuth2 API:
- Password changes
- Email updates
- Permission grants/revokes
- Account deletion
Direct DB access is for reading user profiles and permissions, not modifying them.
When Should YOU Build Custom Auth?
After three years, here’s my honest assessment of when custom OAuth2 makes sense:
✅ Build Custom Auth When:
1. You Have Multiple Related Applications
- SSO across your own services
- Unified user experience
- Shared session management
2. You Need OAuth2 Provider Capabilities
- Third-party integrations planned
- Partner API access
- Mobile app support
- Cross-platform requirements
3. You Have Specific Requirements
- Unusual permission models
- Custom user attributes
- Industry-specific compliance
- Theological/ethical constraints (like us!)
4. You Have Technical Capacity
- Team understands security
- Resources for maintenance
- Can handle support burden
- Willing to stay current on vulnerabilities
5. Long-Term Strategic Value
- Authentication is core to your business
- Competitive advantage in your approach
- Plans for significant scale
- Investment pays off over time
❌ Use Devise (or Alternatives) When:
1. Single Application
- Only one Rails app
- No SSO requirements
- Simpler authentication needs
2. Standard Requirements
- Email/password login
- Password reset
- Remember me
- Session management
- Standard stuff!
3. Limited Resources
- Small team
- Tight timeline
- Other priorities more important
- Can’t justify maintenance burden
4. Want Battle-Tested
- Thousands of apps using it
- Security issues found and fixed
- Comprehensive documentation
- Large community support
🤔 Consider Doorkeeper When:
Middle ground option:
- Need OAuth2 provider
- Want to use with Devise
- Don’t want to build from scratch
- Can handle some complexity
We actually tried Doorkeeper before going custom. It’s excellent software, but we found:
- Still needed customization for our needs
- Conceptual overlap with our desired architecture
- Wanted deeper understanding of OAuth2 flows
- Learning investment was same either way
Lessons Learned (The Real Talk)
What Worked Brilliantly
1. Standards Compliance Pays Off
Following RFC 6749 exactly meant:
- Works with standard OAuth2 libraries
- Easier to debug (compare to spec)
- Future-proof against framework changes
- Transferable knowledge for career
2. The Learning Investment Was Worth It
Building this taught me:
- Deep understanding of authentication flows
- Security thinking and threat modeling
- API design principles
- How to explain OAuth2 to stakeholders
This knowledge directly helped me in job interviews for Solutions Engineer roles. Being able to explain OAuth2 confidently, discuss security tradeoffs, and describe real production architecture is valuable.
3. Flexibility for Ministry Context
We could build features specific to our needs:
- Prayer privacy levels
- Ministry-specific permissions
- Theological alignment verification
- Church partnership models
These would have been hacks in Devise but are first-class in our system.
What Was Harder Than Expected
1. The Security Burden
You are responsible for protecting user credentials. This means:
- Constant vigilance for vulnerabilities
- Staying current on security best practices
- Regular security audits
- Stress about potential breaches
It’s serious responsibility. We handle it by:
- Following OWASP guidelines
- Regular dependency updates
- Penetration testing (annual)
- Insurance for security incidents
2. Maintenance Overhead
More code to maintain than Devise:
- ~2,000 lines for OAuth2 implementation
- Custom controllers and models
- Integration tests for all flows
- Documentation for team and partners
We budget ~5 hours/month for Heis Soma maintenance.
3. The “Am I Missing Something?” Anxiety
With Devise, thousands of apps have tested edge cases. With custom, you wonder:
- Did I handle expired tokens correctly?
- Is my PKCE implementation secure?
- What about token revocation race conditions?
- Am I compliant with the latest OAuth2 security BCP?
We mitigate this through:
- Comprehensive test suite (95% coverage)
- Security-focused code reviews
- Following security mailing lists
- Regular third-party audits
If I Could Go Back
What I’d do differently:
- Start with better documentation – We documented as we went, but starting with architecture docs would have helped.
- Implement PKCE from day one – We added it later for mobile apps. Should have been there from the start.
- Plan for token rotation earlier – We implemented this in year 2. Should have been v1 feature.
- Set up monitoring sooner – Authentication metrics are critical. We added comprehensive monitoring 6 months in.
What I’d do the same:
- Follow the spec religiously – This saved us during upgrades.
- Write extensive tests – Our test suite caught so many edge cases.
- Document everything – Future-me was very grateful.
- Take security seriously from day one – No shortcuts on auth.
The Future: Where Heis Soma Goes Next
We built Heis Soma to serve our ecosystem, but the vision is bigger:
Phase 1: Internal (Current)
- ✅ Prayer Nook authentication
- ✅ Unity Accord integration
- ✅ Admin tools
- ⏳ Events platform (Q2 2025)
- ⏳ Small groups management (Q3 2025)
Phase 2: Partner Churches (2025-2026)
Imagine a small church with:
- Their own website
- A giving platform
- An event registration tool
- A volunteer coordination app
Currently: 4 different logins, 4 different passwords, 4 different user profiles.
With Heis Soma SSO:
- One login across all tools
- Unified member directory
- Christian values-aligned provider
- No surveillance/data mining
- Church maintains control
We’re building this out now:
# Register a church as OAuth client
church = OauthApplication.create!(
name: "First Baptist Phoenix",
trust_level: :partner,
redirect_uris: [
"https://fbcphoenix.org/auth/callback",
"https://giving.fbcphoenix.org/callback",
"https://events.fbcphoenix.org/callback"
],
scopes: [:basic, :email, :church_membership]
)
Phase 3: Public API (2026+)
Opening to broader ministry ecosystem:
- Bible study apps
- Devotional platforms
- Ministry management tools
- Christian social networks
The vision: A Christian alternative to “Login with Google” that:
- Respects privacy
- Aligns with faith values
- Keeps data in ministry control
- Serves the kingdom, not shareholders
Conclusion: Was It Worth It?
Three years later, I can definitively say: Yes, building Heis Soma was worth it.
Not because we’re smarter than the Devise team (we’re not). Not because our code is better (it’s not). But because our needs aligned with OAuth2 principles, and we had the capacity to do it right.
The decision to build custom authentication in 2021 looked crazy. In 2025, after three Rails upgrades, multiple application integrations, and 1,000+ users, it looks like strategic foresight.
The numbers:
- Development time: ~200 hours initial, ~60 hours/year maintenance
- Cost vs. Auth0/Okta: Saving ~$3,000/year
- Performance improvement: 10x faster than HTTP API calls
- Rails upgrades: Minimal authentication-related changes
- Security incidents: Zero (knock on wood)
The intangibles:
- Deep understanding of authentication
- Confidence in our technical foundation
- Ability to customize for ministry needs
- Career skills (OAuth2 expertise)
- Positioning for future growth
If you’re facing a similar decision, ask yourself:
- Do we have multiple related applications?
- Do we need OAuth2 provider capabilities?
- Do we have the technical capacity?
- Is this a long-term strategic investment?
If you answered yes to most of those, consider the custom route. If not, Devise is excellent and will serve you well.
Ember’s Closing Wisdom: “Sometimes the hardest path leads to the strongest foundation. Penguins may waddle awkwardly on land, but they’re masters in the water. Build for the environment where you’ll spend most of your time.” 🐧→🔥
Code Repository & Resources
Want to dive deeper? I’ve created:
📚 Resources:
- Heis Soma Architecture Case Study – Full technical details
- OAuth2 Security Checklist – What to audit
- Sample OAuth2 Implementation – Simplified example code
- Integration Guide for Partners – How to connect your app
🔗 Related Posts:
- Post #1: Rails Migration Journey – How Heis Soma survived three Rails versions
- Coming Next: Post #3: Multiple Database Connections – Deep dive into the Prayer Nook ↔ Heis Soma architecture
💬 Discussion:
- What’s your authentication architecture?
- Have you built custom auth? Share your story!
- Considering OAuth2? Questions welcome in comments!
About This Series
This post is #2 in the Rails Renaissance series, exploring modern Ruby on Rails development through real-world applications serving a faith-based nonprofit. I’m Christopher “Topher” Warrington, bridging 20+ years of ministry with 20+ years of tech.
Philosophy: Build systems that serve people, write code that lasts, and never forget that behind every OAuth2 flow is a human being seeking connection.
Currently seeking Solutions Engineer roles where I can bridge complex technical systems and human needs. Let’s connect!
Tags: #OAuth2, #Rails8, #RubyOnRails, #Authentication, #SSO, #CustomAuth, #HeisSoma, #SoftwareSecurity, #APIDesign, #NonprofitTech
Published: March 31, 2025 | Reading Time: 14 minutes | Series: Rails Renaissance #2





0 Comments