Organizations

Group users in organizations with roles and settings. Owner users can manage other users and invite new users. Allows users to define a custom URL slug for their organization.

Level: Advanced
16
1013
2

The Scope struct

The Scope struct is the foundation for multi-tenancy. It carries information about the current user and, optionally, the current organization. This allows us to pass context through our application without threading individual parameters everywhere.

The struct has two fields: user and organization . The for_user/1 function creates a scope for a given user, and put_organization/2 adds the active organization to an existing scope.

Organizations context

The Organizations context module provides the public API for working with organizations. All functions accept a Scope struct as the first argument, which provides the current user context.

Key functions include:

For member management:

The create_organization/2 function uses Ecto.Multi to atomically create both the organization and the initial membership record for the creator as owner.

Organization schema

The Organization schema defines the structure for organizations. Each organization has a name and a slug . The slug is used in URLs and must follow a specific format: lowercase letters and numbers separated by dashes (e.g., my-org ).

The schema includes a virtual field user_role which gets populated when querying organizations through the join table. This tells us the current user's role within that organization.

The cast_default_slug/2 function automatically generates a slug from the organization name when creating a new organization, making the UX friendlier.

We also implement Phoenix.Param for the organization, so it can be used directly in URL helpers with its slug.

OrganizationUser join schema

The OrganizationUser schema represents the many-to-many relationship between users and organizations. It uses a composite primary key consisting of organization_id and user_id , which prevents duplicate memberships.

Each membership has a role field that can be either :owner or :member . Owners have full control over the organization, while members have limited access.

The module provides several query helpers for joining users with organizations. organization_with_user_query/2 retrieves organizations for a user and populates the user_role virtual field. user_with_organization_query/2 does the inverse, retrieving users for an organization and populating organization_role .

The update_multi/3 function includes a safety check to ensure an organization always has at least one owner.

Creating and editing organizations

The organization form LiveView handles both creating new organizations and editing existing ones. It uses the live_action assign to differentiate between :new and :edit modes.

When creating a new organization, the slug is automatically generated from the name as the user types, thanks to the cast_default_slug: true option passed to the changeset.

Listing organizations

The organizations listing LiveView shows all organizations the current user belongs to. It uses LiveView streams for efficient rendering of the table.

The table displays the organization name and slug, with conditional edit and delete actions that only appear for owners. Clicking a row navigates to that organization's home page.

Router configuration

Organization routes are configured in two scopes. First, the organization listing and creation routes live under the :require_authenticated_user live_session, as they only require a logged-in user.

Organization-specific routes use a URL pattern of /~/slug , where slug is the organization's slug. These routes are placed in separate live_sessions that include the :require_organization_access on_mount hook.

Owner-only routes (like editing the organization or managing members) add the :require_organization_owner on_mount hook for additional authorization.

The pipelines include both plug-based and on_mount-based authorization to ensure consistent protection whether routes are accessed via initial page load or LiveView navigation.

Authentication helpers

The UserAuth module is extended with organization-related authentication helpers. These work as both plugs (for controller routes) and on_mount callbacks (for LiveViews).

require_organization_access checks that the current user has access to the organization specified by the :slug parameter in the URL. If access is granted, it updates the scope with the organization. If not, it redirects with an error.

require_organization_owner is an additional check that ensures the user has the :owner role for the current organization. This is used to protect admin-only routes.

Both helpers are available as plug functions and on_mount/4 clauses, allowing consistent authorization across controller and LiveView routes.

Database migration

The migration creates two tables: organizations and organizations_users .

The organizations table has a UUID primary key, a name, and a case-insensitive slug (using the citext type) with a unique index.

The organizations_users table uses a composite primary key of organization_id and user_id , with a role field. Both foreign keys are set to cascade deletes, so removing a user or organization automatically cleans up the memberships.