Skip to content

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

ts
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

tsx
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 theme on 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 reading ctx.theme at 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.
  • api and eventBus keep 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.

ts
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.

tsx
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

Built on the Tango Vision platform. Questions? developers@tango.vision