Jetstream Built-in Teams vs Custom Hierarchical Multi-tenancy for Nested Organizations
0 reputation · 07 Oct 2021, 00:22 UTC
0 reputation · 07 Oct 2021, 00:22 UTC
Laravel Jetstream ships with a first-party Team feature that models a flat many-to-many relationship between users and teams via a team_user pivot table. The active context is tracked by a current_team_id column on the User model, and authorization relies on Gates and Policies scoped to that single active team. This design assumes each user belongs to one active team at a time and does not natively represent parent-child team hierarchies or organizational units nested within other teams.
The application must support organizations that contain multiple divisions, each with their own sub-teams, where permissions cascade from parent to child and users may hold different roles at different levels of the hierarchy simultaneously.
Extending the built-in pivot table with additional columns (e.g., parent_team_id, hierarchy_path) keeps the Jetstream authentication and session workflow intact but requires overriding core methods such as currentTeam and rewriting policy checks to traverse the hierarchy. Building a separate multi-tenancy layer (e.g., using a package like stancl/tenancy or a custom tenant resolver) cleanly isolates hierarchical logic but introduces a second tenancy context that must stay synchronized with Jetstream's current_team_id for features like team-scoped API tokens and UI switching components.
Which approach better balances long-term maintainability with the least friction against Jetstream's existing team-switching UI, invitation flow, and API token scoping?
33775 reputation · 07 Oct 2021, 07:39 UTC
For a shared‑database application that needs divisions, sub‑teams, and cascading roles, the best trade‑off is to extend Jetstream’s built‑in Teams feature with a closure table (or materialized path) and a dedicated PermissionService. This keeps the current_team_id workflow, team‑switching UI, invitation flow, and API‑token scoping untouched, while adding the hierarchy logic only where it is needed. A separate multi‑tenancy layer (e.g., stancl/tenancy) introduces a second tenancy context that must be kept in sync with Jetstream, adds routing and database‑separation complexity, and forces you to rebuild much of the existing Jetstream UI and authorization logic.
current_team_id. Adding a parent_team_id or hierarchy_path column does not change that UI.team_id, continue to work. The PermissionService simply starts from the token’s team and walks up the hierarchy.HierarchicalGate) needs to be updated. All other Jetstream logic remains untouched.In the scenario described—shared‑database nested organizations with cascading permissions—those use‑cases are not present, so the extension approach is the leaner, more maintainable choice.
CREATE TABLE team_ancestors (
ancestor_id BIGINT UNSIGNED NOT NULL,
descendant_id BIGINT UNSIGNED NOT NULL,
depth INT NOT NULL,
PRIMARY KEY (ancestor_id, descendant_id)
);
Use a closure table so that queries for ancestors or descendants are O(1) with a single index.
currentTeam() only when necessary to return the active team plus its ancestors. The override should call the parent method and then fetch the ancestor chain via the closure table.
PermissionService:
Team (or token’s team) and walk up the ancestor chain, merging role‑based permissions.HierarchicalGate that delegates to PermissionService::allows.
php artisan test --filter=Team to ensure Jetstream’s own tests still pass.team_id pointing to the leaf team.tokenCan uses PermissionService to resolve ancestor permissions.If your application requires a user to be “active” in multiple teams at the same time (e.g., a dashboard that shows data from several divisions simultaneously), you may need a different approach such as a separate tenancy context or a custom multi‑active team resolver. Let me know if that scenario applies, and we can adjust the recommendation accordingly.
Use comments to ask for clarification. Post a solution as an answer.
No question comments on this page.