Skip to Content

Build a Scope Switcher

The earlier tutorials sent the active scope by hand on each request. In a real app the user picks their tenant once from a scope switcher, and every subsequent request is filtered to that scope automatically. This guide builds one against /api/scope/available, following the component DaaS ships in components/ScopeSwitcher.tsx (mounted in the Studio header).

How the active scope travels

The server resolves the active scope for a request from either of two sources (see lib/scope/extract-resource-uri.ts):

TransportUsed byName
CookieBrowser / Studio UIdaas_resource_uri
HeaderAPI clients, server-to-serverX-Resource-Uri

A browser switcher just needs to set the cookie — the server reads it on the next request. API clients send the header instead. The values are identical: a scope item’s uri_path.

Both transports flow through the same extractor, so everything you proved in the earlier tutorials (inheritance, reject mode, scoped roles) applies identically whether the scope came from the cookie or the header.

Fetch the user’s reachable scopes

Call GET /api/scope/available. It returns the scopes the current user can reach (data), each with a uri_path, scope_type, and a selectable flag, plus a top-level rootAccess boolean. Render only the selectable items — the others are ancestors included for breadcrumb names.

const res = await fetch('/api/scope/available'); const { data, rootAccess } = await res.json(); const options = data.filter((s) => s.selectable !== false);

The switcher’s whole job is to manage the daas_resource_uri cookie. Use a session cookie scoped to the whole site:

const SCOPE_COOKIE = 'daas_resource_uri'; function readScope(): string | null { const m = document.cookie.match(new RegExp(`(?:^|; )${SCOPE_COOKIE}=([^;]*)`)); return m ? decodeURIComponent(m[1]) : null; } function writeScope(uriPath: string | null) { if (!uriPath) { document.cookie = `${SCOPE_COOKIE}=; path=/; max-age=0; SameSite=Lax`; // clear → root } else { document.cookie = `${SCOPE_COOKIE}=${encodeURIComponent(uriPath)}; path=/; SameSite=Lax`; } }

Switch scope and refresh

On selection, write the cookie and reload so every server-rendered query re-runs under the new scope:

function selectScope(uriPath: string | null) { writeScope(uriPath); window.location.reload(); }

A full reload is the simplest correct approach because server components and cached fetches must re-read the cookie. In a pure client-side app, re-fetch your scoped data instead — but make sure outgoing API calls carry the new scope.

Show readable labels with breadcrumbs

A uri_path is /typeId:itemId/typeId:itemId. Turn it into Acme Corp › Sales › North Region by resolving each ancestor prefix against a name map built from the response:

const nameByUri = new Map(data.map((s) => [s.uri_path, s.name])); function breadcrumb(uri: string): string { const segs = uri.split('/').filter(Boolean); return segs .map((_, i) => nameByUri.get('/' + segs.slice(0, i + 1).join('/'))) .filter(Boolean) .join(' › '); }

Offer a root option (when allowed)

If the response has rootAccess: true, show a “Root scope” choice that clears the cookie (selectScope(null)). Users without root access should not see it.

Putting it together

function ScopeSwitcher() { const [options, setOptions] = useState([]); const [rootAccess, setRootAccess] = useState(false); const [current, setCurrent] = useState<string | null>(null); useEffect(() => { setCurrent(readScope()); fetch('/api/scope/available') .then((r) => r.json()) .then(({ data, rootAccess }) => { setOptions(data.filter((s) => s.selectable !== false)); setRootAccess(rootAccess); }); }, []); return ( <select value={current ?? ''} onChange={(e) => selectScope(e.target.value || null)} > {rootAccess && <option value="">Root scope</option>} {options.map((s) => ( <option key={s.id} value={s.uri_path}>{breadcrumb(s.uri_path)}</option> ))} </select> ); }

DaaS already includes a polished version of this — components/ScopeSwitcher.tsx, rendered in the Studio header — with search, an active-item pin, and a one-click “clear scope” button. Use it as a reference, or drop your own into your app.

For API clients

Non-browser clients skip the cookie and send the chosen uri_path as a header on every request:

curl https://your-domain.com/api/items/orders \ -H "Authorization: Bearer <token>" \ -H "X-Resource-Uri: /<tenantTypeId>:<acmeId>/<deptTypeId>:<salesId>"

Next Steps

  • Persist the user’s last scope so it survives a new session (store it server-side or in a longer-lived cookie).
  • Combine the switcher with Scope-Aware Roles & Access Control so the options reflect exactly what each user may access.
  • Scope every tenant collection — see Scope Additional & Related Collections — so switching scope filters your whole app at once.
Last updated on