For many applications, access is usually through a single domain, such as yourapp.com. This way, the application developer is able to offer a unified experience to all users. This works great most of the time, but imagine a situation where you need to give each user a customized experience; how would you achieve this? One of the ways you can customize the user experience in a single application is by using account-based subdomains. Instead of giving users a single entry point into your app, offer them customized subdomains: user1.exampleapp.com, user2.exampleapp.com, and so forth. With account-based subdomains, you essentially give users an individualized entry point where they can manage their app experience as they wish. In this article, you’ll learn what account-based subdomains are all about, why they matter, and when you should use them. You’ll iteratively build a simple Ruby on Rails application that implements account-based subdomains, which should provide foundational knowledge you can use when building your next app.
Pre-requisites
Here’s what you’ll need to follow along with this tutorial:
- Ruby on Rails 7 installed on your development machine (we’ll use version 7.1.1 for this tutorial).
- Some experience using Ruby and Rails since we won’t cover beginner concepts in this tutorial.
- You can find the source code for the example app we’ll be building here.
What are account-based subdomains?
Account-based subdomains are a common feature of multi-tenant applications and are subdomains named after user accounts: user1.exampleapp.com, account1.exampleapp.com, and so forth. Each subdomain acts as an individualized entry point for users belonging to that account. This fine-grained separation of users gives you two main advantages:
- Personalization – Since users are separated at the account level, it’s often easier to offer personalized user experiences, such as custom branding and settings.
- Security – A major concern for any app developer building a multi-tenant app is security, especially how to prevent users from accessing other user accounts. With an account subdomain-based structure, you have more control when it comes to isolating user data.
As you can see, account-based subdomains are great for separating user data in multi-tenant environments. You can use them in a multi-tenant app that utilizes either a single database or multiple databases. For this tutorial, we’ll use a multi-tenant single database because it is simpler to implement.
The app we’ll be building
The app we’ll be building is a Ruby on Rails 7 to-do app featuring account-based subdomains that allow different users to log into their own accounts and create to-do task lists that cannot be accessed by other users.
Creating the app skeleton
Spin up a new Rails 7 application by running the command below:
rails new todo_app -css=tailwind
It’s not necessary to use Tailwind CSS for your app; you can use whatever meets your needs. Now cd into the app directory and run the command below to create the database (we use SQLite to keep things simple):
cd todo_app
rails db:create
Adding basic user authentication
There’s no need to recreate things from the ground up. Let’s use the Devise gem to quickly build out a user authentication system for our to-do app. Start by adding the Devise gem to the app’s Gemfile:
# Gemfile
# ...
gem 'devise'
# ...
Then run bundle install
, followed by rails devise:install
. Additionally, make sure to follow the instructions that will be printed to console after the Devise installation, especially the one on adding the line config.action_mailer.default_url_options = { host: 'localhost', port: 3000 }
to the development configuration. Otherwise, you might experience weird configuration errors.
Next, create the basic user model by running the commands shown below:
rails generate devise User && rails db:migrate
Up to this point, you should have:
- A working user model
- Basic authentication routes, including login and logout routes.
Next, let’s add a root route to act as our app’s home page.
Adding a homepage
We don’t need to create a dedicated home page; instead, let’s define the login page as the app’s homepage by modifying the routes.rb file as shown below:
# config/routes.rb
Rails.application.routes.draw do
# ...
devise_scope :user do
root 'devise/sessions#new'
end
end
Now spin up the app’s server with bin/dev
, and you should be greeted by the user login page when you visit localhost:3000. We’re making good progress so far. Let’s now shift our attention to the Todo model next.
Modeling to-dos
Let’s now add the Todo model with the command shown below:
rails generate scaffold Todo title description:text status:boolean users:references
Then run the migration that will be generated with rails db:migrate
. Finally, open up the User model and modify it to associate it with the newly created Todo model:
# app/models/user.rb
class User < ApplicationRecord
# ...
has_many :todos
end
Next, let’s make sure a to-do is associated with the user who created it.
Associating to-dos to users
Open the todos_controller.rb file and modify the create method as shown below:
# app/controllers/todos_controller.rb
class TodosController < ApplicationController
# ...
def create
@todo = Todo.new(todo_params)
@todo.user_id = current_user.id # Add this line
# ...
end
# ...
end
Now, whenever a new to-do is created, its user_id field is populated with the currently logged in user’s ID. With that done, it would be a good idea to redirect users to the index of to-dos after successful login. Let’s build this functionality next.
Redirecting users to the to-do index
Open the application_controller.rb file and add the following lines:
# app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
# ...
def after_sign_in_path_for(resource)
todos_path
end
end
Additionally, let’s also ensure that all Todo CRUD actions can only be performed by authenticated users:
# app/controllers/todos_controller.rb
class TodosController < ApplicationController
before_action :authenticate_user!
# ...
end
Go ahead and register a couple of users. Then log in as these users and create a few to-do items. Now, since we haven’t created any separation in the user data, it’s possible for users to access each other’s data.
Next, let’s work on user-data separation and the account-based subdomain structure so that whenever user1 logs into their dashboard, they are only able to see their to-do items. Likewise, user2 and other users of the application will only be able to see their own data.
Separating user data and implementing account-based subdomains
When it comes isolating user data in a multi-tenant environment, there are a few options:
- Separate databases and schemas for all users – It’s possible to isolate each user by creating separate databases and schemas for each one. This is the most secure strategy, but it’s very complex to implement and costly to maintain.
- Same database, separate schema for all users – Another very secure strategy with separate schemas for each user but still uses the same database. Relatively costly and complex to set up and maintain.
- Same database, same schema for all users – This is the most common and the most cost-effective method employed by many app developers when dealing with multiple users or accounts. Here, all users share the same database and schema, and it’s the strategy we’ll follow for the app we’re building.
It’s possible to build multi-tenant functionality with subdomains using various gems, such as the Apartment gem, acts_as_tenant, activerecord_multi_tenant, and others. However, for this tutorial, we will not use any gems for this purpose. Our first task will be to create a Tenant model to store information about the tenant (or account), including the subdomain name.
Setting up the tenant structure
Create a new model named Account with the attributes subdomain and user_id:
rails generate model Account subdomain user:references
In a real-world multi-tenant application, you would structure this type of tenant model in such a way that multiple users (i.e., members of the same tenant or account) can be associated with a tenant. However, since ours is a simple app, we’ll assume that a single tenant is associated with a single user. Now modify the User and Todo models as shown below:
# app/models/user.rb
class User < ApplicationRecord
# ...
has_one :account, dependent: :destroy
end
And the Todo model:
# app/models/todo.rb
class Todo < ApplicationRecord
belongs_to :account
end
Next, let’s modify the user creation action so that an account is created automatically upon user creation.
Creating accounts upon user creation
To automatically create an account upon user creation, we’ll need to modify the create action in the Devise registrations controller. Run the following command to generate this controller:
rails generate devise:controllers users -c=registrations # Depending on how you've...
Source link