Advanced Multi-tenancy for Elixir Applications Using Ecto
Tags: elixir
Published: 2023-12-19 11:16:25

Welcome to part two of this series. In the previous tutorial, we learned about multi-tenancy, including different multi-tenancy implementation strategies. We also started building a multi-tenant Phoenix link shortening app and added basic user authentication.

In this final part of the series, we'll build the link resource, associate users to links, and set up the redirect functionality in our app. As we finalize the app build, we'll learn about features (like Ecto custom types) that make Phoenix such a powerful framework for Elixir.

Let's now turn our attention to link shortening.

## Adding `Link` Resources

As a reminder, here's how link shortening will happen in our app:

- Logged-in user enters a long URL
- A random string is generated and associated with the long URL
- Whenever this short URL is visited, the app will keep a tally of the number of visits

We'll get started by generating a controller, schema, migration, and views for a `Link` resource:

```shell
mix phx.gen.html Links Link links url:string visits:integer account_id:references:accounts
```

This should generate most of the boilerplate code we need to work with the `Link` resource.

You may notice that we haven't included a field for the short random string referencing the long URL. We'll cover this next.

## Creating Short URLs Using the Ecto Custom Type in Phoenix

By default, Ecto models have an `id` field used as a model's primary key. Usually, it's in the form of an integer or, in some cases, a [binary ID](https://hexdocs.pm/ecto/0.13.0/Ecto.Schema.html). In either case, when present in a model, this field is automatically incremented for every additional model created in an app.

A core function of our app is generating short, unique strings to represent long URLs. We could write a custom function to generate such strings, but since we are on a quest to learn Elixir, let's use a more creative approach.

Let's substitute the `id` primary key in the `Link` model with a custom [Ecto type](https://elixir-lang.org/getting-started/typespecs-and-behaviours.html) called `HashId`.

Using this approach, we'll:

- Learn how to create and use Elixir custom types.
- Automatically generate unique string hashes to form the short URL field for links. Ecto will manage the process whenever a new link model is created.

First, create a new file to represent this new type:

```elixir
# lib/ecto/hash_id.ex

defmodule Urlbot.Ecto.HashId do
  @behaviour Ecto.Type
  @hash_id_length 8

  # Called when creating an Ecto.Changeset
  @spec cast(any) :: Map.t
  def cast(value), do: hash_id_format(value)

  # Accepts a value that has been directly placed into the ecto struct after a changeset
  @spec dump(any) :: Map.t
  def dump(value), do: hash_id_format(value)

  # Changes a value from the default type into the HashId type
  @spec load(any) :: Map.t
  def load(value), do: hash_id_format(value)

  # A callback invoked by autogenerate fields
  @spec autogenerate() :: String.t
  def autogenerate, do: generate()

 # The Ecto type that is being converted
  def type, do: :string

  @spec hash_id_format(any) :: Map.t
  def hash_id_format(value) do
    case validate_hash_id(value) do
      true -> {:ok, value}
      _ -> {:error, "'#{value}' is not a string"}
    end
  end

  # Validation of given value to be of type "String"
  def validate_hash_id(string) when is_binary(string), do: true
  def validate_hash_id(_other), do: false

  # The function that generates a HashId
  @spec generate() :: String.t
  def generate do
    @hash_id_length
    |> :crypto.strong_rand_bytes()
    |> Base.url_encode64
    |> binary_part(0, @hash_id_length)
  end
end
```

Check out the [Ecto custom types documentation](https://elixir-lang.org/getting-started/typespecs-and-behaviours.html), which covers the subject in a much more exhaustive way than we could in this tutorial.

For now, what we've just done allows us to use the custom data type `HashId` when defining a field's data type.

Let's use this new data type to define the short URL field for the `Link` resource next.

## Using the Custom Ecto Type in Phoenix

Open up the `Link` schema and edit it to reference the new Ecto type we've just defined:

```elixir
# lib/urlbot/links/link.ex

defmodule Urlbot.Links.Link do
  use Ecto.Schema
  import Ecto.Changeset
  alias Urlbot.Ecto.HashId # Add this line

  @primary_key {:hash, HashId, [autogenerate: true]} # Add this line
  @derive {Phoenix.Param, key: :hash} # Add this line

  ...
end
```

Before moving on, let's briefly explain the use of `@derive` in the code above.

In Elixir, a [protocol](https://hexdocs.pm/elixir/1.14/Protocol.html) defines an API and its specific implementations. [`Phoenix.Param`](https://hexdocs.pm/phoenix/Phoenix.Param.html) is a protocol used to convert data structures into URL parameters. By default, this protocol is used for integers, binaries, atoms, and structs. The default key `:id` is usually used for structs, but it's possible to define other parameters. In our case, we use the parameter `hash` and make its implementation derivable to actually use it.

Let's modify the links migration to use this new parameter type:

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

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

  def change do
    create table(:links, primary_key: false) do
      add :hash, :string, primary_key: true
      add :url, :string
      add :visits, :integer
      add :account_id, references(:accounts, on_delete: :delete_all)

      timestamps()
    end

    create index(:links, [:account_id])
  end
end
```

Then run the migration to create the links table:

```shell
mix ecto.migrate
```

We now need to associate `Link` and `Account`— but before we do, let's ensure that only an authenticated user can make CRUD operations for `Link`.

## Updating `Link` Routes in Phoenix for Elixir

In the router, we need to create a new pipeline to process routes that only authenticated users can access.

Go ahead and add this pipeline:

```elixir
# lib/urlbot_web/router.ex

defmodule UrlbotWeb.Router do
  use UrlbotWeb, :router
  use Pow.Phoenix.Router

  ...

  pipeline :protected do
    plug Pow.Plug.RequireAuthenticated, error_handler: Pow.Phoenix.PlugErrorHandler
  end

  ...
end
```

Then add the scope to handle `Link` routes:

```elixir
# lib/urlbot_web/router.ex

defmodule UrlbotWeb.Router do
  use UrlbotWeb, :router
  use Pow.Phoenix.Router

  ...

  pipeline :protected do
    plug Pow.Plug.RequireAuthenticated, error_handler: Pow.Phoenix.PlugErrorHandler
  end

  scope "/", UrlbotWeb do
    pipe_through [:protected, :browser]

    resources "/links", LinkController
  end

  ...
end
```

Now, if you try to access any of the protected routes, Pow should redirect you to a login page:

![Trying to access a protected route](https://blog.appsignal.com/images/blog/2023-12/link-routes-protected.png)

Our app build is progressing well. Next, let's associate `Link` and `Account` since this forms the basis of tenant separation in our app.

## Assigning `Link` to `Account` in Elixir

Take a look at the `Link` schema below. We want to make sure `account_id` is included, since this will form the link to `Account`:

```elixir
# lib/urlbot/links/link.ex

defmodule Urlbot.Links.Link do
  use Ecto.Schema
  import Ecto.Changeset
  alias Urlbot.Ecto.HashId

  ...

  schema "links" do
    field :url, :string
    field :visits, :integer
    field :account_id, :id

    timestamps()
  end
  ...

end
```

Next, edit the changeset block, adding `account_id` to the list of allowed attributes and those that will go through validation:

```elixir
# lib/urlbot/links/link.ex

defmodule Urlbot.Links.Link do
  use Ecto.Schema
  import Ecto.Changeset
  alias Urlbot.Ecto.HashId

  ...

  def changeset(link, attrs) do
    link
    |> cast(attrs, [:url, :visits, :account_id])
    |> validate_required([:url, :visits, :account_id])
  end

end
```

Then, edit the `Account` schema and add a `has_many` relation as follows:

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

defmodule Urlbot.Accounts.Account do
  use Ecto.Schema
  import Ecto.Changeset
  alias Urlbot.Users.User
  alias Urlbot.Links.Link

  schema "accounts" do
    field :name, :string

    has_many :users, User
    has_many :links, Link # Add this line

    timestamps()
  end

  ...

end
```

We also need to edit the `Link` context to work with `Account`. Remember, scoping a resource to a user or account is the foundation of any multi-tenant structure.

Edit your file as shown below:

```elixir
# lib/urlbot/links.ex

defmodule Urlbot.Links do
  ...

  def list_links(account) do
    from(s in Link, where: s.account_id == ^account.id, order_by: [asc: :id])
    |> Repo.all()
  end

  def get_link!(account, id) do
    Repo.get_by!(Link, account_id: account.id, id: id)
  end

  def create_link(account, attrs \\ %{}) do
    Ecto.build_assoc(account, :links)
    |> Link.changeset(attrs)
    |> Repo.insert()
  end

  ...

end
```

## Adding a `Current_Account`

Our edits to the `Link` context show that most related controller methods use the `account` variable. This variable needs to be extracted from the currently logged-in user's `account_id` and passed on to the respective controller action.

So we need to make a custom plug to add to the `conn`. Create a new file — `lib/plugs/set_current_account.ex` — and edit its content as shown below:

```elixir
# lib/plugs/set_current_account.ex

defmodule UrlbotWeb.Plugs.SetCurrentAccount do
  import Plug.Conn
  alias Urlbot.Repo
  alias Urlbot.Users.User

  def init(options), do: options

  def call(conn, _opts) do
    case conn.assigns[:current_user] do
      %User{} = user ->
        %User{account: account} = Repo.preload(user, :account)
        assign(conn, :current_account, account)
      _ ->
        assign(conn, :current_account, nil)
    end
  end
end
```

Next, let's use this new plug by adding it to the router (specifically to the `protected` pipeline since user sessions are handled there):

```elixir
# lib/urlbot_web/router.ex

defmodule UrlbotWeb.Router do
  use UrlbotWeb, :router
  use Pow.Phoenix.Router

  ...

  pipeline :protected do
    plug Pow.Plug.RequireAuthenticated, error_handler: Pow.Phoenix.PlugErrorHandler
    plug UrlbotWeb.Plugs.SetCurrentAccount # Add this line
  end

  ...

  scope "/", UrlbotWeb do
    pipe_through [:protected, :browser]

    resources "/links", LinkController
  end

  ...

end
```

Then, edit the `Link` controller to pass the `current_account` in every action where it's required, like so:

```elixir
# lib/urlbot_web/controllers/link_controller.ex

defmodule UrlbotWeb.LinkController do
  use UrlbotWeb, :controller

  alias Urlbot.Links
  alias Urlbot.Links.Link

  def index(conn, _params) do
    current_account = conn.assigns.current_account  # Add this line
    links = Links.list_links(current_account)
    render(conn, :index, links: links)
  end
  def create(conn, %{"link" => link_params}) do
    current_account = conn.assigns.current_account # Add this line
    case Links.create_link(current_account, link_params) do
      {:ok, link} ->
        conn
        |> put_flash(:info, "Link created successfully.")
        |> redirect(to: ~p"/links")

      {:error, %Ecto.Changeset{} = changeset} ->
        render(conn, :new, changeset: changeset)
    end
  end

  def edit(conn, %{"id" => id}) do
    current_account = conn.assigns.current_account  # Add this line
    link = Links.get_link!(current_account, id)
    changeset = Links.change_link(link)
    render(conn, :edit, link: link, changeset: changeset)
  end

  def update(conn, %{"id" => id, "link" => link_params}) do
    current_account = conn.assigns.current_account  # Add this line
    link = Links.get_link!(current_account, id)

    case Links.update_link(link, link_params) do
      {:ok, link} ->
        conn
        |> put_flash(:info, "Link updated successfully.")
        |> redirect(to: ~p"/links/#{link}")

      {:error, %Ecto.Changeset{} = changeset} ->
        render(conn, :edit, link: link, changeset: changeset)
    end
  end

  def delete(conn, %{"id" => id}) do
    current_account = conn.assigns.current_account  # Add this line
    link = Links.get_link!(current_account, id)
    {:ok, _link} = Links.delete_link(link)

    conn
    |> put_flash(:info, "Link deleted successfully.")
    |> redirect(to: ~p"/links")
  end

end
```

That's it! Now, any created `Link` will be associated with a currently logged-in user's account.

At this point, we have everything we need for users to create links that are properly scoped to their respective accounts. You can see this in action when you consider the list index action:

```elixir

defmodule UrlbotWeb.LinkController do
  use UrlbotWeb, :controller

  alias Urlbot.Links
  alias Urlbot.Links.Link

  def index(conn, _params) do
    current_account = conn.assigns.current_account
    links = Links.list_links(current_account)
    render(conn, :index, links: links)
  end

  ...
end
```

Here, a user should only see links that are associated with them.

For example, this is a list index view for one user:

![First user's link index view](https://blog.appsignal.com/images/blog/2023-12/user1s-links-index.png)

And this is a view for a different user (notice the logged-in user's email and the different link URLs):

![Second user's link index view](https://blog.appsignal.com/images/blog/2023-12/user2s-links-index.png)

Next, let's build the link redirection feature.

## Redirecting Links and Incrementing Link Views in Phoenix

Let's begin by adding a new controller to handle the redirect. This controller will have one `show` action:

```elixir
# lib/urlbot_web/controllers/redirect_controller.ex

defmodule UrlbotWeb.RedirectController do
  use UrlbotWeb, :controller

  alias Urlbot.Links

  def show(conn, %{"id" => id}) do
    short_url = Links.get_short_url_link!(id)
    Links.increment_visits(short_url)
    redirect(conn, external: short_url.url)
  end

end
```

We also need to modify the `Link` context with a custom `get` function that does not reference the current user:

```elixir
# lib/urlbot/links.ex

defmodule Urlbot.Links do
  ...

  def get_short_url_link!(id) do
    Repo.get!(Link, id: id)
  end

  ...

end
```

Next, modify the router accordingly:

```elixir
# lib/urlbot_web/router.ex

defmodule UrlbotWeb.Router do
  use UrlbotWeb, :router
  use Pow.Phoenix.Router

  ...

  scope "/", UrlbotWeb do
    pipe_through :browser

    get "/", PageController, :home
    get "/links/:id", RedirectController, :show # Add this line
  end

  ...

end
```

Finally, to handle the link view visits, we'll add a custom function to the `Link` context:

```elixir
# lib/urlbot/links.ex

defmodule Urlbot.Links do
  ...

  def increment_visits(%Link{} = link) do
    from(s in Link, where: s.id == ^link.hash, update: [inc: [visits: 1]])
    |> Repo.update_all([])
  end

end
```

That's it! Now, when we visit a link like `http://localhost:4000/links/rMbzTz3o`, this should redirect to the original long URL and increment the link's view counter.

## Wrapping Up

This two-part series has taken you on a journey to build a multi-tenant Elixir app using the Phoenix web framework. You've learned how to implement a simple tenant separation system using foreign keys and related models within a shared database and shared schema.

Although we've tried to be as exhaustive as possible, we couldn't possibly capture the whole app-building process, including testing, deployment options, or even the use of the amazing LiveView.

All the same, we hope this tutorial provides a foundation for you to build your very own Elixir app that helps solve serious problems for your users.

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)!**