By the end of this lesson, you will know/be able to:
- Explain what multi-tenancy is and why we implement it.
- Implement multi-tenancy at the routes level.
- Implement multi-tenancy at the controller level.
- Design a database for a mutli-tenant application.
- Available here
Discussion – What is Multitenancy?
Development as a metaphor for development
As a developer (the real-estate variety) you start by buying a small plot of land, and building a little shop. A business moves in and that business has it’s own customers. You can do this for a few more businesses, and each building is completely different, and built to the needs of the business. These are a basic web apps; the type you’re used to building.
After some successes, you build a strip mall. Many businesses move in, and each of them have their own customers. You’ve built one large building to house them all, but each business has their own address. They really aren’t related in any way. They can put up a logo on the front, and they can put whatever fixtures they want inside, but each business is in basically the same space. This is a basic form of multi-tenancy. Squarespace is like this.
Now you want to go big. You build a mall. Your mall has many shops in it, but you can sell gift cards that all of the shops take. Customers can browse all of the stores by walking around the mall, and go into the stores they want. At your mall, customers can bundle items from different shops, and pay for them all at once. Innovation!
This is the kind of multi-tenancy we’re going to talk about. We’re going to build a “mall” where users register once for your site, and can shop anywhere, and check out once.
Multi-tenancy on the web
- View some examples (etsy, ebay, shopify, squarespace)
- What distinguishes a software “platform”?
- What differentiates a store that sells its own goods from a store that lets other people sell goods?
- What other ways might a software company support multiple tenants – multi-instance (self-hosted) vs single-instance (SaaS)
- Marketplace / Platform / Whitelabel – very common software product patterns
- Economics of a multi-tenant product – what is the additional value that the platform owner is providing?
Multitenancy Scoping, Security, and Authorization Concerns
- Recall - what’s the difference between authorization and authentication?
- What additional burdens does a multi-tenant system add with regard to these concepts?
- How can we keep one store from interacting with data from another?
- Multitenancy – kind of a big scary word for just another type of DB relationship
Multitenancy in Rails – DB and Routing Concerns
What about actually adding multitenancy to an application? Let’s consider the changes we would need to make.
Let’s start with the Storedom schema – what changes would we need to make in order to make this application support multiple stores?
- Need a way to represent our stores / tenants
- Need a way to associate other “nested” data with stores
Similarly, at the routing and controller level, we need to identify which specific store we’re talking about at certain points (for example when browsing a list of items)
- How can we include and capture store/tenant information from our URLs?
- What extra work will we need to do in our controllers to account for this?
Code Along – Adding Multitenant Stores to Storedom
For this, we will work through the process of adding a concept of separate stores to the storedom project, and scoping items and orders so that they are attached to specific stores.
git clone https://github.com/turingschool-examples/storedom-5.git multitenancy cd multitenancy bundle && bundle exec rake db:drop db:setup
Here’s a breakdown of the big goals we need to achieve. Let’s see if we as a group can devise a way to tackle these in terms of the routing and relationship constructs Rails gives us.
- Need a record to model our “stores”
- Need a way to view our stores in the app (at least an index / show)
- Need a way to associate an item with a store (Q: what shape does this relationship take)
- Need a way to view an item within the app with contextual info about what store it’s associated with.
- Need a way to prevent accessing the items in the wrong store.
Step 1: Routes
Right now, we have routes leading straight to items and orders view pages.
Our end goal is to see items and orders by store, so we’ll want to modify these routes to be namespaced by
We also want to route to different stores by their
slugis a piece of the URL’s path that is typically a hyphenated version of the title or main piece of a webpage you’re on.
We’ll also change the root of our application to
stores#index as a jumping point for our browsing users.
root `stores#index` namespace :stores, as: :store, path: ':store' do resources :items, only: [:index, :show] end
Step 2: Controllers
Our root path is looking for a
Our namespaced routes are looking for a corresponding
Step 3: Models
We need a
stores table in our database. Each store (for now) will have a
slug. We’re going to create the slug based on the store’s name - not meant for a user to create by hand.
rails g model store name slug
What type of relationship to
To reflect this, we’ll want to add a
store_id column to the
rails g migration AddStoreToItems store:references
Let’s head back to the
Store model to configure.
has_many :items validates :name, uniqueness: true validates :slug, uniqueness: true # callback to execute once uniqueness is verified before_validation :generate_slug def generate_slug self.slug = name.parameterize end
Take a moment to play around with
parameterize (a Rails helper) in your
On the flip-side, we need to add our
belongs_to macro on the
On your own, create at least 4 stores in your
rails console and add at least 2 items to each.
Step 4: Views
Starting with our
stores#index view, let’s loop through each store.
To do so, we’ll need access to all stores in the view.
Let’s also add a link to each store. We’ll want to leverage the slug in our URL to determine which store we’re viewing.
<%= link_to 'Visit Store', store_items_path(store: store.slug)
We’re linking to the
stores/items#index view from the root of our app. Let’s grab all items associated with the store in our slug and loop through those in the view.
How can we make this better?
current_store helper, similar to the common
We can set up a
Stores::StoresController to act like an
ApplicationController scoped just for our stores.
# stores/stores_controller.rb class StoresController < ApplicationController helper_method :current_store def current_store @current_store ||= Store.find_by(slug: params[:store]) end end
Now we can utilize the
current_store helper in our
Discuss with your neighbor the benefits of this scoped
Stores::StoresController and other applications this structure could be useful for.
Step 5: Store Not Found
What error do we see if we visit a store page via a slug that does not exist?
We can add another callback to our
Stores::StoresController that checks to see if our
current_store exists and redirects back to our root if it doesn’t.
before_action :store_not_found def store_not_found redirect_to root_path unless current_store end
Step 6: Modify Navbar
We can replace our
items with a conditional
items link for items in our store if a
link_to “Items”, store_items_path(store: @current_store.slug)
Go through the same procedure for the “orders” model:
- Modify your DB schema and AR relationships to associate an order with a given store
- Create a namespaced route for the orders within a store
- Add a stores/order controller and a view that lists the orders that belong to that store
- Add an order to the store via the console and verify that it works
- Add an orders url in the navbar to access these orders only when a store is present
- When you want text in your URLs, use slugs. Slugs are short text strings that use only lowercase letters, numbers and dashes. Rails gives you
- When you have models that only exist in the context of other models, you’ll nest child resources inside folders for the parent resources in the
- Then to access nested controller actions, use something like the following in your routes file
get ':store/items', to: 'store/items#index', as: :store_items get ':store/items/:id', to: 'store/items#show', as: :store_item
is the same as…
namespace :store, path: ':store', as: :store do get 'items', to: "items#index", as: :items get 'items/:id', to: "items#show", as: :item end
is the same as…
namespace :store, path: ':store', as: :store do resources :items, only: [:index, :show] end
they all give this result when you
Prefix Verb URI Pattern Controller#Action store_items GET /:store/items(.:format) store/items#index store_item GET /:store/items/:id(.:format) store/items#show