Set up a basic multi-tenant architecture in Rails without gem - 2
Harsh patel
Posted on February 3, 2023
Please go through this article
Multi tenancy with Apartment Gem
A simple multi-tenancy architecture in Rails without using the apartment gem can be implemented using a subdomain-based approach. Here's an outline of the steps:
Set up your Rails app with subdomains routing:
# config/routes.rb
Rails.application.routes.draw do
constraints(subdomain: /.+/) do
resources :tenants, only: [:show]
resources :posts, only: [:index, :show]
root to: 'tenants#show'
end
root to: 'home#index'
end
Create a Tenant model to store information about each tenant:
# app/models/tenant.rb
class Tenant < ApplicationRecord
validates :subdomain, presence: true, uniqueness: true
has_many :posts
end
Create a middleware to set the current tenant based on the subdomain:
# app/middleware/current_tenant.rb
class CurrentTenant
def initialize(app)
@app = app
end
def call(env)
tenant = Tenant.find_by(subdomain: request.subdomain)
if tenant
RequestStore.store[:tenant] = tenant
@app.call(env)
else
[404, { 'Content-Type' => 'text/plain' }, ['Tenant not found.']]
end
end
private
def request
@request ||= Rack::Request.new(env)
end
end
Use the middleware in your Rails app:
# config/application.rb
config.middleware.use CurrentTenant
Scope your models to the current tenant:
# app/models/post.rb
class Post < ApplicationRecord
belongs_to :tenant
default_scope -> { where(tenant_id: RequestStore[:tenant].id) }
end
Use the tenant data in your controllers and views:
# app/controllers/tenants_controller.rb
class TenantsController < ApplicationController
def show
@tenant = RequestStore[:tenant]
end
end
# app/views/tenants/show.html.erb
<h1><%= @tenant.name %></h1>
<%= link_to 'Posts', posts_path %>
This implementation provides a basic setup for a multi-tenancy architecture in Rails, with the tenant information being set based on the subdomain, and the models being scoped to the current tenant. You can further add error handling and security measures as required.
Example Request: GET http://tenant1.example.com/posts
Example Response:
HTTP/1.1 200 OK
Content-Type: text/html; charset=utf-8
...
<h1>Tenant 1</h1>
<a href="/posts">Posts</a>
2nd Example
class Tenant < ApplicationRecord
has_many :users
end
class User < ApplicationRecord
belongs_to :tenant
end
class TenantMiddleware
def initialize(app)
@app = app
end
def call(env)
tenant = Tenant.find_by(subdomain: request.subdomain)
if tenant
Current.tenant = tenant
else
raise ActiveRecord::RecordNotFound
end
@app.call(env)
end
end
Add a scope to your User model:
class User < ApplicationRecord
belongs_to :tenant
default_scope -> { where(tenant: Current.tenant) }
end
Configure your database connection:
class ApplicationRecord < ActiveRecord::Base
self.abstract_class = true
establish_connection(ENV.fetch("#{Rails.env}_DATABASE_URL"))
end
class Tenant < ApplicationRecord
establish_connection("#{Rails.env}_tenant_#{id}")
has_many :users
end
class User < ApplicationRecord
belongs_to :tenant
default_scope -> { where(tenant: Current.tenant) }
end
Now, the application will automatically switch database connections based on the subdomain in the request URL. This way, each tenant will have their own isolated data.
Example request-response:
Request:
GET http://tenant1.example.com/users
Response -:
[
{
"id": 1,
"name": "User 1",
"email": "user1@example.com"
},
{
"id": 2,
"name": "User 2",
"email": "user2@example.com"
}
]
3rd Example
- First, you'll need to create a model to represent the tenant. For example:
class Tenant < ApplicationRecord
has_many :photos
validates :name, presence: true, uniqueness: true
end
- Next, you'll need to create a model to represent the photos that are uploaded by each tenant. For example:
class Photo < ApplicationRecord
belongs_to :tenant
has_one_attached :image
validates :image, presence: true
end
- You'll need to create a mechanism to set the tenant context for each request. One way to do this is to use a
before_action
in yourApplicationController
. For example:
class ApplicationController < ActionController::Base
before_action :set_tenant
private
def set_tenant
tenant = Tenant.find_by(subdomain: request.subdomain)
Tenant.set_current_tenant(tenant)
end
end
4.In your controller, you'll need to handle the file attachment and save it to the database. For example:
class PhotosController < ApplicationController
def new
@photo = Photo.new
end
def create
@photo = Tenant.current_tenant.photos.new(photo_params)
if @photo.save
redirect_to @photo, notice: "Photo was successfully uploaded."
else
render :new
end
end
private
def photo_params
params.require(:photo).permit(:image)
end
end
- In your routes, you'll need to specify the tenant_id in the URL to ensure that each photo is associated with the correct tenant. For example:
constraints subdomain: /^[\w-]+/ do
resources :photos, only: [:new, :create]
end
- Finally, you'll need to add a form field for the file attachment in your view. For example:
<%= form_with model: @photo, local: true do |form| %>
<% if @photo.errors.any? %>
<div id="error_explanation">
<h2><%= pluralize(@photo.errors.count, "error") %> prohibited this photo from being saved:</h2>
<ul>
<% @photo.errors.full_messages.each do |message| %>
<li><%= message %></li>
<% end %>
</ul>
</div>
<% end %>
<div class="field">
<%= form.label :image %>
<%= form.file_field :image %>
</div>
<div class="actions">
<%= form.submit %>
</div>
<% end %>
Here's an example of an HTTP request and response for uploading a photo for a tenant with subdomain "tenant1":
Request -:
POST http://tenant1.yourdomain.com/photos
Content-Type: multipart/form-data
photo[image]: (binary data for the photo image file)
Response -:
HTTP/1.1 201 Created
Content-Type: application/json
{
"status": "success",
"message": "Photo was successfully uploaded."
}
Here's an example of an HTTP request and response for uploading a photo:
#Request -:
POST /tenants/1/photos
Content-Type: multipart/form-data
photo[image]: (binary data for the photo image file)
#Response -:
HTTP/1.1 201 Created
Content-Type: application/json
{
"status": "success",
"message": "Photo was successfully uploaded."
}
Posted on February 3, 2023
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.