Skip to main content

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:

  1. Opt out of discovery with enableMenuDiscovery={false} while keeping the standard shell services.
  2. Compose directly with ShellContext.Provider when 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=true
  • GET /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​

NeedRecipe
Default Desk behavior<ShellProvider> (discovery on)
Custom navigation, keep shell servicesenableMenuDiscovery={false}
Fully bespoke shell/contextShellContext.Provider
Third-party plugins keep workingEither opt-out recipe (plugin sections always register)