Setting Up a Multi-tenant Phoenix App for Elixir
Tags: elixir
Published: 2023-12-05 11:18:16

Apps built with Elixir can support massive scalability, real-time interactivity, great fault tolerance, and the language's syntax is actually a joy to use. Elixir is a natural fit for applications such as chat apps, data dashboard apps, and anything needed to support a large userbase.

In this article, we'll use Elixir — specifically, the web framework Phoenix — to build a multi-tenant link shortening app.

Let's get started!

## What's Needed for Our Phoenix Project Built with Elixir

With the growing popularity of social media, there's been an increase in the use of URL shortening services. As a budding entrepreneur, you believe there's a market for yet another URL shortening service. And since such an app lives in between a generated short URL and the long version, it's necessary that the app be fast and scalable.

These features and more are already built into Phoenix, which makes it a great choice for this project, enabling you to stand out in the market.

## Takeaways

After this tutorial, you will:

- Understand the internal workings of an Elixir/Phoenix app (for example, how to create and use a custom Ecto type, how to handle authentication, and more).
- Understand the concept of multi-tenancy, including the different strategies available.
- Build a multi-tenant link shortening Elixir/Phoenix app that can be extended into something more advanced with real-world use.

To follow along with the tutorial, let's set up a few things.

## Prerequisites

