Create a multi-tenant, whitelabel application in Elixir & Phoenix part III: Adding accounts (tenants)

Makis
Red Squirrel
Published in
9 min readMar 9, 2023

In the last post in the series, we added a custom router and enabled our application to dispatch subdomain requests to separate controllers which loaded different pages based on the domain requested.

The posts in these series are:

In this article we will introduce the concept of accounts, in this context also known as “tenants”, and see how we can load tenant-specific content (brand colour and products) based on their associated subdomain. Here’s a small demo of the end result:

End result gif showing how different subdomains load different results like different background colour and products, things that are specific to a tenant.

What are “tenants”? (Explaining multi-tenancy)

Multi-tenancy represents a specific software arhitecture where groups of users use the same piece of software but each group has different privileges, configuration (i.e branding) and in general experiences the app differently than other groups. A group of users is referred to as a “tenant”. A tenant usually refers to an organisation. An example of a multi-tenant application is Slack. You can read more about software multi-tenancy here.

In Elixir we are very fortunate to have some great packages that allow us to add specific functionality into our apps without reinventing the wheel. This is true for adding multi-tenancy as well where we can use a package called Triplex. From Triplex’s README:

A simple and effective way to build multitenant applications on top of Ecto.

Triplex leverages database data segregation techniques (such as Postgres schemas) to keep tenant-specific data separated, while allowing you to continue using the Ecto functions you are familiar with.

Let’s take a closer look at how this looks in practice.

Adding Triplex

Head back to your app and add Triplex as a depencency in your mix.exs file:

def deps do
[
....
{:triplex, "~> 1.3.0"}
]
end

and fetch it into the project by running the following on the command line:

mix deps.get

We also need to update our configuration to include Triplex:

# config/config.exs
config :triplex, repo: Fresco.Repo, tenant_prefix: "fresco_"

One part worth clarifying from the above is the tenant_prefix key. This instructs Triplex to prefix any tenant name with fresco_ so a tenant with name green would have a schema called fresco_green. This makes it easier to identify tenant-specific schemes.

Working with tenants using Triplex means working with different Postgres schemas, one for each tenant. The first thing we'll do is add an accounts table in the public schema, the default app schema which is not tenant-specifc. There we will store all the tenants and any specific configuration a tenant may have. In a rather contrived example, we will store a specific branding colour for each tenant alongside their name.

There are many pieces to go through so to speed up the process we’ll use the built in Phoenix generators, in this case the context generator, to create some of the code we need. Run the following command in the project’s directory:

mix phx.gen.context Accounts Account accounts name:string:unique brand_colour:string

The above will generate a few files for us but a couple of key ones are:

  • The Accounts module/context which we will use to manage accounts: lib/fresco/accounts.ex
  • The migration file to create the accounts table with two fields name, which is unique, and brand_colour: priv/repo/migrations/20230307111651_create_accounts.exs (the timestamp will be different)

You should see a similar output like the below:

phoenix context generator CLI output

Your migration file should have contents very similar to the below:

defmodule Fresco.Repo.Migrations.CreateAccountsTable do
use Ecto.Migration

def change do
create table(:accounts) do
add :name, :string
add :brand_colour, :string

timestamps()
end

create unique_index(:accounts, :name)
end
end

Run the migrations with mix ecto.migrate and you should now have an accounts table in Postgres with the two columns we specified.

We need to make one change to our Accounts context. In the file lib/fresco/accounts.ex change this line:

def get_account!(id), do: Repo.get!(Account, id)

to this:

def get_account!(name), do: Repo.get_by!(Account, name: name)

We’re essentially changing the field that we will be querying for an account to use the account’s name (aka tenant name).

Tenants Helper

Triplex comes with plenty of convenient functions to manage tenants. Because we will also associate a tenant with an account, we’ll create a TenantsHelper moduel which will be responsible for creating both a tenant using Triplex and a record for that tenant in the accounts table.

Create the following file: lib/fresco/tenants_helper.ex

