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.

Installation

The WorkspaceWrapper is available in the web app at @/components/workspace-wrapper.
import WorkspaceWrapper from '@/components/workspace-wrapper';

Basic Usage

Function Children Pattern

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

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

Using withWorkspace Helper

For client components that need workspace context, use the withWorkspace helper:
import { withWorkspace } from '@/components/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

'use client';

interface MyClientComponentProps {
  workspace: Workspace & { role: WorkspaceUserRole; joined: boolean };
  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>User Role: {workspace.role}</p>
      <p>Personal: {isPersonal ? 'Yes' : 'No'}</p>
      <p>Root: {isRoot ? 'Yes' : 'No'}</p>
    </div>
  );
}

API Reference

WorkspaceWrapper Props

PropTypeRequiredDescription
paramsPromise<{ wsId: string }>YesThe route params containing wsId
childrenFunctionYesFunction that receives { workspace, wsId, isPersonal, isRoot }
fallbackReactNodeNoLoading fallback component

withWorkspace Parameters

ParameterTypeRequiredDescription
wsIdstringYesThe workspace identifier (legacy or UUID)
ComponentReact.ComponentTypeYesClient component to render
propsTYesProps to pass to the component
fallbackReactNodeNoLoading fallback component

Children Function Parameters

ParameterTypeDescription
workspaceWorkspace & { role: WorkspaceUserRole; joined: boolean }Full workspace object with user role and joined status
wsIdstringValidated UUID from workspace.id
isPersonalbooleanWhether the workspace is a personal workspace
isRootbooleanWhether the workspace is the root/internal workspace

Migration Guide

Before (Manual Resolution)

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)

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:
// Example 1: Basic Dashboard Page
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>
  );
}

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