The layout chain

A studio page such as /{slug}/clients renders through four layouts.

Root layout

app/layout.tsx is an async server component.
  • Reads the locale with getLocale() and the messages with getMessages() from next-intl.
  • Sets lang, dir (from getLocaleDirection(locale)), translate="no" and the notranslate class on html. The metadata also sets google: notranslate, so the browser does not offer to translate the Hebrew UI.
  • Loads two Google fonts through next/font: Figtree as --font-sans for body text and Heebo as --font-heebo for headings. Heebo carries Hebrew glyphs, Figtree does not.
  • Imports globals.css and the cropper stylesheet.
Providers, outermost first:
ClientProviders hardcodes Direction.Provider dir="rtl". The html element follows the locale, but Radix primitives that read direction from this provider are told the app is always right to left. This matches the Hebrew default and is wrong for the four left to right locales.
Two progress bars are mounted: NextTopLoader and the @bprogress provider. modules/shared/hooks/router.ts re-exports useRouter from @bprogress/next/app, and some components import useRouter from nextjs-toploader/app instead. Both wrap the Next router so programmatic navigation shows progress.

Authenticated layout

(authenticated)/layout.tsx exports dynamic = "force-dynamic" and revalidate = 0.
  1. getSession(). No session redirects to /login.
  2. Gets the request scoped QueryClient with getServerQueryClient().
  3. Prefetches the session under sessionQueryKey and the organization list under organizationListQueryKey.
  4. Wraps children in a HydrationBoundary and these providers:
The last two are described in Shared components.

Main layout

(authenticated)/(main)/layout.tsx renders children unchanged. It only decides whether the user may enter. It runs userHasPassword() and getOrganizationList() in parallel, then applies the redirects listed in Route tree. userHasPassword() returns true when the accounts call fails, so an API hiccup never traps a user on /set-password.

Account layout

(main)/(account)/layout.tsx picks the organization matching session.session.activeOrganizationId, or the first one, and loads its appearance so the account pages keep the studio’s colours and logo. With no organization it uses DEFAULT_STUDIO_APPEARANCE.

Organization layout

(main)/(organizations)/[organizationSlug]/layout.tsx:
  1. getActiveOrganization(slug). A missing organization or a non member returns notFound().
  2. Prefetches that organization under activeOrganizationQueryKey({ slug }) and wraps the subtree in its own HydrationBoundary. This is what makes the sidebar render the right studio on the first paint. See Data fetching.
  3. getStudioAppearance(slug) returns branding (parsed from the studio record) and calendarEnabled.
  4. Reads the perform-nav-collapsed cookie.
  5. Renders OrgTheme and AppWrapper.

OrgTheme

A server component in modules/shared/components/OrgTheme.tsx. It turns the studio’s Branding into a Google Fonts link and a style tag built by brandingCss() in modules/shared/lib/branding.ts. The style overrides design tokens at :root and .dark, so the studio colour reaches every component, including Radix portals.

AppWrapper

A client component in modules/shared/components/AppWrapper.tsx. It renders NavBar and the scrolling main element, and owns the sidebar open state.
  • The collapse preference is a cookie (NAV_COLLAPSED_COOKIE, value 1 when collapsed, one year). The server reads it and passes initialNavCollapsed, so the first paint has the right width.
  • Settings routes (settings, coaches, branding) always start expanded. A manual collapse there lasts until the user leaves the section.
  • Canvas routes start collapsed regardless of the preference: paths ending in /automations or /assistant, and any path containing /checkin/.
  • Board routes (/clients, /workboard) make main a full height flex column that scrolls inside itself.
  • NavExpandedProvider exposes the state through useNavExpanded(). The plan builders pin their own chrome to the viewport and need it to stay aligned with the sidebar.
The width classes live in one place, modules/shared/lib/nav-menu.ts: NAV_WIDTH_CLASS, NAV_OFFSET_CLASS and NAV_INSET_CLASS. The collapsed rail is 76px. Change all three together.

Other layouts

(authenticated)/not-found.tsx renders the 404 inside AppWrapper, so a wrong URL still shows the sidebar.

Page chrome

Two generations of page header are in use. PageContainer uses a border only. In page containers do not carry shadows. Floating layers such as dialogs and popovers keep theirs.

Things that exist but are not mounted

modules/shared/components/Topbar.tsx renders NotificationCenter, the bell with the unread count. No layout or page imports Topbar, so the notification center is not on screen in the current code.