Files
myeasycms-v2/docs/api/user-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

359 lines
9.3 KiB
Plaintext

---
status: "published"
label: "User Workspace API"
order: 3
title: "User Workspace API | Next.js Supabase SaaS Kit"
description: "Access personal workspace data in MakerKit layouts. Load user account information, subscription status, and account switcher data with the User Workspace API."
---
The User Workspace API provides personal account context for pages under `/home/(user)`. It loads user data, subscription status, and all accounts the user belongs to, making this information available to both server and client components.
{% sequence title="User Workspace API Reference" description="Access personal workspace data in layouts and components" %}
[loadUserWorkspace (Server)](#loaduserworkspace-server)
[useUserWorkspace (Client)](#useuserworkspace-client)
[Data structure](#data-structure)
[Usage patterns](#usage-patterns)
{% /sequence %}
## loadUserWorkspace (Server)
Loads the personal workspace data for the authenticated user. Use this in Server Components within the `/home/(user)` route group.
```tsx
import { loadUserWorkspace } from '~/home/(user)/_lib/server/load-user-workspace';
export default async function PersonalDashboard() {
const data = await loadUserWorkspace();
return (
<div>
<h1>Welcome, {data.user.email}</h1>
<p>Account: {data.workspace.name}</p>
</div>
);
}
```
### Function signature
```tsx
async function loadUserWorkspace(): Promise<UserWorkspaceData>
```
### 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 loadUserWorkspace(); // First call: hits database
const page = await loadUserWorkspace(); // Second call: returns cached data
```
{% callout title="Performance consideration" %}
While calls are deduplicated within a request, the data is fetched on every navigation. If you only need a subset of the data (like subscription status), consider making a more targeted query.
{% /callout %}
---
## useUserWorkspace (Client)
Access the workspace data in client components using the `useUserWorkspace` hook. The data is provided through React Context from the layout.
```tsx
'use client';
import { useUserWorkspace } from '@kit/accounts/hooks/use-user-workspace';
export function ProfileCard() {
const { workspace, user, accounts } = useUserWorkspace();
return (
<div className="rounded-lg border p-4">
<div className="flex items-center gap-3">
{workspace.picture_url && (
<img
src={workspace.picture_url}
alt={workspace.name ?? 'Profile'}
className="h-10 w-10 rounded-full"
/>
)}
<div>
<p className="font-medium">{workspace.name}</p>
<p className="text-sm text-muted-foreground">{user.email}</p>
</div>
</div>
{workspace.subscription_status && (
<div className="mt-3">
<span className="text-xs uppercase tracking-wide text-muted-foreground">
Plan: {workspace.subscription_status}
</span>
</div>
)}
</div>
);
}
```
{% callout type="warning" title="Context requirement" %}
The `useUserWorkspace` hook only works within the `/home/(user)` route group where the context provider is set up. Using it outside this layout will throw an error.
{% /callout %}
---
## Data structure
### UserWorkspaceData
```tsx
import type { User } from '@supabase/supabase-js';
interface UserWorkspaceData {
workspace: {
id: string | null;
name: string | null;
picture_url: string | null;
public_data: Json | null;
subscription_status: SubscriptionStatus | null;
};
user: User;
accounts: Array<{
id: string | null;
name: string | null;
picture_url: string | null;
role: string | null;
slug: string | null;
}>;
}
```
### 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 |
### accounts array
The `accounts` array contains all accounts the user belongs to, including:
- Their personal account
- Team accounts where they're a member
- The user's role in each account
This data powers the account switcher component.
---
## Usage patterns
### Personal dashboard page
```tsx
import { loadUserWorkspace } from '~/home/(user)/_lib/server/load-user-workspace';
export default async function DashboardPage() {
const { workspace, user, accounts } = await loadUserWorkspace();
const hasActiveSubscription =
workspace.subscription_status === 'active' ||
workspace.subscription_status === 'trialing';
return (
<div className="space-y-6">
<header>
<h1 className="text-2xl font-bold">Dashboard</h1>
<p className="text-muted-foreground">
Welcome back, {user.user_metadata.full_name || user.email}
</p>
</header>
{!hasActiveSubscription && (
<div className="rounded-lg border border-yellow-200 bg-yellow-50 p-4">
<p>Upgrade to unlock premium features</p>
<a href="/pricing" className="text-primary underline">
View plans
</a>
</div>
)}
<section>
<h2 className="text-lg font-medium">Your teams</h2>
<ul className="mt-2 space-y-2">
{accounts
.filter((a) => a.slug !== null)
.map((account) => (
<li key={account.id}>
<a
href={`/home/${account.slug}`}
className="flex items-center gap-2 rounded-lg p-2 hover:bg-muted"
>
{account.picture_url && (
<img
src={account.picture_url}
alt=""
className="h-8 w-8 rounded"
/>
)}
<span>{account.name}</span>
<span className="ml-auto text-xs text-muted-foreground">
{account.role}
</span>
</a>
</li>
))}
</ul>
</section>
</div>
);
}
```
### Account switcher component
```tsx
'use client';
import { useUserWorkspace } from '@kit/accounts/hooks/use-user-workspace';
import { useRouter } from 'next/navigation';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@kit/ui/select';
export function AccountSwitcher() {
const { workspace, accounts } = useUserWorkspace();
const router = useRouter();
const handleChange = (value: string) => {
if (value === 'personal') {
router.push('/home');
} else {
router.push(`/home/${value}`);
}
};
return (
<Select
defaultValue={workspace.id ?? 'personal'}
onValueChange={handleChange}
>
<SelectTrigger className="w-[200px]">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="personal">
Personal Account
</SelectItem>
{accounts
.filter((a) => a.slug)
.map((account) => (
<SelectItem key={account.id} value={account.slug!}>
{account.name}
</SelectItem>
))}
</SelectContent>
</Select>
);
}
```
### Feature gating with subscription status
```tsx
'use client';
import { useUserWorkspace } from '@kit/accounts/hooks/use-user-workspace';
interface FeatureGateProps {
children: React.ReactNode;
fallback?: React.ReactNode;
requiredStatus?: string[];
}
export function FeatureGate({
children,
fallback,
requiredStatus = ['active', 'trialing'],
}: FeatureGateProps) {
const { workspace } = useUserWorkspace();
const hasAccess = requiredStatus.includes(
workspace.subscription_status ?? ''
);
if (!hasAccess) {
return fallback ?? null;
}
return <>{children}</>;
}
// Usage
function PremiumFeature() {
return (
<FeatureGate
fallback={
<div className="text-center p-4">
<p>This feature requires a paid plan</p>
<a href="/pricing">Upgrade now</a>
</div>
}
>
<ExpensiveComponent />
</FeatureGate>
);
}
```
### Combining with server data
```tsx
import { loadUserWorkspace } from '~/home/(user)/_lib/server/load-user-workspace';
import { getSupabaseServerClient } from '@kit/supabase/server-client';
export default async function TasksPage() {
const { workspace, user } = await loadUserWorkspace();
const client = getSupabaseServerClient();
// Fetch additional data using the workspace context
const { data: tasks } = await client
.from('tasks')
.select('*')
.eq('account_id', workspace.id)
.eq('created_by', user.id)
.order('created_at', { ascending: false });
return (
<div>
<h1>My Tasks</h1>
<TaskList tasks={tasks ?? []} />
</div>
);
}
```
## Related documentation
- [Team Workspace API](/docs/next-supabase-turbo/api/account-workspace-api) - Team account context
- [Account API](/docs/next-supabase-turbo/api/account-api) - Account operations
- [Authentication API](/docs/next-supabase-turbo/api/authentication-api) - User authentication