Inside this file add the following code:

defmodule Fresco.TenantsHelper do
alias Fresco.Accounts
alias Fresco.Repo

def create_tenant(name, brand_colour) do
Triplex.create_schema(name, Repo, fn(tenant, repo) ->
{:ok, _} = Triplex.migrate(tenant, repo)

Repo.transaction(fn ->
with {:ok, account} <- Accounts.create_account(%{name: tenant, brand_colour: brand_colour}) do
{:ok, account}
else
{:error, error} ->
Repo.rollback(error)
end
end)
end)
end
end

There a couple of things worth mentioning in the above code. One is the lack of Triplex.create which is what we normally call to create a tenant. That is because we want to combine the creation of a tenant with that of an account using a transaction. If one of the calls fails for whatever reason, we don't want the other one to execute. The recommended way in Triplex docs seems to have an issue so the code is adapted to the solution offered in this discussion.

With the above in place, let’s load the code in the Elixir repl with:

iex -S mix

Now, let’s create our tenants with their account records. We should use one of the subdomains we used in the previous tutorials:

iex(1)> Fresco.TenantsHelper.create_tenant("green", "#00b300")
...
iex(2)> Fresco.TenantsHelper.create_tenant("farm", "#e5e2a7")

If you connect to your running Postgres instance using psql, you can check that you have two newly created schemas called fresco_green and fresco_farm with their equivalent account records with the following two commands:

-- Get all schemas
select schema_name from information_schema.schemata;

-- Get all accounts
select * from public.accounts;

You should see a similar result to the following picture:

SQL queries in psql showing all schemas and all accounts

Adding Products

Since our app around the concept of farmer’s market-like shops, the next step is to add products. Products will be tenant-specific. There are many customisations a tenant might want to do on a given product but a good example is one tenant might want to sell fresh tomatoes at a different price than another. To create tenant-specific tables, we will use Triplex’s migration generator. Run the following command:

mix triplex.gen.migration create_products_table

This will create a new migration file under a new folder called tenant_migrations. Bar the timestamp, the path should look similar to this: priv/repo/tenant_migrations/20230306101542_create_products_table.exs.

Inside this file let’s add the code to create the products table:

defmodule Fresco.Repo.Migrations.CreateProductsTable do
use Ecto.Migration

def change do
create table("products") do
add :name, :string
add :price, :string

timestamps()
end
end
end

Note: In a non-toy application you will want to do things differently (i.e adding constraints etc) but to keep things simple and short we will leave it to the code above.

Run the migration with:

mix triplex.migrate

This should create two products tables, one in each schema, as shown in the picture below:

Triplex migration CLI output

Time to add the context and Ecto schema we’ll use to interact with the above tables. Under lib/fresco/, create a new directory called products and inside a file called product.ex:

mkdir lib/fresco/products

touch lib/fresco/products/product.ex

Inside the product.ex file add the following code:

defmodule Fresco.Products.Product do
use Ecto.Schema
import Ecto.Changeset

schema "products" do
field :price, :string
field :name, :string

timestamps()
end

@doc false
def changeset(product, attrs) do
product
|> cast(attrs, [:name, :price])
|> validate_required([:name, :price])
end
end

Now let’s add the context that will use the above schema. Inside lib/fresco/ create a new file called products.ex:

touch lib/fresco/products.ex

Add the following code inside of it:

defmodule Fresco.Products do
import Ecto.Query, warn: false
alias Fresco.Repo
alias Fresco.Products.Product

def list_products(tenant) do
Repo.all(Product, prefix: Triplex.to_prefix(tenant))
end

def create_product(tenant, attrs \\ %{}) do
%Product{}
|> Product.changeset(attrs)
|> Repo.insert(prefix: Triplex.to_prefix(tenant))
end
end

The above looks fairly standard if you look at other Elixir/Phoenix/Ecto tutorials. The key noticable difference is the fact that we’re passing the tenant (i.e the tenant’s name) as the first argument to both of our functions and inside the functions we use Triplex to get the full prefix of a tenant (i.e fresco_green vs just green). We do this to utilise Ecto's prefix argument that tells Ecto which schema to run the query against.

