> ## Documentation Index
> Fetch the complete documentation index at: https://docs.tuturuuu.com/llms.txt
> Use this file to discover all available pages before exploring further.

# WorkspaceWrapper Component

> A standardized component for handling workspace ID resolution and validation across the application

The `WorkspaceWrapper` component provides a centralized way to handle workspace ID resolution and validation across the application. It automatically converts legacy workspace identifiers to validated UUIDs and provides the full workspace object to child components.

## Problem Solved

Currently, workspace ID resolution is handled inconsistently across components:

* `'personal'` → resolves to the user's personal workspace UUID
* `'internal'` → resolves to `ROOT_WORKSPACE_ID`
* UUID strings → validates and uses as-is

The `WorkspaceWrapper` centralizes this logic and ensures that child components always receive a validated workspace UUID. After resolution, the `isPersonal` flag is derived from the resolved `workspace.personal` field (not from the original `'personal'` URL string), and `isRoot` is derived by comparing `workspace.id` to `ROOT_WORKSPACE_ID`.

<Note>
  This component is specific to the **Next.js App Router** server-component model in `apps/web` (port `7803`). It relies on async `params`, React Suspense, and server-side `getWorkspace`. The platform is actively migrating `apps/web` to `apps/tanstack-web` (TanStack Start) plus `apps/backend` (Rust); see [/platform/architecture/tanstack-rust-migration](/platform/architecture/tanstack-rust-migration). This wrapper pattern does not carry over verbatim to the TanStack Start app, but the `apps/web` documentation below remains valid for the current platform.
</Note>

## URL Context

The `WorkspaceWrapper` is designed for use in Next.js App Router pages with dynamic segments:

### Basic Pattern

* **File structure**: `apps/web/src/app/[locale]/(dashboard)/[wsId]/page.tsx`
* **User-facing URL**: `/[wsId]/...` (locale handled transparently by middleware)
* **Route params**: `{ wsId: string }`

### Advanced Pattern with Additional Params

* **File structure**: `apps/web/src/app/[locale]/(dashboard)/[wsId]/[datasetId]/page.tsx`
* **User-facing URL**: `/[wsId]/[datasetId]/...`
* **Route params**: `{ wsId: string, datasetId: string }`

The component automatically passes through any additional parameters beyond `wsId` to the children function.

## Installation

The canonical source lives in the shared UI package at `@tuturuuu/ui/custom/workspace-wrapper` (file: `packages/ui/src/components/ui/custom/workspace-wrapper.tsx`).

```tsx theme={null}
// Canonical import (shared package — works in any consumer)
import WorkspaceWrapper, {
  withWorkspace,
  type BaseParams,
  type WorkspaceWrapperProps,
} from '@tuturuuu/ui/custom/workspace-wrapper';
```

Within `apps/web` you may also see the legacy path `@/components/workspace-wrapper`. That module is a thin backward-compatible re-export of the canonical package and exposes the same `default`, `withWorkspace`, `BaseParams`, and `WorkspaceWrapperProps` symbols:

```tsx theme={null}
// Backward-compatible re-export inside apps/web
import WorkspaceWrapper from '@/components/workspace-wrapper';
```

Prefer the `@tuturuuu/ui/custom/workspace-wrapper` import for new code, especially in shared packages and other apps.

## Basic Usage

### Function Children Pattern

```tsx theme={null}
export default async function MyPage({ 
  params 
}: { 
  params: Promise<{ wsId: string }> 
}) {
  return (
    <WorkspaceWrapper params={params}>
      {({ workspace, wsId, isPersonal, isRoot }) => (
        <div>
          <h1>{workspace.name}</h1>
          <p>Workspace UUID: {wsId}</p>
          <p>Is Personal: {isPersonal ? 'Yes' : 'No'}</p>
          <p>Is Root: {isRoot ? 'Yes' : 'No'}</p>
        </div>
      )}
    </WorkspaceWrapper>
  );
}
```

### With Loading Fallback

```tsx theme={null}
import LoadingSpinner from '@/components/loading-spinner';

export default async function MyPage({ 
  params 
}: { 
  params: Promise<{ wsId: string }> 
}) {
  return (
    <WorkspaceWrapper 
      params={params}
      fallback={<LoadingSpinner />}
    >
      {({ workspace, wsId, isPersonal, isRoot }) => (
        <MyWorkspaceContent workspace={workspace} wsId={wsId} />
      )}
    </WorkspaceWrapper>
  );
}
```

## Advanced Usage

### With Additional Route Parameters

For pages with additional dynamic segments beyond `wsId`:

```tsx theme={null}
// File: apps/web/src/app/[locale]/(dashboard)/[wsId]/[datasetId]/page.tsx
// URL: /personal/datasets/123, /550e8400-e29b-41d4-a716-446655440000/datasets/456
export default async function DatasetPage({
  params
}: {
  params: Promise<{ wsId: string; datasetId: string }>
}) {
  return (
    <WorkspaceWrapper params={params}>
      {({ workspace, wsId, isPersonal, isRoot, datasetId }) => (
        <div>
          <h1>{workspace.name} - Dataset {datasetId}</h1>
          <p>Workspace UUID: {wsId}</p>
          <p>Dataset ID: {datasetId}</p>
          <p>Is Personal: {isPersonal ? 'Yes' : 'No'}</p>
        </div>
      )}
    </WorkspaceWrapper>
  );
}
```

The `WorkspaceWrapper` automatically passes through `datasetId` (and any other additional params) to the children function.

### Using withWorkspace Helper

For client components that need workspace context, use the `withWorkspace` helper:

```tsx theme={null}
import { withWorkspace } from '@tuturuuu/ui/custom/workspace-wrapper';
import MyClientComponent from './my-client-component';

export default async function MyPage({
  params
}: {
  params: Promise<{ wsId: string }>
}) {
  const { wsId } = await params;

  return withWorkspace(
    wsId,
    MyClientComponent,
    {
      someProp: 'value',
      anotherProp: 42
    },
    <div>Loading workspace...</div>
  );
}
```

### Client Component Receiving Workspace Props

```tsx theme={null}
'use client';

import type { Workspace, WorkspaceProductTier } from '@tuturuuu/types';

interface MyClientComponentProps {
  workspace: Workspace & {
    joined: boolean;
    tier: WorkspaceProductTier | null;
  };
  wsId: string; // Validated UUID
  isPersonal: boolean;
  isRoot: boolean;
  someProp: string;
  anotherProp: number;
}

export function MyClientComponent({ 
  workspace, 
  wsId, 
  isPersonal,
  isRoot,
  someProp, 
  anotherProp 
}: MyClientComponentProps) {
  // Use workspace and wsId safely - they're guaranteed to be valid
  return (
    <div>
      <h1>{workspace.name}</h1>
      <p>Workspace ID: {wsId}</p>
      <p>Personal: {isPersonal ? 'Yes' : 'No'}</p>
      <p>Root: {isRoot ? 'Yes' : 'No'}</p>
    </div>
  );
}
```

## API Reference

### WorkspaceWrapper Props

| Prop       | Type                                                  | Required | Description                                                                           |
| ---------- | ----------------------------------------------------- | -------- | ------------------------------------------------------------------------------------- |
| `params`   | `Promise<TParams>` where `TParams extends BaseParams` | Yes      | The route params containing `wsId` and any additional params                          |
| `children` | `Function`                                            | Yes      | Function that receives `{ workspace, wsId, isPersonal, isRoot, ...additionalParams }` |
| `fallback` | `ReactNode`                                           | No       | Loading fallback component                                                            |

**Note**: The `params` type is generic and supports additional route parameters beyond the base `locale` and `wsId`.

### withWorkspace Parameters

| Parameter   | Type                  | Required | Description                               |
| ----------- | --------------------- | -------- | ----------------------------------------- |
| `wsId`      | `string`              | Yes      | The workspace identifier (legacy or UUID) |
| `Component` | `React.ComponentType` | Yes      | Client component to render                |
| `props`     | `T`                   | Yes      | Props to pass to the component            |
| `fallback`  | `ReactNode`           | No       | Loading fallback component                |

### Children Function Parameters

| Parameter             | Type                                                                  | Description                                                                                                                     |
| --------------------- | --------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------- |
| `workspace`           | `Workspace & { joined: boolean; tier: WorkspaceProductTier \| null }` | Full workspace object, including `joined` status and the resolved product `tier` (`null` when the workspace has no active tier) |
| `wsId`                | `string`                                                              | Validated UUID from `workspace.id`                                                                                              |
| `isPersonal`          | `boolean`                                                             | Whether the workspace is a personal workspace                                                                                   |
| `isRoot`              | `boolean`                                                             | Whether the workspace is the root/internal workspace                                                                            |
| `...additionalParams` | `TParams extends BaseParams`                                          | Any additional route parameters beyond `wsId` (e.g., `datasetId`, `taskId`)                                                     |

**Note**: Additional parameters are automatically passed through from the route params object.

## Migration Guide

### Before (Manual Resolution)

```tsx theme={null}
export default async function MyPage({
  params
}: {
  params: Promise<{ wsId: string }>;
}) {
  const { wsId: id } = await params;

  const workspace = await getWorkspace(id);
  if (!workspace) notFound();

  const wsId = workspace.id; // Manual extraction

  return <MyComponent workspace={workspace} wsId={wsId} />;
}
```

### After (With WorkspaceWrapper)

```tsx theme={null}
export default async function MyPage({
  params
}: {
  params: Promise<{ wsId: string }>
}) {
  return (
    <WorkspaceWrapper params={params}>
      {({ workspace, wsId }) => (
        <MyComponent workspace={workspace} wsId={wsId} />
      )}
    </WorkspaceWrapper>
  );
}
```

