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.
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)
- useWorkspace (Client)
- WorkspaceShape
- setActiveAccountFunction (Switching)
- 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.
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:
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.
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
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().
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). |
// Resolve members for the active account server-side (no slug needed).const { data } = await client.rpc('get_active_account_members');Related Topics
- Account API - Personal account data and billing
- Team Account API - Team account operations
- Paths Configuration - Centralized path management