Implementing Multi-Tenancy in an `existing` Ruby On Rails (6+, 7+)Applications Using Apartment Gem and Subdomains (Part 3/3)

Hamzawais
4 min readMar 22, 2024

This is the third and final part of the article. If you want to see what we did earlier, Check part 1 and part 2.

Updating Sidekiq (or other background/cron) Jobs

class CheckInventoryWorker
include Sidekiq::Worker

def perform(*args)
Clinic.all.each do |current_clinic|
Apartment::Tenant.switch!(clinic.subdomain)

begin
puts "Checking inventory for #{current_clinic.id}"
rescue
puts "Checking inventory Job failed for #{current_clinic.id}"
end
end
end
end

Now, whenever we run a job — we will iterate over all the tenants, and switch the schema to the current tenant.

Updates to Production.rb (Or any other environment)

# config/environments/production.rb

Rails.application.configure do
# Settings specified here will take precedence over those in config/application.rb.

# Code is not reloaded between requests.
config.cache_classes = true

# Eager load code on boot. This eager loads most of Rails and
# your application in memory, allowing both threaded web servers
# and those relying on copy on write to perform better.
# Rake tasks automatically ignore this option for performance.
config.eager_load = true

# Full error reports are disabled and caching is turned on.
config.consider_all_requests_local = false
config.action_controller.perform_caching = true

# Ensures that a master key has been made available in either ENV["RAILS_MASTER_KEY"]
# or in config/master.key. This key is used to decrypt credentials (and other encrypted files).
# config.require_master_key = true

# Disable serving static files from the `/public` folder by default since
# Apache or NGINX already handles this.
config.public_file_server.enabled = true #ENV['RAILS_SERVE_STATIC_FILES'].present?

# Compress CSS using a preprocessor.
# config.assets.css_compressor = :sass

# Do not fallback to assets pipeline if a precompiled asset is missed.
config.assets.compile = true

# Enable serving of images, stylesheets, and JavaScripts from an asset server.
# config.action_controller.asset_host = 'http://assets.example.com'

# Specifies the header that your server uses for sending files.
# config.action_dispatch.x_sendfile_header = 'X-Sendfile' # for Apache
# config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' # for NGINX

# Store uploaded files on the local file system (see config/storage.yml for options).
config.active_storage.service = :amazon

# Mount Action Cable outside main process or domain.
# config.action_cable.mount_path = nil
# config.action_cable.url = 'wss://example.com/cable'
# config.action_cable.allowed_request_origins = [ 'http://example.com', /http:\/\/example.*/ ]

config.action_cable.url = 'wss://yourdomain.com//cable' # ws:// is non-secure, wss:// is secure
config.action_cable.allowed_request_origins = [ 'https://yourdomain.com/' ]

# Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies.
config.force_ssl = true

# Use the lowest log level to ensure availability of diagnostic information
# when problems arise.
config.log_level = :debug

# Prepend all log lines with the following tags.
config.log_tags = [ :request_id ]

# Use a different cache store in production.
# config.cache_store = :mem_cache_store

# Use a real queuing backend for Active Job (and separate queues per environment).
# config.active_job.queue_adapter = :resque
# config.active_job.queue_name_prefix = "railsapp_production"

config.action_mailer.perform_caching = false

# Ignore bad email addresses and do not raise email delivery errors.
# Set this to true and configure the email server for immediate delivery to raise delivery errors.
# config.action_mailer.raise_delivery_errors = false

# Enable locale fallbacks for I18n (makes lookups for any locale fall back to
# the I18n.default_locale when a translation cannot be found).
config.i18n.fallbacks = true

# Send deprecation notices to registered listeners.
config.active_support.deprecation = :notify

# Use default logging formatter so that PID and timestamp are not suppressed.
config.log_formatter = ::Logger::Formatter.new

# Use a different logger for distributed setups.
# require 'syslog/logger'
# config.logger = ActiveSupport::TaggedLogging.new(Syslog::Logger.new 'app-name')

if ENV["RAILS_LOG_TO_STDOUT"].present?
logger = ActiveSupport::Logger.new(STDOUT)
logger.formatter = config.log_formatter
config.logger = ActiveSupport::TaggedLogging.new(logger)
end

# Do not dump schema after migrations.
config.active_record.dump_schema_after_migration = false

config.active_record.dump_schema_after_migration = false
config.action_mailer.delivery_method = :smtp
ENV['ROOT_URL']='https://yourdomain.com/'

config.action_mailer.default_url_options = { protocol: 'https', host: 'https://yourdomain.com' }

config.action_mailer.raise_delivery_errors = true
# SMTP settings for sendgrid
ActionMailer::Base.smtp_settings = {
domain: 'yourdomain.com',
address: 'email-smtp.us-east-1.amazonaws.com',
port: 587,
authentication: :plain,
user_name: Rails.application.credentials[:production].dig(:smtp_ses, :user_name),
password: Rails.application.credentials[:production].dig(:smtp_ses, :password)
}
end

Updating Actioncable (Optional)

# app/channels/application_cable/connection.rb
# frozen_string_literal: true

module ApplicationCable
class Connection < ActionCable::Connection::Base
identified_by :current_user


def connect
subdomain = request.env['HTTP_HOST'].split('.').first
Apartment::Tenant.switch!(subdomain)

user_id = cookies.encrypted[:user_id]

return reject_unauthorized_connection if user_id.nil?

# your custom logic here

return reject_unauthorized_connection if user.nil?

self.current_user = user
end


protected

def session
@request.session
end

def find_verfied_user
# your custom logic here
end
end
end
// app/javascript/channels/consumer.js
import { createConsumer } from "@rails/actioncable"

// Dynamically determine the WebSocket URL based on the current location
const protocol = window.location.protocol === 'https:' ? 'wss://' : 'ws://';
const host = window.location.host; // Includes subdomain and port if any
const actionCableUrl = `${protocol}${host}/cable`;

// Create the Action Cable consumer with the dynamically set URL
export default createConsumer(actionCableUrl)

The above will take care of Rails action cable.

There are still some pieces left that I did not cover, for example:

  1. Updating Nginx
  2. Updating Route 53
  3. Adding SSL certificate
  4. Data migration (if you have an existing application)

If you wish to learn about any of the above, or any other related piece do let me know. I would love to discuss and help you out.

Happy developing!

Hamza Awais

You get in touch with me:

email: hamzawais54@gmail.com | Phone: +923244105651 | web: meet-hamza.com | linkedin: https://www.linkedin.com/in/hamza-awais-1908/

--

--