# Workspace API

> Access the active account (personal or team) in MakerKit. Load account data, role and permissions, subscription status, and the account switcher list, and switch the active workspace.

*Canonical: https://makerkit.dev/docs/tanstack-supabase/api/workspace-api*

---

The Workspace API exposes the **active account** — the personal or team account the user is currently working in. Unlike the URL-slug model, the active account is persisted in the database (`public.user_active_account`) and resolved server-side, so a single API serves both surfaces. It loads the account, the user's role and permissions (for team accounts), subscription status, and the list of accounts the user can switch to.

## Contents

- [fetchWorkspace (Server)](#fetchworkspace-server)
- [useWorkspace (Client)](#useworkspace-client)
- [WorkspaceShape](#workspaceshape)
- [setActiveAccountFunction (Switching)](#setactiveaccountfunction-switching)
- [Underlying database functions](#underlying-database-functions)

## fetchWorkspace (Server)

`fetchWorkspace` is a `GET` server function that resolves the active account for the authenticated user and returns the workspace. The `_authenticated` route loads it in `beforeLoad`, exposes it on the router context, and flows it through `WorkspaceContextProvider`.

```tsx {% title="apps/web/src/routes/_authenticated/route.tsx" %}
import { Outlet, createFileRoute } from '@tanstack/react-router';

import { WorkspaceContextProvider } from '#/components/workspace-context.tsx';
import { fetchWorkspace } from '#/lib/server/active-workspace.functions.ts';

export const Route = createFileRoute('/_authenticated')({
  beforeLoad: async () => {
    const workspace = await fetchWorkspace();

    return { workspace };
  },
  component: AuthenticatedLayout,
});

function AuthenticatedLayout() {
  const { workspace } = Route.useRouteContext();

  return (
    <WorkspaceContextProvider value={workspace}>
      <Outlet />
    </WorkspaceContextProvider>
  );
}
```

The active account is resolved by the `active_account_workspace` Postgres function, which reads the stored pointer, falls back to the personal account when none is set, and self-heals if a stored team membership becomes stale. Because the workspace lives on the router context, nested routes can gate on it in their own `beforeLoad` without re-fetching:

```tsx
beforeLoad: ({ context }) => {
  if (context.workspace.account.is_personal_account) {
    throw redirect({ to: '/settings' });
  }
},
```

## useWorkspace (Client)

Read the workspace in client components with the `useWorkspace` hook. It throws if used outside the `_authenticated` shell where the provider is set up.

```tsx {% title="apps/web/src/components/some-component.tsx" %}
import { useWorkspace } from '#/components/workspace-context.tsx';

function Header() {
  const { account, accounts, user } = useWorkspace();

  if (account.is_personal_account) {
    return <PersonalHeader name={account.name} />;
  }

  return <TeamHeader name={account.name} role={account.role} />;
}
```

## WorkspaceShape

```ts
interface WorkspaceShape {
  // The active account (personal or team) — superset shape.
  account: {
    id: string;
    name: string | null;
    picture_url: string | null;
    slug: string | null; // null for personal accounts
    is_personal_account: boolean | null;
    role: string | null; // null for personal accounts
    role_hierarchy_level: number | null;
    primary_owner_user_id: string;
    public_data: Json;
    subscription_status: SubscriptionStatus | null;
    permissions: AppPermissions[]; // empty for personal accounts
  };
  // The teams the user can switch to (for the workspace switcher).
  accounts: Array<{
    id: string;
    name: string | null;
    picture_url: string | null;
    slug: string | null;
    role: string | null;
    public_data: Json;
  }>;
  user: JWTUserData;
}
```

Use `account.is_personal_account` to branch: for a personal account `role`/`slug` are `null` and `permissions` is empty; for a team account `permissions` lists the current user's permission names (e.g. `account.permissions.includes('billing.manage')`).

## setActiveAccountFunction (Switching)

Switching the active account mutates the DB-backed pointer and re-runs the loaders — it does **not** navigate. The workspace switcher calls `setActiveAccountFunction`, then `router.invalidate()`.

```tsx {% title="apps/web/src/components/dashboard/workspace-dropdown.tsx" %}
import { useMutation } from '@tanstack/react-query';
import { useRouter } from '@tanstack/react-router';
import { useServerFn } from '@tanstack/react-start';

import { useWorkspace } from '#/components/workspace-context.tsx';
import { setActiveAccountFunction } from '#/lib/server/active-workspace.functions.ts';

function useAccountSwitcher() {
  const router = useRouter();
  const setActiveAccount = useServerFn(setActiveAccountFunction);
  const { user } = useWorkspace();

  const switchAccount = useMutation({
    mutationFn: (accountId: string) => setActiveAccount({ data: { accountId } }),
    onSuccess: () => router.invalidate(),
  });

  // Switch to a team by id, or to the personal account with `user.id`.
  return {
    switchToTeam: (accountId: string) => switchAccount.mutate(accountId),
    switchToPersonal: () => switchAccount.mutate(user.id),
  };
}
```

`setActiveAccountFunction` validates that the caller belongs to the target account (or that it is their own personal account) before persisting — the same membership guard is enforced by RLS on `user_active_account`.

## Underlying database functions

The API is backed by SQL functions in `apps/web/supabase/schemas/18-active-account.sql`. You can call these directly from server code when you need the active account outside a loader.

| Function | Returns | Description |
|----------|---------|-------------|
| `active_account_id()` | `uuid` | The active account id; falls back to the personal account when unset or when the stored team membership is stale. |
| `active_account_workspace()` | one row | The active account in the superset shape above. |
| `set_active_account(target_account_id uuid)` | `void` | Validates membership, then upserts the caller's active account. |
| `get_active_account_members()` | rows | Members of the active team account (empty for a personal account). |
| `get_active_account_invitations()` | rows | Invitations of the active team account (empty for a personal account). |

```ts
// Resolve members for the active account server-side (no slug needed).
const { data } = await client.rpc('get_active_account_members');
```

## Related Topics

- [Account API](/docs/tanstack-supabase/api/account-api) - Personal account data and billing
- [Team Account API](/docs/tanstack-supabase/api/team-account-api) - Team account operations
- [Paths Configuration](/docs/tanstack-supabase/configuration/paths-configuration) - Centralized path management
