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:
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. 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 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:
Check out the Ecto custom types documentation, 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:
Before moving on, let's briefly explain the use of @derive
in the code above.
In Elixir, a protocol defines an API and its specific implementations. Phoenix.Param
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:
Then run the migration to create the links table:
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:
Then add the scope to handle Link
routes:
Now, if you try to access any of the protected routes, Pow should redirect you to a login page:
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
:
Next, edit the changeset block, adding account_id
to the list of allowed attributes and those that will go through validation:
Then, edit the Account
schema and add a has_many
relation as follows:
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:
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:
Next, let's use this new plug by adding it to the router (specifically to the protected
pipeline since user sessions are handled there):
Then, edit the Link
controller to pass the current_account
in every action where it's required, like so:
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:
Here, a user should only see links that are associated with them.
For example, this is a list index view for one user:
And this is a view for a different user (notice the logged-in user's email and the different link URLs):
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:
We also need to modify the Link
context with a custom get
function that does not reference the current user:
Next, modify the router accordingly:
Finally, to handle the link view visits, we'll add a custom function to the Link
context:
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!