Let’s use the above to create a product for each tenant. Exit and restart the Elixir repl with iex -S mix and run the following commands to create a product for each of our two tenants:

iex(1)> Fresco.Products.create_product("green", %{name: "Fresh Tomatoes", price: "2.99"})
...
iex(2)> Fresco.Products.create_product("farm", %{name: "Fresh Tomatoes", price: "3.99"})

Going back into our Postgres repl and using psql we can check that each table has one product by running the following psql commands:

-- Check the products of the tenant "green"
select * from fresco_green.products;

-- Check the products of the tenant "farm"
select * from fresco_farm.products;

You should see a similar output to this:

Listing products for each tenant in the Postgres shell

Showing Products

For the final strech, we will update our web resources to add an endpoint which we can visit and see the products for each tenant. Let’s start with by adding the endpoint in the subdomain router file lib/fresco_web/subdomain_router.ex:

get "/", PageController, :index

# Add the below
get "/products", ProductsController, :index

Next, let’s create the necessary controller. Create the following file inside the lib/fresco_web/subdomain/ folder:

touch lib/fresco_web/subdomain/products_controller.ex

Inside this new file we’ll add the following code:

defmodule FrescoWeb.Subdomain.ProductsController do
use FrescoWeb, :controller

alias Fresco.Products
alias Fresco.Accounts

def index(conn, _params) do
tenant = conn.private[:subdomain]
all_products = Products.list_products(tenant)
brand_colour = Accounts.get_account!(tenant).brand_colour

render(
conn,
"index.html",
%{
subdomain: tenant,
products: all_products,
style: "background-color: #{brand_colour};"
}
)
end
end

The first thing we do in the index action is to grab the subdomain which also indicates the tenant making the request. We then grab all the products for that tenant. We also fetch the specific account related to that tenant and grab the brand colour associated with it. We then pass this information to our template.

Note I: You’ll notice that we’re passing CSS styling to the template. This is not something I would do in a real-world project but it helps keep the example simple 🙂

Note II: Triplex also provides a plug which helps you extract the tenant from the request. See the docs for this.

Let’s create the view and necessary template for the above controller and action:

# Create ProductsView file
touch lib/fresco_web/views/subdomain/products_view.ex

# Create products template folder
mkdir lib/fresco_web/templates/subdomain/products

# Create products index template
touch lib/fresco_web/templates/subdomain/products/index.html.heex

The code for the view is minimal:

# lib/fresco_web/views/subdomain/products_view.ex

defmodule FrescoWeb.Subdomain.ProductsView do
use FrescoWeb, :view
end

And here’s the code for the index template:

<section class="phx-hero" style={@style}>
<h1><%= gettext "Welcome to %{name}!", name: @subdomain %></h1>
<p>Peace of mind from prototype to production</p>
</section>

<section class="row">
<article class="column">
<h2>Products</h2>
<ul>
<%= for product <- @products do %>
<li>
Product: <%= product.name %> | Price: <%= product.price %>
</li>
<% end %>
</ul>
</article>
</section>

Party time

If you have followed all the way through, now comes the payoff. Start the Phoenix server with iex -S mix phx.server. Open two browser tabs, one pointing to http://green.fresco.com:4000/products and the other to http://farm.fresco.com:4000/products. You should be greeted by the following:

Two browser tabs, one for each subdomain, showing different results that match the specific subdomain/tenant

In these tabs you should see that each submdomain that maps to a different tenant loads different results. The banner background has a different colour and the products it loads are specific to that tenant.

Summary

We covered a lot of stuff so here’s a brief summary of everything we covered in these three posts:

I hope you as the reader have found these series useful and thank you in advance for any time you have spent on them. Until next time! 👋🏼

--

--

Makis
Red Squirrel

Tries to solve problems, occasionally with software.