Multi Tenancy in Phoenix - S02E02

Show Notes

In this episode we are going to create a multi tenant phoenix 1.3 application using the apartmentex library. We are going to use subdomains to differentiate our tenants.

Stating from a brand new phoenix 1.3 app, lets get apartmentex added.

{:apartmentex, "~> 0.2.3"}
$ mix deps.get && deps.compile

Let's create a context now and add some schemas to it.

$ mix phx.gen.context Accounts Company companies sub_domain:string
$ mix phx.gen.html Accounts User users name:string
$ mkdir priv/repo/tenant_migrations
$ mv priv/repo/migrations/create_accounts_users.exs priv/repo/tenant_migrations

We've moved the create user migration to the folder where apartmentex expects it. This way it can be ran against each new tenant we create.

I'm going to rename the users table in the migration and schema from accounts_users to users. I believe in the final release of phoenix 1.3 this will be default.

In our companies migration file we'll want to create an index for the sub_domain column and make sure sub_domain is unique.

create unique_index(:accounts_companies, [:sub_domain])  

Now I'll migrate the database bringing up the companies table.

$ mix ecto.migrate

With that up let's update our create_company and create_user functions to work with apartmentex.

# accounts.ex
def create_company(attrs \\ %{}) do  
  with changeset <- Company.changeset(%Company{}, attrs),                                                
    {:ok, company} <- Repo.insert(changeset),                                                            
    do: Apartmentex.new_tenant(Repo, company.sub_domain)                                                 
end

def create_user(attrs \\ %{}, sub_domain) do                with changeset <- User.changeset(%User{}, attrs),  
     do: Apartmentex.insert(Repo, changeset, sub_domain)                                                  
end  

Now let's hop into iex and take a look how apartmentex handles our db records for us. I've gone ahead and aliased the modules we'll be using in my .iex.exs. For more info on that check out debugging in phoenix

$ iex -S mix
iex(1)> Accounts.create_company(%{sub_domain: "company-one"})  
iex(2)> Accounts.create_company(%{sub_domain: "company-two"})  
iex(3)> Accounts.create_user(%{name: "Cory One"}, "company-one")  
iex(4)> Accounts.create_user(%{name: "Cory Two"}, "company-two")  
iex(4)> Accounts.create_user(%{name: "Cory Two More"}, "company-two")

We've got our data segregated by tenant, now lets see how we can setup phoenix to access it by the subdomain.

First we'll add the users routes to our router.

# router.ex
resources "/users", UserController  

We'll need a way for our controller actions to have access to to the subdomain. To accomplish this we'll override Phoenix.Controller.Pipeline.action. By doing it this way we'll force our actions to have an arrity of 3 and be explicit about the sub_domain.

# user_controller.ex
def action(conn, _) do  
  [sub_domain | _ ] = String.split(conn.host, ".")                                                       
  apply(__MODULE__, action_name(conn), [conn, conn.params, sub_domain])
end

def index(conn, _params, sub_domain) do  
  users = Accounts.list_users(sub_domain)
  render conn, "index.html", users: users
end  

We'll need to update our accounts API to handle the sub_domain param on list_users.

# accounts.ex
def list_users(sub_domain) do  
  Apartmentex.all(Repo, User, sub_domain)
end  

To access my dev server through a domain I've updated my /etc/hosts to point them to the loopback ip address.

127.0.0.1 company-one.dev.local dev  
127.0.0.1 company-two.dev.local dev  

Everything should be in place now let's fire up our server and give it a look.

$ mix phx.server

By switching subdomains of company-one.dev.local:4000/users and company-two.dev.local:4000/users we get only the users in that companies table.

That should get you started creating a multi tenant app using phoenix 1.3. If you have any questions or comments send me an email cory[at-sign]schmitty.me or reach out on twitter.