Making a resource multi-tenancy aware after forgetting to add the multi-tenancy block

Hello everyone am following a tutorial blog on medium titled Part 13 — Ash Framework for Phoenix Developers | Multitenancy - SAAS

now, while following this instruction

Finally, we need to update all other resources (except users, teams, and user_teams) to inform Ash that they are multi-tenant resources. Add the following block to each resource:

# Make this resource multi-tenant
multitenancy do
  strategy :context
end

Unfortunately i forgot to add that code to one of the resources(token)
i went ahead and did the following instructions
Let’s generate migrations for our multi-tenant schema by running:

mix ash_postgres.generate_migrations add_multitenancy_tables

Then, run the migrations with: mix ash_postgres.migrate

If you open an IEx session with iex -S mix and run:

Ash.create!(Helpcenter.Accounts.Team, %{name: "Zippiker", domain: "zippiker"})

During inspection of my database it’s when i discovered that a table for token was not created. this has affected me greatly in the next tutorials as i ran into such errors

 1) test User tests: User creation - creates personal team automatically (Zippiker.Accounts.UserTest)
     test/zippiker/accounts/user_test.exs:8
     ** (Ash.Error.Unknown) 
     Bread Crumbs:
       > Exception raised in: Zippiker.Accounts.User.register_with_password

     Unknown Error

     * ** (MatchError) no match of right hand side value: :error
       (ash_authentication 4.5.2) lib/ash_authentication/generate_token_change.ex:46: AshAuthentication.GenerateTokenChange.generate_token/4
       (ash_authentication 4.5.2) lib/ash_authentication/generate_token_change.ex:19: anonymous fn/4 in AshAuthentication.GenerateTokenChange.change/3
       (ash 3.4.63) lib/ash/changeset/changeset.ex:4066: anonymous fn/2 in Ash.Changeset.run_after_actions/3
       (elixir 1.18.1) lib/enum.ex:4964: Enumerable.List.reduce/3
       (elixir 1.18.1) lib/enum.ex:2600: Enum.reduce_while/3
       (ash 3.4.63) lib/ash/changeset/changeset.ex:3544: anonymous fn/3 in Ash.Changeset.with_hooks/3
       (ecto_sql 3.12.1) lib/ecto/adapters/sql.ex:1400: anonymous fn/3 in Ecto.Adapters.SQL.checkout_or_transaction/4
       (db_connection 2.7.0) lib/db_connection.ex:1756: DBConnection.run_transaction/4
       (ash 3.4.63) lib/ash/changeset/changeset.ex:3542: anonymous fn/3 in Ash.Changeset.with_hooks/3
       (ash 3.4.63) lib/ash/changeset/changeset.ex:3686: anonymous fn/2 in Ash.Changeset.transaction_hooks/2
       (ash 3.4.63) lib/ash/changeset/changeset.ex:3523: Ash.Changeset.with_hooks/3
       (ash 3.4.63) lib/ash/actions/create/create.ex:260: Ash.Actions.Create.commit/3
       (ash 3.4.63) lib/ash/actions/create/create.ex:132: Ash.Actions.Create.do_run/4
       (ash 3.4.63) lib/ash/actions/create/create.ex:50: Ash.Actions.Create.run/4
       (ash 3.4.63) lib/ash.ex:2222: Ash.create!/3
       test/zippiker/accounts/user_test.exs:17: Zippiker.Accounts.UserTest."test User tests: User creation - creates personal team automatically"/1
       (ex_unit 1.18.1) lib/ex_unit/runner.ex:511: ExUnit.Runner.exec_test/2
       (stdlib 6.2) timer.erl:595: :timer.tc/2
       (ex_unit 1.18.1) lib/ex_unit/runner.ex:433: anonymous fn/6 in ExUnit.Runner.spawn_test_monitor/4
     code: Ash.create!(
     stacktrace:
       (ash_authentication 4.5.2) lib/ash_authentication/generate_token_change.ex:46: AshAuthentication.GenerateTokenChange.generate_token/4
       (ash_authentication 4.5.2) lib/ash_authentication/generate_token_change.ex:19: anonymous fn/4 in AshAuthentication.GenerateTokenChange.change/3
       (ash 3.4.63) lib/ash/changeset/changeset.ex:4066: anonymous fn/2 in Ash.Changeset.run_after_actions/3
       (elixir 1.18.1) lib/enum.ex:4964: Enumerable.List.reduce/3
       (elixir 1.18.1) lib/enum.ex:2600: Enum.reduce_while/3
       (ash 3.4.63) lib/ash/changeset/changeset.ex:3544: anonymous fn/3 in Ash.Changeset.with_hooks/3
       (ecto_sql 3.12.1) lib/ecto/adapters/sql.ex:1400: anonymous fn/3 in Ecto.Adapters.SQL.checkout_or_transaction/4
       (db_connection 2.7.0) lib/db_connection.ex:1756: DBConnection.run_transaction/4
       (ash 3.4.63) lib/ash/changeset/changeset.ex:3542: anonymous fn/3 in Ash.Changeset.with_hooks/3
       (ash 3.4.63) lib/ash/changeset/changeset.ex:3686: anonymous fn/2 in Ash.Changeset.transaction_hooks/2
       (ash 3.4.63) lib/ash/changeset/changeset.ex:3523: Ash.Changeset.with_hooks/3
       (ash 3.4.63) lib/ash/actions/create/create.ex:260: Ash.Actions.Create.commit/3
       (ash 3.4.63) lib/ash/actions/create/create.ex:132: Ash.Actions.Create.do_run/4
       (ash 3.4.63) lib/ash/actions/create/create.ex:50: Ash.Actions.Create.run/4
       (ash 3.4.63) lib/ash.ex:2222: Ash.create!/3
       test/zippiker/accounts/user_test.exs:17: (test)


Finished in 0.8 seconds (0.00s async, 0.8s sync)
1 test, 1 failure

which i believe originate from that mistaken.
i have laboured to find the solution, but all in vain, how can i solve this

Would you be able to share the project itself? It could be a bug in the migration generator.

sure am going to do that; however i am quite sure that not including the token.ex resource generated by the ash authentication is the cause for the failure of my test hence the first culprit of the causation to the bug;
take a look at this test code

# test/zippiker/accounts/user_test.exs
defmodule Zippiker.Accounts.UserTest do
  use ZippikerWeb.ConnCase, async: false

  require Ash.Query
  
  describe "User tests:" do

    test "User creation - creates personal team automatically" do
      # Create a new user
      user_params = %{
        email: "john.tester@example.com",
        password: "12345678",
        password_confirmation: "12345678"
      }

      user =
        Ash.create!(
          Zippiker.Accounts.User,
          user_params,
          action: :register_with_password,
          authorize?: false
        )

      # Confirm that the new user has a personal team created for them automatically
      refute Zippiker.Accounts.User
             |> Ash.Query.filter(id == ^user.id)
             |> Ash.Query.filter(email == ^user_params.email)
             |> Ash.Query.filter(is_nil(current_team))
             |> Ash.exists?(authorize?: false)
    end
  end
end

and this is the failure message

Running tests...
Compiling 2 files (.ex)
Generated zippiker app
Getting extensions in current project...
Running setup for AshPostgres.DataLayer...
Running ExUnit with seed: 887465, max_cases: 16

21:50:20.785 [error] Failed to generate token for user: %Ash.Error.Invalid{
  bread_crumbs: ["Error returned from: Zippiker.Accounts.Token.store_token"], 
  changeset: "#Changeset<>", 
  errors: [
    %Ash.Error.Invalid.TenantRequired{
      resource: Zippiker.Accounts.Token,
      splode: Ash.Error,
      bread_crumbs: ["Error returned from: Zippiker.Accounts.Token.store_token"],
      vars: [],
      path: [],
      stacktrace: #Splode.Stacktrace<>,
      class: :invalid
    }
  ]
}


  1) test User tests: User creation - creates personal team automatically (Zippiker.Accounts.UserTest)
     test/zippiker/accounts/user_test.exs:8
     ** (Ash.Error.Unknown) 
     Bread Crumbs:
       > Exception raised in: Zippiker.Accounts.User.register_with_password

     Unknown Error

     * ** (MatchError) no match of right hand side value: :error
       (ash_authentication 4.5.2) lib/ash_authentication/generate_token_change.ex:46: AshAuthentication.GenerateTokenChange.generate_token/4
       (ash_authentication 4.5.2) lib/ash_authentication/generate_token_change.ex:19: anonymous fn/4 in AshAuthentication.GenerateTokenChange.change/3
       (ash 3.4.63) lib/ash/changeset/changeset.ex:4066: anonymous fn/2 in Ash.Changeset.run_after_actions/3
       (elixir 1.18.1) lib/enum.ex:4964: Enumerable.List.reduce/3
       (elixir 1.18.1) lib/enum.ex:2600: Enum.reduce_while/3
       (ash 3.4.63) lib/ash/changeset/changeset.ex:3544: anonymous fn/3 in Ash.Changeset.with_hooks/3
       (ecto_sql 3.12.1) lib/ecto/adapters/sql.ex:1400: anonymous fn/3 in Ecto.Adapters.SQL.checkout_or_transaction/4
       (db_connection 2.7.0) lib/db_connection.ex:1756: DBConnection.run_transaction/4
       (ash 3.4.63) lib/ash/changeset/changeset.ex:3542: anonymous fn/3 in Ash.Changeset.with_hooks/3
       (ash 3.4.63) lib/ash/changeset/changeset.ex:3686: anonymous fn/2 in Ash.Changeset.transaction_hooks/2
       (ash 3.4.63) lib/ash/changeset/changeset.ex:3523: Ash.Changeset.with_hooks/3
       (ash 3.4.63) lib/ash/actions/create/create.ex:260: Ash.Actions.Create.commit/3
       (ash 3.4.63) lib/ash/actions/create/create.ex:132: Ash.Actions.Create.do_run/4
       (ash 3.4.63) lib/ash/actions/create/create.ex:50: Ash.Actions.Create.run/4
       (ash 3.4.63) lib/ash.ex:2222: Ash.create!/3
       test/zippiker/accounts/user_test.exs:17: Zippiker.Accounts.UserTest."test User tests: User creation - creates personal team automatically"/1
       (ex_unit 1.18.1) lib/ex_unit/runner.ex:511: ExUnit.Runner.exec_test/2
       (stdlib 6.2) timer.erl:595: :timer.tc/2
       (ex_unit 1.18.1) lib/ex_unit/runner.ex:433: anonymous fn/6 in ExUnit.Runner.spawn_test_monitor/4
     code: Ash.create!(
     stacktrace:
       (ash_authentication 4.5.2) lib/ash_authentication/generate_token_change.ex:46: AshAuthentication.GenerateTokenChange.generate_token/4
       (ash_authentication 4.5.2) lib/ash_authentication/generate_token_change.ex:19: anonymous fn/4 in AshAuthentication.GenerateTokenChange.change/3
       (ash 3.4.63) lib/ash/changeset/changeset.ex:4066: anonymous fn/2 in Ash.Changeset.run_after_actions/3
       (elixir 1.18.1) lib/enum.ex:4964: Enumerable.List.reduce/3
       (elixir 1.18.1) lib/enum.ex:2600: Enum.reduce_while/3
       (ash 3.4.63) lib/ash/changeset/changeset.ex:3544: anonymous fn/3 in Ash.Changeset.with_hooks/3
       (ecto_sql 3.12.1) lib/ecto/adapters/sql.ex:1400: anonymous fn/3 in Ecto.Adapters.SQL.checkout_or_transaction/4
       (db_connection 2.7.0) lib/db_connection.ex:1756: DBConnection.run_transaction/4
       (ash 3.4.63) lib/ash/changeset/changeset.ex:3542: anonymous fn/3 in Ash.Changeset.with_hooks/3
       (ash 3.4.63) lib/ash/changeset/changeset.ex:3686: anonymous fn/2 in Ash.Changeset.transaction_hooks/2
       (ash 3.4.63) lib/ash/changeset/changeset.ex:3523: Ash.Changeset.with_hooks/3
       (ash 3.4.63) lib/ash/actions/create/create.ex:260: Ash.Actions.Create.commit/3
       (ash 3.4.63) lib/ash/actions/create/create.ex:132: Ash.Actions.Create.do_run/4
       (ash 3.4.63) lib/ash/actions/create/create.ex:50: Ash.Actions.Create.run/4
       (ash 3.4.63) lib/ash.ex:2222: Ash.create!/3
       test/zippiker/accounts/user_test.exs:17: (test)


Finished in 0.8 seconds (0.00s async, 0.8s sync)
1 test, 1 failure

when you look closely at this part, it rings a bell

21:47:09.624 [error] Failed to generate token for user: %Ash.Error.Invalid{
  bread_crumbs: ["Error returned from: Zippiker.Accounts.Token.store_token"], 
  changeset: "#Changeset<>", 
  errors: [
    %Ash.Error.Invalid.TenantRequired{
      resource: Zippiker.Accounts.Token,
      splode: Ash.Error,
      bread_crumbs: ["Error returned from: Zippiker.Accounts.Token.store_token"],
      vars: [],
      path: [],
      stacktrace: #Splode.Stacktrace<>,
      class: :invalid
    }
  ]
}

my approach was to add the multi-tenancy block like this

and then generate migration files like this

but as you can see no migration is created. so am stuck at that point.

finally as you can see from this image, when i create a new team(tenant) in this case; zippiker, ash automatically creates a schema and runs the migrations for me, so the tokens table should be included in case i had told ash about my intention of making the respective resource multitenant, which i clearly forgot to do.

Yes, it is very likely a bug, but not so much I can do w/o looking at where it went wrong. You can manually modify the underlying migrations to create the table to unblock yourself in the short term.

1 Like

here is the project please

Hi @johnsseruwagi,

Token resource should not be tenant aware at this point in the serie. You will have to remove below block from zippiker/lib/zippiker/accounts/token.ex at main · johnsseruwagi/zippiker · GitHub

  multitenancy do
    strategy :context
  end

Then delete migration except https://github.com/johnsseruwagi/zippiker/blob/main/priv/repo/migrations/20250211194154_initialize_extensions_1.exs

Then generate migrations again.

Here the repository for that serie. GitHub - kamaroly/helpcenter: A Helpcenter System Built in Elixir's Phoenix and Ash Frameworks Let me know if it heps.

2 Likes

Hello bro thanks, i removed the block from the token.ex file and the test passed, however i have read through your code and you don’t have a support domain in your code, i guess it was generated for me when i generated the authentication, so i had to remove the block also from the corresponding blocks,

now where i have stagnated from is where you say that i delete the migration files except the one you have mentioned, ofwhich i have diligently done, but am stuck at a point where when i try to generate new migration files i get the following response

Extension Migrations: 
No extensions to install

Generating Tenant Migrations: 
No tenant changes detected, so no migrations or snapshots have been created.

Generating Migrations:
No changes detected, so no migrations or snapshots have been created.

I believe that is happening because zippiker/priv/resource_snapshots/repo/tokens at main · johnsseruwagi/zippiker · GitHub is still there.

1 Like

Hello bro, thank you very much, i have managed to solve the migrations issue, however, deleting the /resource_snapshots/repo/tokens is not a good idea, this is because the generated migration file, will try to attempt to create a new table in the data base which actually already exists.

So, this is how i have gone about it:

  1. deleted this directory; priv/repo/tenant_migrations
  2. and then this one priv/repo/migrations/20250301143241_add_multitenancy_tables.exs
  3. finally this resource_snapshots/repo/tenants

i finally generated new migration files which recreated the respective files, but this time with only resources that are supposed to be multitenant. and it has worked properly

Now heading to part 15. I can’t thank you enough and @zachdaniel for the help provided.

1 Like

Great! I will publishish Part 17 sometime this week. Once done with auth, we’ll dive into more good stuff. Hope you’ll enjoy.

3 Likes