## Real-World Example

Here's how the dashboard page uses the WorkspaceWrapper:

<CodeGroup>
  ```tsx theme={null}
  // Example 1: Basic Dashboard Page
  // File: apps/web/src/app/[locale]/(dashboard)/[wsId]/dashboard/page.tsx
  // URL: /personal/dashboard, /550e8400-e29b-41d4-a716-446655440000/dashboard
  export default async function DashboardPage({
    params
  }: {
    params: Promise<{ wsId: string }>
  }) {
    return (
      <WorkspaceWrapper params={params}>
        {({ workspace, wsId, isPersonal, isRoot }) => (
          <div className="p-6">
            <h1 className="text-2xl font-bold mb-4">
              {workspace.name || 'Unnamed Workspace'}
            </h1>
            <div className="grid gap-4">
              <div className="p-4 bg-gray-100 rounded">
                <h2 className="font-semibold">Workspace Details</h2>
                <p>ID: {wsId}</p>
                <p>Type: {isPersonal ? 'Personal' : 'Team'}</p>
                <p>Role: {workspace.role}</p>
                <p>Joined: {workspace.joined ? 'Yes' : 'No'}</p>
                <p>Root: {isRoot ? 'Yes' : 'No'}</p>
              </div>
            </div>
          </div>
        )}
      </WorkspaceWrapper>
    );
  }
  ```

  ```tsx theme={null}
  // Example 2: Settings Page with Loading State
  // File: apps/web/src/app/[locale]/(dashboard)/[wsId]/(workspace-settings)/settings/page.tsx
  // URL: /personal/settings, /550e8400-e29b-41d4-a716-446655440000/settings
  export default async function SettingsPage({
    params
  }: {
    params: Promise<{ wsId: string }>
  }) {
    return (
      <WorkspaceWrapper
        params={params}
        fallback={<div className="p-6">Loading workspace settings...</div>}
      >
        {async ({ workspace, wsId }) => {
          const currentUser = await getCurrentUser();
          const { withoutPermission } = await getPermissions({ wsId });

          return (
            <div className="p-6">
              <h1 className="text-2xl font-bold mb-4">Workspace Settings</h1>

              {withoutPermission('manage_workspace') ? (
                <div className="text-red-600">
                  You don't have permission to manage this workspace.
                </div>
              ) : (
                <div className="space-y-4">
                  <div>
                    <label className="block text-sm font-medium mb-2">
                      Workspace Name
                    </label>
                    <input
                      type="text"
                      defaultValue={workspace.name || ''}
                      className="w-full p-2 border rounded"
                    />
                  </div>

                  <div>
                    <label className="block text-sm font-medium mb-2">
                      Workspace ID (Read-only)
                    </label>
                    <input
                      type="text"
                      value={wsId}
                      readOnly
                      className="w-full p-2 border rounded bg-gray-100"
                    />
                  </div>
                </div>
              )}
            </div>
          );
        }}
      </WorkspaceWrapper>
    );
  }
  ```
</CodeGroup>

## Benefits

1. **Centralized Logic**: All workspace ID resolution logic is centralized
2. **Type Safety**: TypeScript ensures proper workspace object structure
3. **Error Handling**: Automatic `notFound()` when workspace doesn't exist
4. **Loading States**: Built-in Suspense support with custom fallbacks
5. **Cleaner Code**: Reduces boilerplate in page components
6. **Future-Proof**: Easy to modify workspace resolution logic in one place

## Error Handling

The `WorkspaceWrapper` automatically handles common error cases:

* Invalid workspace ID → `notFound()`
* User not authenticated → Redirects to login (via `getWorkspace`)
* Workspace not found → `notFound()`
* User not a member → `notFound()`

## Performance Considerations

* The wrapper uses React Suspense for loading states
* Workspace data is fetched once and passed down to children
* Consider using `fallback` prop for better UX during loading
* The component is optimized for server-side rendering

## Best Practices

1. **Always use the validated `wsId`** from the wrapper, not the original parameter
2. **Provide meaningful fallbacks** for better user experience
3. **Use the `withWorkspace` helper** for client components that need workspace context
4. **Keep workspace logic centralized** - don't duplicate workspace resolution elsewhere
5. **Handle loading states gracefully** with appropriate fallback components

## Troubleshooting

### Common Issues

**Issue**: `notFound()` is called unexpectedly
**Solution**: Ensure the user has access to the workspace and the workspace ID is valid

**Issue**: TypeScript errors with workspace object
**Solution**: Make sure you're using the workspace object provided by the wrapper, not importing it separately

**Issue**: Loading state never resolves
**Solution**: Check that the `getWorkspace` function is working correctly and not throwing errors

### Debug Tips

1. Check the browser's Network tab for failed workspace requests
2. Verify the workspace ID in the URL is correct
3. Ensure the user is authenticated and has workspace access
4. Check the console for any error messages from `getWorkspace`
