Custom Shells & Standalone Composition
The default Desk shell (@framework-m/desk) is a replaceable battery. Its
ShellProvider bootstraps standard services — sidebar navigation, omni-search,
and realtime — and, by default, discovers the backend DocType menu.
Some applications do not want that discovery. A kiosk, a purpose-built inbox, a customer portal, or an embedded widget typically knows exactly which navigation items it needs and should not fire metadata or identity requests it will not use. This guide covers two supported composition recipes:
- Opt out of discovery with
enableMenuDiscovery={false}while keeping the standard shell services. - Compose directly with
ShellContext.Providerwhen you need total control.
1. Opting Out of Backend Menu Discovery
ShellProvider accepts an enableMenuDiscovery prop (default true). When set
to false, the shell skips these discovery-triggered requests:
GET /api/meta/doctypes?show_in_desk=trueGET /api/v1/auth/me(and any subsequent per-DocType authorization checks)
Everything else remains intact: the shell context, user session, omni-search, realtime adapter, and any sections you register explicitly or via plugins.
import { ShellProvider } from "@framework-m/desk";
export function KioskApp() {
return (
<ShellProvider
focusMode={false}
setFocusMode={setFocusMode}
enableMenuDiscovery={false}
>
<MyKioskLayout />
</ShellProvider>
);
}
Authentication is unaffected.
enableMenuDiscovery={false}suppresses only discovery-triggered requests. Native authentication (login, session validation) and server-side authorization continue to work exactly as before. This is not a way to bypass identity checks — it only avoids generating menu and identity requests the standalone shell does not need.
Registering Your Own Sections
With discovery disabled, you remain in full control of navigation. Register
sections from any child of the provider using the useShell() hook:
import { useShell } from "@framework-m/desk";
import { useEffect } from "react";
export function MyKioskLayout() {
const { registerSection } = useShell();
useEffect(() => {
// registerSection returns a cleanup function.
return registerSection({
id: "inbox",
label: "My Inbox",
route: "/inbox",
slot: "sidebar",
module: "Kiosk",
order: 10,
});
}, [registerSection]);
return <Outlet />;
}
Plugin-provided navigation (initialPlugins) is still registered regardless of
the discovery flag, so third-party plugins keep working in standalone shells.
2. Composing Directly with ShellContext.Provider
When you need to provide the shell context yourself — for example, to plug in a
bespoke sidebar implementation or to avoid Desk's convenience provider entirely —
use the exported ShellContext directly. The same context shape is used by
useShell():
import { ShellContext, type ShellContextValue } from "@framework-m/desk";
import { useMemo } from "react";
export function MyCustomShell({ children }: { children: React.ReactNode }) {
const value = useMemo<ShellContextValue>(
() => ({
isMobile: false,
isGuest: false,
rightSidebarOpen: false,
setRightSidebarOpen: () => {},
focusMode: false,
setFocusMode: () => {},
toggleFocusMode: () => {},
registerSection: () => () => {},
getSections: () => [],
registerOmniSearchProvider: () => () => {},
searchOmni: async () => [],
setRealtimeAdapter: async () => {},
subscribeRealtime: () => () => {},
publishRealtime: async () => {},
}),
[]
);
return (
<ShellContext.Provider value={value}>
{children}
</ShellContext.Provider>
);
}
This is the most explicit option: you own every field of ShellContextValue.
It is more integration work than enableMenuDiscovery={false}, but gives you
complete control over what the shell advertises.
If you only need to change navigation behavior, prefer
enableMenuDiscovery={false}— it keeps Desk's realtime, omni-search, and workspace-route wiring working for free.
Choosing a Recipe
| Need | Recipe |
|---|---|
| Default Desk behavior | <ShellProvider> (discovery on) |
| Custom navigation, keep shell services | enableMenuDiscovery={false} |
| Fully bespoke shell/context | ShellContext.Provider |
| Third-party plugins keep working | Either opt-out recipe (plugin sections always register) |