What is Multitenancy?
When building multi-customer applications, keeping each customer's data separate and secure is crucial. At its core, multitenancy is about partitioning the data in your database in such a way that one can be confident that data for one a customer won't somehow leak into the data for another customer.
If you are writing in Elixir you have possibly implemented multitenancy, using Ecto. Using Ecto you can use query prefixes or foreign keys to achieve this effect.
Ash has it’s own explicit functionality for elegantly handling multitenancy. In this post, we'll explore how Ash implements multitenancy and when to use different approaches.
Attribute-Based Multitenancy
Ash provides two primary strategies for implementing multitenancy: attribute-based and context-based. In the latter case Ash uses a "schema based multitenancy", which under the hood uses Postgres schemas as the mechanism for separating data.
The schema based approach is great for making the separation of data strict. It is the more involved of the two strategies.
The attribute strategy is the simpler of the two approaches and requires minimal setup. In this post we will be focusing on this simpler approach. If you want to learn more about the context-based approach, take a look at Schema Based Multitenancy.
To implement multi-tenancy we start by adding a multitenancy block to our User resource.
defmodule MyApp.User do
use Ash.Resource, ...
multitenancy do
strategy :attribute
attribute :organization_id
end
relationships do
belongs_to :organization, MyApp.Organization
end
end
With this setup, you must specify a tenant for every operation. We use Ash.Query.set_tenant/2
for queries and Ash.Changeset.set_tenant/2
for Changesets.
# Reading data for a specific tenant
MyApp.User
|> Ash.Query.filter(name == "Alice")
|> Ash.Query.set_tenant(1)
|> Ash.read!()
# Creating data for a specific tenant
MyApp.User
|> Ash.Changeset.for_create(:create, %{name: "Alice"})
|> Ash.Changeset.set_tenant(1)
|> Ash.create!()
It is also worth mentioning that this is completely compatible with Ash's code interface.
Ash.create!(
MyApp.User,
%{name: "Alice"},
tenant: 1
)
Advanced Features
Global Access Option
Sometimes you need to allow queries without a tenant specification. You can enable this with the global?
option:
multitenancy do
strategy :attribute
attribute :organization_id
global? true
end
When doing this you will most likely need to add authorisation rules to your policies to handle the actions that should respect tenancy. You might create a module ActorBelongsToTenant
which checks the if actor belongs to the correct organisation, forbidding access accordingly. If you want to apply this to specific actions and add it to your policies block:
policies do
policy action([:create, :read, :update, :destroy]) do
forbid_unless ActorBelongsToTenant
end
end
Tenant-Aware Identities
Identities are a way to declare that an instance of a resource must be unique subject to some specific set of other attributes.
For example:
defmodule MyApp.MyResource do
use Ash.Resource #, ...
# ...
identities do
# If the `email` attribute must be unique across all records
identity :unique_email, [:email]
# If the `username` attribute must be unique for every record with a given `site` value
identity :special_usernames, [:username, :site]
end
end
A useful feature of Ash's multitenancy is how it handles identities. By default, identities are scoped to tenants, meaning the same identity value can exist across different tenants but must be unique within a single tenant.
defmodule MyApp.User do
use Ash.Resource, ...
multitenancy do
strategy :attribute
attribute :organization_id
end
identities do
# Unique within each tenant
identity :tenant_scoped_email, [:email]
# Unique across all tenants
identity :global_username, [:username], all_tenants?: true
end
end
This means you can have users with the same email in different organisations, but if you need something like a globally unique username, you can specify that too.
Conclusion
Ash's multitenancy support provides a flexible and powerful way to handle multi-customer applications. Whether you choose the simpler attribute-based approach outlined here or you wish to explore the more powerful context-based strategy, you get built-in support for data isolation, identity management, and clean tenant handling.
The framework's thoughtful design means you can start simple with attribute-based multitenancy and graduate to more sophisticated approaches as your needs evolve, all while maintaining a consistent API for your application code.