The Channels console (the realtime modality’s dashboard) wraps every tab in a shared shell: a top bar with the product mark, an app switcher, a status pill, a theme toggle and an account menu, over a horizontal tab bar. This page documents the shell itself — the parts that stay on screen no matter which tab you are on. The tab bar routes to these paths: Any unrecognised path redirects to /.

What the app switcher reads and what the status pip means

The app switcher is backed by the real realtime.apps.list procedure, called with your orgId. The query only runs once an orgId is available, so the switcher has three pre-app states (see below) before it can show anything. Once the list loads, the switcher shows the active app — the one you last selected, or the first app in the list if you have not selected one. It displays:
  • the app’s name
  • its app_id
  • a coloured pip
The pip is a two-state indicator derived from the app’s status field: Opening the switcher lists every app in the org with the same name / app_id / status triple and the same pip per row. Picking a row changes the active app for the whole shell — every app-scoped tab re-reads it immediately. The selection is shell state, not a URL parameter, so switching apps does not change the path you are on. Next to the switcher, when an app is active, the shell renders a status pill with a globe icon, the app’s app_id in monospace, and the raw status string. The pill is absent entirely when there is no active app. It is the same data as the switcher, surfaced so you can see which app the current screen is talking to without opening the menu.

Organisation scope versus app scope across the tabs

Two scopes coexist in this console, and the distinction explains why some tabs work before you have an app and others do not. Organisation-scoped. The app list itself is org-scoped: it is fetched by orgId, not by app. Apps & keys, Webhooks and Settings are management screens that live at the org level. App-scoped. Stats, Messages and the Event Creator read live engine telemetry or publish through the realtime.* procedures against the currently selected app. Without an active app there is nothing for them to address. Every screen receives the same props from the shell, so each one can decide what to render for itself: Because appsError and appsLoading are passed down rather than handled centrally, a failed app list shows up inside the screen you are on, not as a shell-level banner.

The three pre-app states: session resolving, signed out, no app

The app switcher renders one of three placeholders before it can show a real app. They look similar, so it is worth knowing which is which. “Checking session…” — auth is still resolving. The pip is off and the control is inert. This state exists specifically so that a cold load does not flash the signed-out “Sign in” call to action for a moment before the session hydrates. If you see it persist, the session lookup has not settled yet; nothing is wrong with your apps. “Sign in” — there is no session, or no orgId, so the app list cannot be fetched at all. The switcher becomes a link to /login on the portal, with the subtitle “app list is org-scoped · sign in on the portal” and an external-link icon. It deliberately links out instead of rendering a disabled button, because the portal session is shared across subdomains and signing in there resolves this console too. Separately, on cold load the console fires a portal-session guard before the shell paints. The guard resolves when a session exists — or when it has already attempted the check once in this tab — and otherwise navigates you to the portal. So in practice most signed-out visitors are redirected rather than landing on the “Sign in” switcher. “No app yet” — you are signed in and the org list came back empty. The switcher becomes an internal link to /apps with the subtitle “create one in Apps & keys”. This is the state to fix before Stats, Messages or the Event Creator have anything to show.

Signing in, the shared portal session, and the account menu

Authentication is not owned by this console. The session lives in the portal and is shared with the sibling consoles, which is why both the signed-out switcher and the account menu send you to portal URLs rather than rendering a login form here. The account menu is the circular avatar at the right of the top bar. Its letter is the first character of your signed-in email address, upper cased, or an em dash when no email is known. The button’s tooltip is the full email, or “Account” when there is none. The menu subscribes to auth state changes, so the email updates in place if the session changes in another tab without a reload. Opening it shows:
  • your email, or “Not signed in”
  • the active app as App · <name>, or “No app connected” — the same active app the switcher and status pill reflect
  • Portal hub — a link to the portal root
  • Sign out when signed in, or Sign in (portal /login) when not
Sign out is global. It calls the shared sign-out bridge, which signs you out everywhere rather than only in this console, then redirects to the portal root. The redirect happens regardless of whether the sign-out call itself succeeded, so you are never left sitting on an authenticated shell after clicking it.

Branding in the console shell

Two parts of the top-left mark behave differently under white-labelling. “Channels” is the product name of this console and does not change. It is the name of the realtime modality’s dashboard, not a ClutchCall brand string. The logo mark and the platform chip do change. The shell asks the shared branding helpers for a logo source and a platform name. If a branded logo image is configured it renders that; otherwise it falls back to a generated broadcast mark. The small monospace chip beside “Channels” reads <platform> realtime, where the platform name comes from the same branding lookup. A white-labelled deployment replaces those two through the portal, under Settings → Branding. The theme toggle sits between the docs link and the account menu and flips the shell between dark and light mode. The current mode is also passed down to every screen as the theme prop, so screens can match their own rendering to it.

When a screen fails to render

Every routed screen is wrapped in a single top-level error boundary. If a screen throws during render, you get a fallback card in the content area instead of a white screen:
Something broke rendering this screen Reload, or check Sentry for the details.
with a Reload button that performs a full page reload. Three things follow from how this boundary is wired:
  • The shell survives. The top bar, app switcher, status pill and tab bar are outside the boundary, so they keep working. You can still switch apps, open the account menu, or navigate to another tab.
  • Navigating clears the error. The outlet wrapper is keyed by pathname, which remounts the boundary on every navigation. Moving to a different tab resets the error state without a reload — so if only one screen is broken, the rest of the console remains usable.
  • The error is reported. The boundary captures the exception to Sentry along with the React component stack. Error monitoring is a no-op when no Sentry DSN is configured, and events from this console are tagged with vertical='realtime'.
The fallback only catches render errors from the screens beneath it. A failed data fetch — for example apps.list erroring — is not a render error; it arrives at the screen as the appsError prop and is displayed by that screen.