Files
myeasycms-v2/docs/api/account-workspace-api.mdoc
Giancarlo Buomprisco 7ebff31475 Next.js Supabase V3 (#463)
Version 3 of the kit:
- Radix UI replaced with Base UI (using the Shadcn UI patterns)
- next-intl replaces react-i18next
- enhanceAction deprecated; usage moved to next-safe-action
- main layout now wrapped with [locale] path segment
- Teams only mode
- Layout updates
- Zod v4
- Next.js 16.2
- Typescript 6
- All other dependencies updated
- Removed deprecated Edge CSRF
- Dynamic Github Action runner
2026-03-24 13:40:38 +08:00

474 lines
13 KiB
Plaintext

---
status: "published"
label: "Team Workspace API"
order: 4
title: "Team Workspace API | Next.js Supabase SaaS Kit"
description: "Access team account context in MakerKit layouts. Load team data, member permissions, subscription status, and role hierarchy with the Team Workspace API."
---
The Team Workspace API provides team account context for pages under `/home/[account]`. It loads team data, the user's role and permissions, subscription status, and all accounts the user belongs to, making this information available to both server and client components.
{% sequence title="Team Workspace API Reference" description="Access team workspace data in layouts and components" %}
[loadTeamWorkspace (Server)](#loadteamworkspace-server)
[useTeamAccountWorkspace (Client)](#useteamaccountworkspace-client)
[Data structure](#data-structure)
[Usage patterns](#usage-patterns)
{% /sequence %}
## loadTeamWorkspace (Server)
Loads the team workspace data for the specified team account. Use this in Server Components within the `/home/[account]` route group.
```tsx
import { loadTeamWorkspace } from '~/home/[account]/_lib/server/team-account-workspace.loader';
export default async function TeamDashboard({
params,
}: {
params: { account: string };
}) {
const data = await loadTeamWorkspace();
return (
<div>
<h1>{data.account.name}</h1>
<p>Your role: {data.account.role}</p>
</div>
);
}
```
### Function signature
```tsx
async function loadTeamWorkspace(): Promise<TeamWorkspaceData>
```
### How it works
The loader reads the `account` parameter from the URL (the team slug) and fetches:
1. Team account details from the database
2. Current user's role and permissions in this team
3. All accounts the user belongs to (for the account switcher)
### Caching behavior
The function uses React's `cache()` to deduplicate calls within a single request. You can call it multiple times in nested components without additional database queries.
```tsx
// Both calls use the same cached data
const layout = await loadTeamWorkspace(); // First call: hits database
const page = await loadTeamWorkspace(); // Second call: returns cached data
```
{% callout title="Performance consideration" %}
While calls are deduplicated within a request, the data is fetched on every navigation. For frequently accessed data, the caching prevents redundant queries within a single page render.
{% /callout %}
---
## useTeamAccountWorkspace (Client)
Access the team workspace data in client components using the `useTeamAccountWorkspace` hook. The data is provided through React Context from the layout.
```tsx
'use client';
import { useTeamAccountWorkspace } from '@kit/team-accounts/hooks/use-team-account-workspace';
export function TeamHeader() {
const { account, user, accounts } = useTeamAccountWorkspace();
return (
<header className="flex items-center justify-between p-4">
<div className="flex items-center gap-3">
{account.picture_url && (
<img
src={account.picture_url}
alt={account.name}
className="h-8 w-8 rounded"
/>
)}
<div>
<h1 className="font-semibold">{account.name}</h1>
<p className="text-xs text-muted-foreground">
{account.role} · {account.subscription_status || 'Free'}
</p>
</div>
</div>
</header>
);
}
```
{% callout type="warning" title="Context requirement" %}
The `useTeamAccountWorkspace` hook only works within the `/home/[account]` route group where the context provider is set up. Using it outside this layout will throw an error.
{% /callout %}
---
## Data structure
### TeamWorkspaceData
```tsx
import type { User } from '@supabase/supabase-js';
interface TeamWorkspaceData {
account: {
id: string;
name: string;
slug: string;
picture_url: string | null;
role: string;
role_hierarchy_level: number;
primary_owner_user_id: string;
subscription_status: SubscriptionStatus | null;
permissions: string[];
};
user: User;
accounts: Array<{
id: string | null;
name: string | null;
picture_url: string | null;
role: string | null;
slug: string | null;
}>;
}
```
### account.role
The user's role in this team. Default roles:
| Role | Description |
|------|-------------|
| `owner` | Full access, can delete team |
| `admin` | Manage members and settings |
| `member` | Standard access |
### account.role_hierarchy_level
A numeric value where lower numbers indicate higher privilege. Use this for role comparisons:
```tsx
const { account } = useTeamAccountWorkspace();
// Check if user can manage someone with role_level 2
const canManage = account.role_hierarchy_level < 2;
```
### account.permissions
An array of permission strings the user has in this team:
```tsx
[
'billing.manage',
'members.invite',
'members.remove',
'members.manage',
'settings.manage',
]
```
### subscription_status values
| Status | Description |
|--------|-------------|
| `active` | Active subscription |
| `trialing` | In trial period |
| `past_due` | Payment failed, grace period |
| `canceled` | Subscription canceled |
| `unpaid` | Payment required |
| `incomplete` | Setup incomplete |
| `incomplete_expired` | Setup expired |
| `paused` | Subscription paused |
---
## Usage patterns
### Permission-based rendering
```tsx
'use client';
import { useTeamAccountWorkspace } from '@kit/team-accounts/hooks/use-team-account-workspace';
interface PermissionGateProps {
children: React.ReactNode;
permission: string;
fallback?: React.ReactNode;
}
export function PermissionGate({
children,
permission,
fallback = null,
}: PermissionGateProps) {
const { account } = useTeamAccountWorkspace();
if (!account.permissions.includes(permission)) {
return <>{fallback}</>;
}
return <>{children}</>;
}
// Usage
function TeamSettingsPage() {
return (
<div>
<h1>Team Settings</h1>
<PermissionGate
permission="settings.manage"
fallback={<p>You don't have permission to manage settings.</p>}
>
<SettingsForm />
</PermissionGate>
<PermissionGate permission="billing.manage">
<BillingSection />
</PermissionGate>
</div>
);
}
```
### Team dashboard with role checks
```tsx
import { loadTeamWorkspace } from '~/home/[account]/_lib/server/team-account-workspace.loader';
import { getSupabaseServerClient } from '@kit/supabase/server-client';
export default async function TeamDashboardPage() {
const { account, user } = await loadTeamWorkspace();
const client = getSupabaseServerClient();
const isOwner = account.primary_owner_user_id === user.id;
const isAdmin = account.role === 'admin' || account.role === 'owner';
// Fetch team-specific data
const { data: projects } = await client
.from('projects')
.select('*')
.eq('account_id', account.id)
.order('created_at', { ascending: false })
.limit(10);
return (
<div className="space-y-6">
<header className="flex items-center justify-between">
<div>
<h1 className="text-2xl font-bold">{account.name}</h1>
<p className="text-muted-foreground">
{account.subscription_status === 'active'
? 'Pro Plan'
: 'Free Plan'}
</p>
</div>
{isAdmin && (
<a
href={`/home/${account.slug}/settings`}
className="btn btn-secondary"
>
Team Settings
</a>
)}
</header>
<section>
<h2 className="text-lg font-medium">Recent Projects</h2>
<ul className="mt-2 space-y-2">
{projects?.map((project) => (
<li key={project.id}>
<a href={`/home/${account.slug}/projects/${project.id}`}>
{project.name}
</a>
</li>
))}
</ul>
</section>
{isOwner && (
<section className="rounded-lg border border-destructive/20 bg-destructive/5 p-4">
<h2 className="font-medium text-destructive">Danger Zone</h2>
<p className="mt-1 text-sm text-muted-foreground">
Only the team owner can delete this team.
</p>
<button className="mt-3 btn btn-destructive">Delete Team</button>
</section>
)}
</div>
);
}
```
### Team members list with permissions
```tsx
import { loadTeamWorkspace } from '~/home/[account]/_lib/server/team-account-workspace.loader';
import { getSupabaseServerClient } from '@kit/supabase/server-client';
export default async function TeamMembersPage() {
const { account } = await loadTeamWorkspace();
const client = getSupabaseServerClient();
const canManageMembers = account.permissions.includes('members.manage');
const canRemoveMembers = account.permissions.includes('members.remove');
const canInviteMembers = account.permissions.includes('members.invite');
const { data: members } = await client
.from('accounts_memberships')
.select(`
user_id,
role,
created_at,
users:user_id (
email,
user_metadata
)
`)
.eq('account_id', account.id);
return (
<div>
<header className="flex items-center justify-between">
<h1>Team Members</h1>
{canInviteMembers && (
<a href={`/home/${account.slug}/settings/members/invite`}>
Invite Member
</a>
)}
</header>
<table className="w-full">
<thead>
<tr>
<th>Member</th>
<th>Role</th>
<th>Joined</th>
{(canManageMembers || canRemoveMembers) && <th>Actions</th>}
</tr>
</thead>
<tbody>
{members?.map((member) => (
<tr key={member.user_id}>
<td>{member.users?.email}</td>
<td>{member.role}</td>
<td>{new Date(member.created_at).toLocaleDateString()}</td>
{(canManageMembers || canRemoveMembers) && (
<td>
{canManageMembers && member.user_id !== account.primary_owner_user_id && (
<button>Change Role</button>
)}
{canRemoveMembers && member.user_id !== account.primary_owner_user_id && (
<button>Remove</button>
)}
</td>
)}
</tr>
))}
</tbody>
</table>
</div>
);
}
```
### Client-side permission hook
```tsx
'use client';
import { useTeamAccountWorkspace } from '@kit/team-accounts/hooks/use-team-account-workspace';
export function useTeamPermissions() {
const { account } = useTeamAccountWorkspace();
return {
canManageSettings: account.permissions.includes('settings.manage'),
canManageBilling: account.permissions.includes('billing.manage'),
canInviteMembers: account.permissions.includes('members.invite'),
canRemoveMembers: account.permissions.includes('members.remove'),
canManageMembers: account.permissions.includes('members.manage'),
isOwner: account.role === 'owner',
isAdmin: account.role === 'admin' || account.role === 'owner',
role: account.role,
roleLevel: account.role_hierarchy_level,
};
}
// Usage
function TeamActions() {
const permissions = useTeamPermissions();
return (
<div className="flex gap-2">
{permissions.canInviteMembers && (
<button>Invite Member</button>
)}
{permissions.canManageSettings && (
<button>Settings</button>
)}
{permissions.canManageBilling && (
<button>Billing</button>
)}
</div>
);
}
```
### Subscription-gated features
```tsx
'use client';
import { useTeamAccountWorkspace } from '@kit/team-accounts/hooks/use-team-account-workspace';
export function PremiumFeature({ children }: { children: React.ReactNode }) {
const { account } = useTeamAccountWorkspace();
const hasActiveSubscription =
account.subscription_status === 'active' ||
account.subscription_status === 'trialing';
if (!hasActiveSubscription) {
return (
<div className="rounded-lg border-2 border-dashed p-6 text-center">
<h3 className="font-medium">Premium Feature</h3>
<p className="mt-1 text-sm text-muted-foreground">
Upgrade to access this feature
</p>
<a
href={`/home/${account.slug}/settings/billing`}
className="mt-3 inline-block btn btn-primary"
>
Upgrade Plan
</a>
</div>
);
}
return <>{children}</>;
}
```
## Related documentation
- [User Workspace API](/docs/next-supabase-turbo/api/user-workspace-api) - Personal account context
- [Team Account API](/docs/next-supabase-turbo/api/team-account-api) - Team operations
- [Authentication API](/docs/next-supabase-turbo/api/authentication-api) - User authentication
- [Per-seat Billing](/docs/next-supabase-turbo/billing/per-seat-billing) - Team-based pricing