The PlatformContext
Everything your module knows about the platform arrives through one object: PlatformContext. The shell injects it; you consume it through React hooks.
The shape
interface PlatformContext {
user: PlatformUser; // who's signed in + their realm roles
organization: PlatformOrganization; // the tenant + its licensed modules
building: PlatformBuilding | null; // active building — null at org level
locale: string; // "en", "ru", "hi"
theme: 'light' | 'dark';
api: PlatformApiClient; // pre-authenticated, tenant-scoped HTTP client
eventBus: EventBusClient; // publish / subscribe to platform events
}Note building is nullable — the user can be at the org level with no building selected. Reach for useBuilding() when your route genuinely requires one and useOptionalBuilding() when it doesn't.
The surface is deliberately narrow. Identity, tenancy, building selection, locale/theme, and two typed clients — that's the whole contract. Anything you reach for beyond it is outside what the platform supports and can change without notice.
Hooks
import {
usePlatformContext,
useBuilding,
useCurrentUser,
useOptionalBuilding,
} from '@tv/extension-sdk/react';
function MyComponent() {
const ctx = usePlatformContext(); // the whole context; throws outside a provider
const building = useBuilding(); // throws if no building is selected
const user = useCurrentUser(); // the signed-in user
const maybe = useOptionalBuilding(); // null instead of throwing
}Re-renders — what subscribing costs you
PlatformContext is a real React context, so the standard worry applies: every consumer re-renders when the provider's value changes. Here that worry is bounded by construction:
- The value's identity is stable — the shell holds one context object and replaces it only when the signed-in user, the selected building, the locale, or the theme changes. All are rare, user-initiated moments; the first three legitimately invalidate everything a module renders anyway.
- A theme flip is a shallow patch, not a rebuild. The shell patches
themeon the context object and keeps everything else — so consumers re-render once with the new value while connections stay up. Modules that color via CSS variables follow the document theme without readingctx.themeat all; read it only for JS-drawn surfaces (canvas, Three.js materials). - High-frequency data never flows through the context value. Telemetry and platform events arrive through
eventBus.subscribe()callbacks — a burst of events re-renders only the components whose own state you update in the handler, not every context consumer. apiandeventBuskeep their identity across theme patches and are replaced only on a full rebuild, so listing them in hook dependencies (as the Events examples do) is correct and doesn't churn.
If you profile a re-render storm in your module, the cause is your own state management downstream of a handler — not the context.
The api client
ctx.api is an HTTP client that's already carrying the user's auth and scoped to the active tenant. You never see a token.
const spaces = await ctx.api.get<Space[]>(`/api/v1/buildings/${building.id}/spaces`);
await ctx.api.post(`/api/v1/buildings/${building.id}/work-orders`, dto);It exposes get / post / put / patch / delete, each taking an optional { headers, query, signal }.
Why this matters: the same component works in your sandbox and in a customer's production tenant, because the only thing that changes between them — the auth + base URL — is supplied by the platform, not your code.
Reading tenancy
ctx.organization.activeModuleIds tells you which modules the tenant is licensed for. Use it to degrade gracefully rather than to gate security — licensing is enforced server-side by @RequiresLicense(), and a client-side check is a convenience, not a boundary.
const { organization } = usePlatformContext();
const hasCafm = organization.activeModuleIds?.includes('@tv/module-cafm');Don't reach around it
The contract is: PlatformContext is your only door. If you find yourself reading localStorage, building Keycloak URLs, or hardcoding https://tv-api..., stop — that path won't survive moving between tenants, and it's outside what the platform supports.
→ Next: Events