Today Platform Web — Dev Docs
Architecture

OpalShaderShell

Three auth surfaces (`/login`, `/oauth/select-account`, `/oauth/consent`) share one shell that bundles the Opal background, the centered content column, and the beacon plumbing the brand mark needs to paint.

The web app has three auth-adjacent surfaces:

All three are thin aliases over OpalShaderShellapps/web/src/components/app-auth/opal-shader-shell.tsx. The shell exists because writing three independent layouts was the original approach and it left the brand mark broken on two of them.

What the shell bundles

<OpalShaderShell>
  <YourAuthStepComponent />
</OpalShaderShell>

…internally expands to:

<div className='fixed inset-0 bg-[#dcdce0] dark:bg-[#1a1a2e]'>
  <OpalOnboardingShader />
</div>
<BeaconProvider>
  <OpalNarrativeMarkPoseProvider>
    <main className='relative mx-auto max-w-150 ...'>
      {children}
    </main>
    <OpalNarrativeMarkSlot />
  </OpalNarrativeMarkPoseProvider>
</BeaconProvider>

The pieces and what each does:

PieceWhy it's there
bg-[#dcdce0] / bg-[#1a1a2e]Base color matching the (agent) layout sentinel so a subpixel leak at the edges blends instead of contrasts
OpalOnboardingShaderThe Today shader background (full bleed)
max-w-150 content column600 px centered column (Opal design system standard width)
BeaconProviderOwns the beacon position store
OpalNarrativeMarkPoseProviderOwns the brand-mark pose / animation state
OpalNarrativeMarkSlotThe actual <OpalNarrativeMark /> painter

The brand-mark trap

The <OpalNarrativeMark /> placeholders that step components mount are pure geometry. They reserve layout footprint (a fixed-size box where the mark should go) and register their position into a beacon store. They do not paint anything by themselves.

If you mount the placeholder without BeaconProvider + OpalNarrativeMarkSlot in a parent, the slot stays empty and the Today mark never renders. The layout looks right; the mark just doesn't appear.

This is exactly what happened to LoginShell and friends pre-shell — they were stripped-down OpalOnboardingFlow lookalikes that mounted OpalOnboardingShader directly without the beacon plumbing. The result was three auth pages with blank spots where the brand mark should be.

The shell brings the missing pieces in one place.

When to use it

For any new auth-adjacent page: mount it via OpalShaderShell. Mounting OpalOnboardingShader directly is wrong (it gives you the shader but not the brand mark). Wrapping a step component in a custom shell is also wrong (you'd have to re-import all the beacon plumbing).

// In a new page.tsx under (auth)/ or (oauth)/
import { OpalShaderShell } from '@/components/app-auth/opal-shader-shell'
import { MyNewAuthStep } from '@/components/app-auth/my-new-auth-step'

export default function Page() {
  return (
    <OpalShaderShell>
      <MyNewAuthStep />
    </OpalShaderShell>
  )
}

Test mocks

Unit tests for auth / OAuth pages stub @todayai-labs/opal with passthrough fragments for BeaconProvider, OpalNarrativeMarkPoseProvider, OpalNarrativeMarkSlot, plus OpalLoadingOrbs / OpalOnboardingShader testid stubs. The mock setup is in apps/web/src/__tests__/setup.ts (or the per-test vi.mock(...) calls).

When you add a new Opal primitive that gets mounted by an auth-page shell, extend the mocks or the test file fails with:

Error: No "<Name>" export is defined on the "@todayai-labs/opal" mock

That's a familiar error — it just means a real-component import is hitting the mock surface that hasn't grown to cover it yet.

On this page