Back to Blog
Development

Building Enterprise SaaS: Ruby on Rails + Next.js Architecture

December 20, 202412 min readBy Clickbrat Team
Share:

Why Rails + Next.js?


When we started building MailCopilot, we could have gone with the trendy all-JavaScript stack. Instead, we chose Ruby on Rails + Next.js, and here's why.


The Stack


Backend: Ruby on Rails

  • Mature ecosystem
  • Convention over configuration
  • Built-in multi-tenancy support
  • Excellent for complex business logic
  • Battle-tested at scale

  • Frontend: Next.js (React)

  • Modern, fast UI
  • Great developer experience
  • SSR for SEO and performance
  • TypeScript support
  • Huge component ecosystem

  • Database: PostgreSQL

  • JSONB for flexible data
  • Full-text search
  • Rock-solid reliability
  • Perfect for multi-tenancy

  • Caching: Redis

  • Session storage
  • Background job queue
  • Real-time data
  • Rate limiting

  • The Architecture


    Multi-Tenant from Day One


    Every table has a `tenant_id`:


    ```ruby

    class Message < ApplicationRecord

    belongs_to :tenant

    belongs_to :mailbox


    # Automatically scope all queries to current tenant

    acts_as_tenant :tenant

    end

    ```


    This gives us:

  • **Complete data isolation**
  • **Per-tenant customization**
  • **Simplified querying**
  • **Security by default**

  • API Design


    Rails serves a JSON API, Next.js consumes it:


    ```ruby

    Rails: api/v1/messages_controller.rb

    class Api::V1::MessagesController < ApplicationController

    def index

    messages = current_tenant.messages

    .includes(:mailbox, :classification, :draft_response)

    .order(received_at: :desc)

    .page(params[:page])


    render json: messages, serializer: MessageSerializer

    end

    end

    ```


    ```typescript

    // Next.js: lib/api/messages.ts

    export async function getMessages(page = 1) {

    const response = await fetch(`/api/v1/messages?page=${page}`)

    return response.json()

    }

    ```


    Background Jobs


    Heavy lifting happens asynchronously with Sidekiq:


    ```ruby

    class InboxSyncJob < ApplicationJob

    queue_as :high_priority


    def perform(mailbox_id)

    mailbox = Mailbox.find(mailbox_id)


    # Fetch new emails from Gmail/Outlook

    gmail = GmailService.new(mailbox)

    messages = gmail.fetch_messages(since: mailbox.last_sync_at)


    # Process each message

    messages.each do |email_data|

    message = Message.create_from_email(mailbox, email_data)


    # Queue AI classification

    ClassifyMessageJob.perform_later(message.id)

    end


    mailbox.update(last_sync_at: Time.current)

    end

    end

    ```


    Real-Time Updates


    Action Cable (Rails) + WebSockets (Next.js):


    ```ruby

    Rails: channels/inbox_channel.rb

    class InboxChannel < ApplicationCable::Channel

    def subscribed

    stream_for current_tenant

    end

    end


    Broadcast updates

    InboxChannel.broadcast_to(tenant, {

    type: 'new_message',

    message: MessageSerializer.new(message).as_json

    })

    ```


    ```typescript

    // Next.js: hooks/useInboxSubscription.ts

    export function useInboxSubscription() {

    useEffect(() => {

    const cable = createConsumer(WS_URL)

    const subscription = cable.subscriptions.create('InboxChannel', {

    received: (data) => {

    if (data.type === 'new_message') {

    addMessage(data.message)

    }

    }

    })


    return () => subscription.unsubscribe()

    }, [])

    }

    ```


    Key Design Decisions


    1. Monolith Backend, Not Microservices


    Why?

  • Simpler deployment
  • Easier debugging
  • No distributed system complexity
  • Rails handles our scale (1000+ users)

  • When we'll split:

  • 10,000+ active users
  • Specific bottlenecks identified
  • Team size justifies it

  • 2. JSON API, Not GraphQL


    Why JSON API?

  • Simpler implementation
  • Rails has excellent tooling
  • No over-fetching in our use case
  • REST is well-understood

  • GraphQL trade-offs:

  • More flexible, but added complexity
  • We don't need that flexibility yet
  • Can add later if needed

  • 3. Server-Side Rendering (SSR) for Marketing Pages


    Public pages use Next.js SSR:

  • Better SEO
  • Faster initial load
  • Social media previews work

  • Authenticated app is client-side:

  • Faster navigation
  • Better UX
  • No SEO needed

  • 4. Database Choices


    Why PostgreSQL?

  • JSONB for flexible AI response data
  • Full-text search for emails
  • Strong consistency guarantees
  • Mature Rails integration

  • Not using:

  • MongoDB: Don't need document flexibility
  • MySQL: PostgreSQL features are superior
  • Separate search engine: PostgreSQL FTS is enough

  • Performance Optimizations


    1. Query Optimization


    N+1 Queries Prevention:


    ```ruby

    Bad

    messages = Message.all

    messages.each do |message|

    puts message.classification.category # N+1 query

    end


    Good

    messages = Message.includes(:classification).all

    messages.each do |message|

    puts message.classification.category # Single query

    end

    ```


    Database Indexes:


    ```ruby

    add_index :messages, [:tenant_id, :received_at]

    add_index :messages, [:mailbox_id, :processed]

    add_index :classifications, [:message_id, :category]

    ```


    2. Caching Strategy


    Redis Caching:


    ```ruby

    class Message < ApplicationRecord

    def ai_summary

    Rails.cache.fetch("message_summary_#{id}", expires_in: 1.hour) do

    OpenAI.generate_summary(body)

    end

    end

    end

    ```


    Fragment Caching:


    ```ruby

    View caching

    <% cache message do %>

    <%= render message %>

    <% end %>

    ```


    3. Background Processing


    Queue Priorities:


    ```ruby

    class EmailSyncJob < ApplicationJob

    queue_as :high_priority

    end


    class AnalyticsJob < ApplicationJob

    queue_as :low_priority

    end

    ```


    Batch Processing:


    ```ruby

    Message.unprocessed.find_in_batches(batch_size: 100) do |batch|

    ClassifyBatchJob.perform_later(batch.map(&:id))

    end

    ```


    Security Considerations


    1. Tenant Isolation


    Row-Level Security:


    ```ruby

    class ApplicationController < ActionController::API

    before_action :set_current_tenant


    private


    def set_current_tenant

    ActsAsTenant.current_tenant = current_user.tenant

    end

    end

    ```


    2. API Authentication


    JWT Tokens:


    ```ruby

    def encode_token(payload)

    JWT.encode(payload, Rails.application.secrets.secret_key_base)

    end


    def decode_token(token)

    JWT.decode(token, Rails.application.secrets.secret_key_base)[0]

    end

    ```


    3. Rate Limiting


    Rack Attack:


    ```ruby

    Rack::Attack.throttle('api/ip', limit: 100, period: 1.minute) do |req|

    req.ip if req.path.start_with?('/api/')

    end

    ```


    Deployment


    Production Stack


  • **Hosting**: AWS / DigitalOcean
  • **Web Server**: Puma (Rails)
  • **Process Manager**: Systemd
  • **Reverse Proxy**: Nginx
  • **SSL**: Let's Encrypt
  • **Background Jobs**: Sidekiq
  • **Monitoring**: Sentry + DataDog

  • CI/CD Pipeline


    ```yaml

    .github/workflows/deploy.yml

    name: Deploy

    on:

    push:

    branches: [main]


    jobs:

    deploy:

    runs-on: ubuntu-latest

    steps:

    - uses: actions/checkout@v2

    - name: Run Tests

    run: bundle exec rspec

    - name: Deploy to Production

    run: cap production deploy

    ```


    Lessons Learned


    1. Rails is Still Relevant


    Despite the hype around newer frameworks:

  • Rails is fast enough for most SaaS
  • Productivity is unmatched
  • Mature ecosystem solves common problems
  • Great for complex business logic

  • 2. Next.js is Worth the Learning Curve


    Modern React with Next.js provides:

  • Excellent developer experience
  • Fast, responsive UIs
  • Great SEO when needed
  • Easy deployment

  • 3. Don't Over-Engineer Early


    We started simple:

  • Monolithic Rails app
  • Basic Next.js frontend
  • PostgreSQL + Redis
  • Deployed on one server

  • Scale when you need to, not before.


    Want to Build Something Similar?


    We've built three production SaaS applications with this stack. If you're planning a new project, [let's discuss](/contact) how we can help.




    *Questions about this architecture? [Reach out](/contact) — we love talking about technical decisions.*


    Ready to Transform Your Business with AI?

    Let's discuss how we can build a custom solution for you.