Building Team-Scoped Dashboards in Next.js
How GitMyABI uses team-scoped URLs, nested providers, React Query keys, and immutable team IDs to structure a multi-tenant Next.js dashboard.
- Published
- July 12, 2026
- Updated
- Updated July 12, 2026
- Author
- Daniel Mark
- Reading time
- 10 min read
Adding teams to a developer dashboard changes more than navigation. It changes how URLs express scope, how providers derive tenant context, how cached data stays isolated, how billing return paths are constructed, and how deep links survive a refresh.
I worked on this problem at GitMyABI, a smart contract developer platform from AxLabs GmbH. When the product gained shared teams, per-team billing, and shareable settings links, “which team am I in?” stopped being only a React state problem and became a routing architecture problem.
The resulting patterns apply to any B2B developer tool built with the Next.js App Router.
The product and architectural constraints
A team-aware dashboard usually needs several properties at once:
- Shareable URLs. A teammate should be able to open the same billing or settings view you are using.
- Refresh-safe scope. Reloading the page should not silently drop the active team.
- Composable routing. Team scope should compose with nested layouts in the same way that project and build segments do.
- Billing ownership. Subscriptions belong to teams rather than individual users, while checkout and portal flows must return users to the correct dashboard.
- Incremental migration. Products that shipped single-user URLs before introducing teams rarely get a clean cutover. Old and new route families often need to coexist temporarily.
Those constraints pushed team scope into the URL path rather than leaving it in optional client state.
Why team scope belongs in the URL path
Query parameters can carry shareable state, but team identity is not an optional filter. It is part of the resource hierarchy, which makes a path segment the more natural representation.
Consider these two routes:
The first route tells the router, the layout tree, and the reader the same story. The billing page belongs to the acme-corp team.
The second treats the team as an optional modifier on an otherwise global billing page. Every layout and link builder must remember to read and preserve that parameter.
This distinction matters in the App Router. A [teamSlug] dynamic segment can own its layout, loading state, error boundary, and provider composition. The route structure itself carries the scope.
A URL slug is not authorization, however. It identifies the team being requested. Server-side authorization must still determine whether the caller can access that team and its resources.
A URL slug is not authorization, however. It identifies the team being requested. Server-side authorization must still determine whether the caller can access that team and its resources.
How the dashboard routes are divided
GitMyABI’s dashboard contains three broad route families.
Team-level routes
User-level settings
Project and build details
Team-level features use the team slug directly. User preferences remain global because they belong to the authenticated account rather than a particular team.
Project and build pages retain their established project-based route shape. This allows the team shell to evolve without immediately invalidating existing project deep links.
Resolving /dashboard and the default team
/dashboard acts as a routing pivot rather than a final destination. It loads the user’s teams and replaces the URL with the selected team slug:
Default-team selection follows a predictable order:
- The preferred team slug saved in localStorage, provided the user still belongs to that team
- The user’s personal team
- The first team returned by the API
The page also handles loading and empty states. Users see a loading state while their teams are being fetched and an empty state when no team is available.
This resolution currently happens in the browser after session hydration. That fits the existing client-side session model, although resolving the session and default team earlier could reduce the amount of post-render routing work.
Deriving provider state from the route
Team context is composed through layouts rather than stored in a single undifferentiated global object.
The root provider tree handles application-wide concerns such as authentication. The [teamSlug] layout introduces the providers that depend on the active team:
Inside TeamProvider, the slug is matched against the authenticated user’s teams. A successful match becomes the active currentTeam and is stored as the preferred team for future visits.
Switching teams updates the URL while preserving the rest of the current path:
Project routes require slightly different handling because the team is not encoded in their path. Once a project loads, its owning team can be used to align the surrounding dashboard context.
This produces three distinct layers:
- Requested scope comes from the URL on team-scoped routes.
- Resolved UI scope comes from
currentTeamafter matching the authenticated user’s teams. - Permitted scope must be enforced by the API for every protected resource and operation.
Keeping those layers separate prevents React context from being mistaken for an authorization system.
Authentication UX versus tenant authorization
GitMyABI uses a client route guard to coordinate session loading and redirect unauthenticated visitors. This improves navigation UX, but it does not authorize tenant data.
The route guard can decide whether dashboard content should be shown. It cannot prove that a user may read a particular project, manage a team member, or change a subscription.
Project, build, team, and billing operations still require server-side authorization. Each protected request must validate the session, resolve the requested resource, and confirm the caller’s membership and role.
In Next.js 16, Proxy can perform an optimistic authentication check before the page renders. That can reduce redirect flashes and centralize early routing decisions. It should not replace authorization near server-side data access, API handlers, Server Actions, or backend services.
Client guards manage experience. Server-side checks protect data.
Keeping cached data team-scoped
TanStack Query uses a shared QueryClient that persists across dashboard navigation. Tenant separation therefore depends heavily on query-key design.
Team-aware queries include the team dimension explicitly:
When the user changes teams, teamId changes and React Query resolves a different cache entry. Project lists, team builds, and subscription data remain partitioned without clearing the entire cache.
Queries for individual resources may use globally unique identifiers:
That approach is safe only when identifiers cannot collide between tenants and the server authorizes every request independently. If an identifier is unique only within a team, the team ID must be included in the query key:
Clearing the complete query cache on every team switch would avoid some stale-state risks, but it would also discard unrelated data and trigger unnecessary requests. Designing keys around real ownership boundaries is more precise.
Billing, immutable identity, and scoped return paths
Billing is owned by a team, but navigation uses a readable slug.
The billing page lives at:
Checkout mutations pass the immutable team UUID together with a return path:
This separates two responsibilities:
teamIdidentifies the billing owner for API operations.teamSlugidentifies where the user should return in the dashboard.
That distinction matters because team slugs are editable. A mutable navigation label should not become the permanent identifier for a subscription.
Upgrade and paywall surfaces require the same discipline. Any component linking to billing should receive a team-scoped destination or start checkout using the immutable team ID.
In a second iteration, I would centralize these destinations behind a typed route helper:
Shared components could then require teamSlug or a complete upgradeUrl instead of relying on optional unscoped defaults. This makes invalid navigation harder to express and reduces route drift across the codebase.
Migrating legacy routes without breaking deep links
GitMyABI uses coexistence rather than a single large redirect migration.
Team-level features live under /dashboard/[teamSlug]/..., while established project and build pages continue using /dashboard/project/[projectId]/....
Because legacy project URLs do not contain a team slug, the application recovers team context from the loaded project. Once the project’s owning team is known, the surrounding dashboard context can align itself with that team.
This preserves direct links while allowing new team-level features to adopt the clearer route hierarchy.
The trade-off is additional maintenance. During the transition, the application must understand two route shapes, generate the correct links for each resource, and test navigation between them.
A typed route module helps contain that complexity. It gives components one place to construct URLs and makes it easier to change the canonical route shape later.
Recommended behavioural test matrix
Routing tests should follow user journeys rather than mirror the filesystem.
Scenario | Expected behaviour |
|---|---|
Unauthenticated visit to | Redirect to authentication with the return URL preserved |
Authenticated visit to | Redirect to the selected default team |
User with no teams | Show an empty state without entering a redirect loop |
Invalid team slug | Recover to a valid team or return an appropriate error |
User requests a team they cannot access | Reject or redirect the request without rendering tenant data |
Team switch from a settings page | Update the slug while preserving the valid subpath |
Direct project deep link | Load the project and align the surrounding team context |
Successful billing return | Refresh subscription state inside the correct team shell |
Project-list navigation after switching teams | Fetch results using the new team ID |
Build-detail navigation | Preserve the correct team context for related actions |
A small set of integration tests covering these paths provides more confidence than testing each routing component in isolation.
Trade-offs and what I would improve next
Client-side route guarding
The current stack coordinates authentication after client session hydration. Resolving more of that state server-side, with Proxy used only for appropriate optimistic checks, could reduce redirect flashes.
Authorization would still remain close to the data and protected operations.
Path-first team scope
Every new team feature must construct slug-aware links. The payoff is a dashboard where billing, members, settings, and team-wide resources have supportable deep links.
Dual routing during migration
Legacy project URLs remain stable while nested resources move incrementally into the team-scoped hierarchy. Centralized route construction prevents components from drifting between old and new path shapes.
Scoped billing links
Billing destinations should be derived from the active team rather than a global default. Required props and typed route helpers make that constraint visible at development time.
Provider responsibilities
Layout-scoped team context is useful, but provider responsibilities should be revisited as the migration settles. Providers should derive state from the route and server data without becoming a second routing system.
The application runs Next.js 16 as of this writing. Framework versions affect APIs such as Proxy and dynamic route parameters, but the underlying design remains the same: paths express structure, providers derive UI context, query keys define cache boundaries, and the server enforces access.
Closing lessons
Team-scoped dashboards are a bundle of decisions, not a single feature flag.
Put team scope in the path when it is part of the resource hierarchy. Derive UI context from validated membership data rather than trusting the slug alone. Treat client route guards as authentication UX. Keep billing ownership on immutable team IDs while URLs use readable slugs. Include tenant dimensions in query keys wherever resources are not globally unique.
When existing deep links prevent an immediate migration, let route families coexist deliberately and contain the complexity behind typed helpers and behavioural tests.
That is the architecture I would reuse for the next multi-tenant developer console, whether the product compiles smart contracts or manages any other form of team-owned infrastructure.
Written by
Daniel Mark
Senior Frontend Engineer
Daniel Mark is a senior frontend engineer and product consultant with 7+ years of experience building resilient product interfaces for Web3, SaaS, and enterprise teams. He writes about frontend architecture, design systems, performance, SEO, and the engineering decisions that turn complex systems into dependable user experiences.