🏒Organizations & Multitenancy

We have provided a basic structure for organizations, which are basically groups of people. Often user want to create some kind of organization (eg. a company), and then invite their teammates into it. This is a step towards multi-tenancy, with an organization representing a tenant.

If you were introducing a payment system, it would likely be associated with the org (we will implement this in v1.3.0.

Database structure

Org routes

The following routes are provided:

live "/org/new", NewOrgLive
live "/org/:org_slug", OrgDashboardLive
live "/org/:org_slug/edit", EditOrgLive
live "/org/:org_slug/team", OrgTeamLive, :index
live "/org/:org_slug/team/invite", OrgTeamLive, :invite
live "/org/:org_slug/team/memberships/:id/edit", OrgTeamLive, :edit_membership

This provides some basic functionality to get you started:

  • Any user can CRUD an org

  • Basic roles (admin & member)

  • Members can create invitations for new members

  • Invitations turn into memberships upon acceptance

  • Admins can delete memberships

Org plugs

We have some plugs to help with assigning org related data

assign_org_data

This will assign to the conn:

require_org_member

This makes sure the current_user is a member of the current org (determined by the URL param :org_slug). The path must have :org_slug in it.

require_org_admin

Same as require_org_member above but the user must have a role of admin on their membership for the org.

Org on_mount hooks

You can run on_mount hooks in your live_session calls. For example:

live_session :require_confirmed_user,
  on_mount: [
    {PetalProWeb.UserOnMountHooks, :require_confirmed_user},
    {PetalProWeb.OrgOnMountHooks, :assign_org_data}
  ] do
  live "/org/new", NewOrgLive
  # page_builder:live:protected
end

We have hooks matching the plugs above:

Multi-tenancy

Currently you can only see your current org if you are in a route scoped to that org (/:org_slug/page), with the org slug representing the tenant (org).

There are several options when it comes to identifying the current org (listed below). Please let us know if you want more options supported. We have a feedback form here.

1. Params (supported)

Load the org from a body or query param (how it's currently done).

/org/my-org/path_x
/org/my-org/path_y
/org/my-org/path_z/:some_param

2. Session (not yet supported)

Set the current org as a cookie and fetch it in the plug from the cookie. Then you don't have to scope the route:

/path_x
/path_y
/path_z/:some_param

You will need to create a plug that sets the current org - you could use set_locale_plug.ex as inspiration.

3. Subdomain (not yet supported)

Set the tenant slug as a subdomain. Similar to "params" but means you don't have to scope all of your routes.

my-org.yourdomain.com/path_x
my-org.yourdomain.com/path_y
my-org.yourdomain.com/path_z/:some_param

You would need to create a plug to do this.

How to prevent data leakage - true multi-tenancy

Currently, all your orgs (tenants) data is in one database. It is up to you to scope all of your Ecto queries to ensure the data shown on the page belongs to the right org.

A potentially safer option is to make a new Postgres Schema per tenant with the help of Triplex. In which case your queries will be like this:

Repo.all(User, prefix: Triplex.to_prefix("my_tenant"))
Repo.get!(User, 123, prefix: Triplex.to_prefix("my_tenant"))

Triplex provides some plugs to help set the org:

    - `Triplex.ParamPlug` - loads the tenant from a body or query param
    - `Triplex.SessionPlug` - loads the tenant from a session param
    - `Triplex.SubdomainPlug` - loads the tenant from the url subdomain
    - `Triplex.EnsurePlug` - ensures the current tenant is loaded and halts if not

If you are unsure about multi-tenancy, this blog post explains the different approaches well.

Removing organizations

You will have to extract orgs out of your code. This will take approximately 15 minutes. If you have time, consider letting us know at support@petal.build if you do this so we can get an idea of how many people are doing it (if it's a lot, then we will consider making it easier to delete).

Steps to delete

Delete the migrations:

Delete files and folders related to orgs:

Remove references in user.exand log.ex:

From here, you can just do a global search for "org" and delete what's left.

  • Accounts.preload_org_data/2.

  • Router org routes.

  • Router.assign_org_data/2 plug.

  • Reference in Logs.build/2.

  • Org related logs in log.ex - @action_options

  • UserNotifier.deliver_org_invitation/3

  • Delete org_seeder.ex

  • References in seeds.exs

  • References in email_testing_controller.ex

  • References in email.ex

  • Email template: org_invitation.html.heex

  • Test folder:

    • org_team_live_test.exs

    • orgs_fixtures.ex

    • dashboard_live_test.exs