- Elixir installed on your development machine. If you don't have it installed, follow [this guide](https://elixir-lang.org/install.html). For this tutorial, we're using Elixir version 1.14.0.
- Since we'll be using Phoenix for the web app build, you'll also need to [have it installed](https://hexdocs.pm/phoenix/index.html) on your local development machine. We're using Phoenix version 1.7.2 in this tutorial.
- PostgreSQL - Ensure you have PostgreSQL installed, as it's the default database used by Elixir/Phoenix apps.

With that in place, let's see what kind of app we'll be building next.

## An Overview of the Phoenix App We'll Build

The app that we'll build is a simple multi-tenant URL shortening app with the following features:

- **User authentication** - users can register and log in to the application.
- **Multi-tenant separation** - users can create shortened links within their own separate accounts.
- **URL shortening** - users can input normal links and generate shortened versions that redirect back to the original link when clicked.
- **View metrics** - users can see how many times a link was clicked.

Implementing these features should add to your experience in working with Elixir and provide a good foundation to build even more complex Elixir apps.

Next, let's understand what multi-tenancy is and how it works.

## An Overview of Multi-tenancy

At the most basic level, a multi-tenant app is one where the same application and database serve several tenants, with each client's data kept separate from that of other clients. There are thousands of real-world multi-tenant app examples, including AppSignal, Gmail, and others.

In our app, a user will be able to register and create shortened links that are associated with their account. Other users will also be able to sign up and create shortened links associated to them, but users won't be able to see other users' links. This is a simplified but very good example of multi-tenancy in action.

In the next section, we'll outline the different strategies a developer can use when building a multi-tenant app. It's important to point out that these strategies aren't exclusive to Elixir. Rather, think of them as universal building blocks for developing multi-tenant apps regardless of the programming language used.

## Multi-tenancy Strategies

Several multi-tenancy strategies are available, including:

- **Separate databases** - Here, each tenant gets their own database for complete isolation of each client's data. The beauty of this approach is that it's very scalable and great for applications where data security and isolation is key: for example, patient medical records, financial records, and other similar use cases. As you can imagine, one of the biggest disadvantages with this approach is how complex and costly it is to build and maintain apps using this strategy.
- **A shared database with separate schema** - In this strategy, each client gets a separate schema within a shared database. This approach allows for a scalable system that is not too complex to build and maintain. That said, having separate schemas for each client is not something you can easily use to handle massive scale.
- **A shared database with shared schema** - Here, all tenants share a common database and schema. It is a great choice for low-to-medium traffic apps and offers a convenient way to get started with multi-tenancy. Many SaaS startups are often built using this strategy. The biggest disadvantage of this strategy is that it's not built for scale or speed.
- **Hybrid strategy** - A not-so-common approach where you have both the shared database and shared schema for some groups of users (say, the ones that pay the least in a SaaS app), and a common database with a separate schema for premium customers. This approach offers some level of scaling but is very complex to build and maintain.
- **Containerization** - Similar to the separate databases approach, here, each tenant is provided with a completely separate and isolated app container. The obvious advantages are speed and scalability, but this is complex to build and maintain.

You now have a good overview of what strategy to use when you build your own Elixir app next time.

For this app project, we'll be using the _shared database with shared schema_ approach and an app stack described in the next section.

## Our Elixir Application Stack

To build our app project, we will use the up-and-coming Elixir stack called "PETAL", short for:

- P - Phoenix
- E - Elixir
- T - Tailwind CSS
- A - Alpine JS
- L - [Liveview](https://hexdocs.pm/phoenix_live_view/Phoenix.LiveView.html) - LiveView is an Elixir library that you include as a dependency in a Phoenix app to handle interactivity and other real-time flows characteristic of single-page applications (SPAs).

Since our goals are simple, employing the full PETAL framework in this case is overkill. Instead, we'll use the simple stack you get when you generate a new Phoenix web application:

- Phoenix
- PostgreSQL
- HTML and Tailwind CSS

We are now ready to jump into the build, but before we do, it's necessary that we understand how link shortening actually works. This will form the basis for the steps we'll take during the build.

## How Link Shortening Works

Link shortening is actually very simple. The link shortening app lies between a long URL and a generated short URL. When a visitor hits a short URL, their request is received by the app and a quick database search is done to find the matching long URL. Once that long URL is found, the visitor is redirected to it, and the link visits counter updates.

Obviously, this is a very simplified outline, but it will suffice for now. Next, let's start off our build.

## Generating a New Phoenix Application

Begin by generating a new Phoenix application using the following `mix` command:

```shell
mix phx.new urlbot
```

Then open your project directory in a text editor and edit the `dev.exs` file with your development environment's database settings:

```elixir
# config/dev.exs

import Config

config :shorten_my_links_app, ShortenMyLinksApp.Repo,
  username: "DATABASE USERNAME", # Edit this line
  password: "DATABASE PASSWORD", # Edit this line
  hostname: "localhost",
  database: "DATABASE NAME", # Edit this line
  stacktrace: true,
  show_sensitive_data_on_connection_error: true,
  pool_size: 10

  # ...
```

Create the database for the application:

```shell
mix ecto.create
```

Once that is done, run `mix phx.server` in the terminal to compile the application and spin up an instance of the compiled app on `localhost:4000`, where you can see the default Phoenix home page:

![Default Phoenix app homepage](https://blog.appsignal.com/images/blog/2023-11/default-phoenix-app-homepage.png)

Next up, let's set up the multi-tenant structure and user authentication.

## Setting Up Multi-tenancy for Elixir

The tenant structure needs to be implemented as early as possible into a project build, since doing it later can result in all sorts of challenges. To put the _shared database with shared schema_ multi-tenant strategy that we chose into actuality, we'll make use of foreign keys and related models.

Specifically, we'll have two main models: `User` and `Account` (this can also be called `Organization`, `Team`, or even `Company`); a `User` belongs to the `Account` model.

We'll also have a third `Link` model which will belong to the `Account` model. This way, the `User` model can be used exclusively for authentication purposes, while resource ownership will be handled by `Account`.

This structure is represented in the diagram below:

![Multi-tenant account structure](https://blog.appsignal.com/images/blog/2023-11/multi-tenant-account-structure.png)

With this structure in place, all resources created, updated, or deleted in the app can be scoped to a specific `Account` and `User`. And just like that, we can achieve our goal of having separate tenant resources in a _shared database with shared schema_ setup.

An additional benefit to using this structure is that you can easily expand it to invite other users to an account as "teammates" and assign them different roles. However, we won't cover this feature in this tutorial.

Let's begin by generating the [`Account` context](https://hexdocs.pm/phoenix/contexts.html).

## Generating an `Account` Context

Generate the `Account` context, schema, and migration file with the command below:

```shell
mix phx.gen.context Accounts Account accounts name:string
```

Then run the migration with:

```shell
mix ecto.migrate
```

Next, let's move on to the `User`.

## Building a `User` Context and Authentication with Elixir

The `User` context will be used for authentication purposes. Instead of generating the `User` context and trying to integrate it into an authentication flow from scratch, Elixir gives us several libraries we can use, including [`phx.gen.auth`](https://hexdocs.pm/phx_gen_auth/overview.html), [Coherence](https://github.com/smpallen99/coherence), and [Pow](https://github.com/pow-auth/pow), a modular and extendable authentication library for Phoenix apps.

We first add Pow to the project's dependencies:

```elixir
# mix.exs

defp deps do
  [
    # ...
    {:pow, "~> 1.0.30"}
  ]
end
```

And fetch it:

```shell
mix deps.get
```

Then finish by installing it:

```shell
mix pow.install
```

With that, we get a `User` context, schema, and migration file. User authentication routes are also appended to the router.

At this point, we could run the migration, but there are a couple of changes we should make to our generated user files first to ensure they are properly related.

## Adding the `User` and `Account` Relationship

Begin by adding a `belongs_to` association to `User`:

```elixir
# lib/shorten_my_links_app/users/user.ex

defmodule Urlbot.Users.User do
  use Ecto.Schema
  use Pow.Ecto.Schema

  schema "users" do
    pow_user_fields()

    belongs_to :account, Urlbot.Accounts.Account # Add this line

    timestamps()
  end
end
```

Let's also modify `Account` to include the `has_many` relationship:

```elixir
# lib/urlbot/accounts/account.ex

defmodule Urlbot.Accounts.Account do
  use Ecto.Schema
  import Ecto.Changeset

  schema "accounts" do
    field :name, :string

    has_many :users, Urlbot.Users.User # Add this line

    timestamps()
  end

  ...
end
```

Next, let's modify the `users` migration to add the `account_id` field as a foreign key. We also need to indicate that a user will be deleted whenever their related account is deleted:

```elixir
# priv/repo/migrations/XXXXXXXXXX_create_users.exs

defmodule Urlbot.Repo.Migrations.CreateUsers do
  use Ecto.Migration

  def change do
    create table(:users) do
      add :email, :string, null: false
      add :password_hash, :string

      # Add this line
      add :account_id, references(:accounts, on_delete: :delete_all), null: false

      timestamps()
    end

    create index(:users, [:account_id]) # Add this line
    create unique_index(:users, [:email])
  end
end
```

Finally, it's nice to have an `Account` automatically created the first time a user registers on the app.

## Creating an `Account` on User Registration

We need a way to capture the one attribute of the `Acccount` model (the `account_name` in the user registration form) and pass this attribute to a modified user creation process which will create a related `Account` for us.

That sounds like a lot, but let's go step-by-step.

First, add the `account_name` attribute to the user registration form. Since we are working with Pow which comes with pre-built view templates, we need to generate the templates by running the command:

```shell
mix pow.phoenix.gen.templates
```

This will generate Pow's view templates, but we are only interested in the user registration view for now. Edit it by adding the `account_name` field:

```elixir
# lib/urlbot_web/controllers/pow/registration_html/new.html.heex

<div class="mx-auto max-w-sm">
  ...

  <.simple_form :let={f} for={@changeset} as={:user} action={@action} phx-update="ignore">
    ...

    <!-- Add this line -->
    <.input field={f[:account_name]} type="text" label="Account name" />

    ...
  </.simple_form>
</div>
```

By the way, instead of adding the `account_name` like this, we can use a nested form for working with `Account` from `User` (but this method should work just as well).

Next, we want to add `account_name` as a virtual attribute in the `User` schema. The reason it's a virtual attribute is simply because it's not an attribute that is built into the `User` schema, but we still need to use it in there. You can [read more on virtual attributes](https://hexdocs.pm/ecto/Ecto.Schema.html).

```elixir
# lib/urlbot/users/user.ex

defmodule Urlbot.Users.User do
  use Ecto.Schema
  use Pow.Ecto.Schema

  schema "users" do
    pow_user_fields()

    # Add this line
    field :account_name, :string, virtual: true

    belongs_to :account, Urlbot.Accounts.Account

    timestamps()
  end
end
```

At this point, you can run `mix ecto.migrate` to create the user table.

Next, we want to make sure that the virtual attribute we've just added is passed on to the `User` [changeset](https://hexdocs.pm/ecto/Ecto.Changeset.html), which we'll add since it's not included by default when we run the Pow generator.

We'll also add a custom private function to create an account at the same time a user is created:

```elixir
# lib/shorten_my_links_app/users/user.ex

defmodule Urlbot.Users.User do
  use Ecto.Schema
  use Pow.Ecto.Schema

  # Add this line since we'll be using Ecto's changeset in this schema
  import Ecto.Changeset

  # Add this line to reference Account since it's used in the private function
  alias Urlbot.Accounts

  ...

  # Also add this block of code to add a changeset
  def changeset(user, attrs) do
    user
    |> pow_changeset(attrs)
    |> cast(attrs, [:account_name])
    |> validate_required([:account_name])
    |> create_user_account(user)
    |> assoc_constraint(:account)
  end

  # Add the custom private function
  defp create_user_account(%{valid?: true,  changes: %{account_name: account_name}} = changeset, %{account_id: nil} = _user) do
    with {:ok, account} <- Accounts.create_account(%{name: account_name}) do
      put_assoc(changeset, :account, account)
    else
      _ -> changeset
    end
  end

  defp create_user_account(changeset, _), do: changeset

end
```

Let's break down what's going on. First, we define a changeset block passing in a `user` and the user attributes. Then we cast the virtual attribute we added, followed by a validation rule to make sure it's present. We then call the private function `create_user_account` (defined just below the changeset block) and finalize with an `assoc_constraint` which checks that the associated field, `account_id`, exists.

With all that done, whenever a new user registers, a connected account will be automatically created. Any link resources created by the logged-in user will always be scoped to them. For example, an index view under _/links_ will only display links created by the user, and not list other users' links, even if they are available. This resource separation at the account or user level is the foundation of a multi-tenant structure.

## Up Next: Adding `Link` Resources and Custom Ecto Types

We've explored how multi-tenancy works and discussed a few multi-tenancy strategies. We created a Phoenix app and set up multi-tenancy. Finally, we added accounts, users and authentication, and associated users and accounts.

In the next and final part of this series, we'll look at:

- Generating the link resource
- Using Ecto custom types
- Generating a `current_account`
- Assigning a `current_account` to links
- Redirecting links and updating the `views` counter

Until then, happy coding!

**P.S. If you'd like to read Elixir Alchemy posts as soon as they get off the press, [subscribe to our Elixir Alchemy newsletter and never miss a single post](/elixir-alchemy)!**