Multitenancy in Ash Framework

Understanding Multitenancy in Ash Framework: A Practical Guide

Nicholas Hammond portrait

Nicholas Hammond

4 March 2025 · 3 min read

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.

Nicholas Hammond portrait

WRITTEN BY

Nicholas Hammond

Hi, I’m Nicholas! I'm an analytically and creatively minded software developer who is always learning new technology. I've worked with TypeScript, React, GraphQL, Elixir, Ruby on Rails, HTML, and CSS. I have a deep love of pure mathematics and digital animation.

Want to read more?

The latest news, articles, and resources, sent to your inbox occasionally.