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:
| Route | Component |
|---|---|
apps/web/src/app/(auth)/login/page.tsx | LoginShell |
apps/web/src/app/(oauth)/oauth/select-account/page.tsx | SelectAccountShell |
apps/web/src/app/(oauth)/oauth/consent/page.tsx | AuthorizeShell |
All three are thin aliases over OpalShaderShell —
apps/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:
| Piece | Why 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 |
OpalOnboardingShader | The Today shader background (full bleed) |
max-w-150 content column | 600 px centered column (Opal design system standard width) |
BeaconProvider | Owns the beacon position store |
OpalNarrativeMarkPoseProvider | Owns the brand-mark pose / animation state |
OpalNarrativeMarkSlot | The 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" mockThat's a familiar error — it just means a real-component import is hitting the mock surface that hasn't grown to cover it yet.
Related
- Auth interaction architecture — what the three surfaces do in the OAuth dance
- Better-Auth quirks — the client-side request injection that fires from these pages
- Workspace → Design system — what
@todayai-labs/opalexports and how the placeholders relate to the rest of the design system
Better-Auth quirks
The `oauthProviderClient` plugin auto-injects `oauth_query` into non-GET request bodies. What that means in practice and the workaround for calls that shouldn't progress the OAuth flow.
Cross-platform headers
X-Client-Platform and X-App-Version are sent on every API request from web, macOS, and Android. The cloud server treats web-client as a first-class platform — no special-casing needed.