Getting started
New here? Start with the walkthrough
From zero to your module in your sandbox takes you through every step in order, from registry access to your module rendering in a sandbox shell. This page is the reference behind it.
This guide takes you from zero to a Tango Vision module running in your own sandbox. Registering a module in the platform registry is a separate, platform-administrator step — see ingest below and See your module in the sandbox shell, which is the path that is open to you today.
What you need
- A developer account — request one. Registration is invite-only while the developer platform is in preview.
- Node 24 (22 also works) and pnpm 10.18.2 — pnpm is the platform standard, and 10.18.2 is the exact version the platform's CI installs. Every scaffolder and template targets it. Corepack, which ships with Node 24, installs it:
corepack enable && corepack prepare pnpm@10.18.2 --activate. - Access to the private registry
https://npm.k8s.tangovision.dev/ - A credential for the sandbox API: either
npx @tv/extension-sdk login(CLI sign-in) at your terminal, or a portal-minted API key for CI
Install the SDK
The @tv/* scope lives on a private registry and does not serve anonymous downloads — an unauthenticated fetch returns 401. Put both the registry and your token in .npmrc:
# .npmrc
@tv:registry=https://npm.k8s.tangovision.dev/
//npm.k8s.tangovision.dev/:_authToken=${TV_NPM_TOKEN}Export your registry token, then install. The token arrives in a separate message from your Tango Vision contact, not in the account email:
export TV_NPM_TOKEN=... # keep it in your shell profile or secret manager
pnpm add @tv/extension-sdkDocker builds
.npmrc with a registry URL but no token is the single most common CI failure — the registry answers 401 to any request without a token, metadata included. In a Dockerfile, mount the token as a build secret rather than baking it into a layer:
RUN --mount=type=secret,id=npm_token \
echo "@tv:registry=https://npm.k8s.tangovision.dev/" > .npmrc && \
if [ -f /run/secrets/npm_token ]; then \
echo "//npm.k8s.tangovision.dev/:_authToken=$(cat /run/secrets/npm_token)" >> .npmrc; \
fi && \
pnpm install --frozen-lockfileWhat's in the package
The SDK ships everything in one package, exposed via subpaths:
| Subpath | What it gives you | Tier |
|---|---|---|
@tv/extension-sdk | Root barrel — re-exports manifest, context, events, api, heartbeat and i18n. Not a union of everything: react, nestjs, testing, permissions, graph and version-check are subpath-only. | @stable |
@tv/extension-sdk/manifest | Manifest schema, validator, scaffolders, checkExposes | @stable |
@tv/extension-sdk/context | PlatformContext runtime types, plus readBuildingSelection(), subscribeBuildingSelection(), isSameBuildingSelection(), and getAccessToken(), authorizationHeader(), resolveAccessToken() for authenticating a module's own API calls (1.15.0+) | @stable |
@tv/extension-sdk/events | Event catalog, envelope, subjects | @stable |
@tv/extension-sdk/api | PlatformApiClient types | @stable |
@tv/extension-sdk/graph | Typed building-graph client — createBuildingGraphClient(), buildingGraphKeys, platformApiFromAxios(). Read + write over storeys, spaces, elements and the changeset lifecycle. | @stable |
@tv/extension-sdk/react | <PlatformProvider>, usePlatformContext(), useBuilding(), useOptionalBuilding(), useCurrentUser(), useSelectedBuildingId(), useBuildingSelection(), <LocationPicker> | @stable |
@tv/extension-sdk/i18n | resolveLocale() and the shell's locale storage keys — follow the host's language without writing your own detector | @stable |
@tv/extension-sdk/nestjs | @ModuleCapability(), @RequiresLicense(), McpSignatureGuard, TvHeartbeatModule | @stable |
@tv/extension-sdk/testing | createMockPlatformContext() | @stable |
@tv/extension-sdk/permissions | extractCatalog(), PermissionCatalog, SubjectEntry | @stable |
@tv/extension-sdk/heartbeat | Liveness reporting to the shell | @experimental |
@tv/extension-sdk/version-check | useVersionCheck(), <UpdatePrompt> — deployed-client update prompts | @experimental |
tv-sdk (bin) | The CLI — see below | mixed |
Four JSON artefacts ship in the package and are importable as subpaths of their own (import schema from '@tv/extension-sdk/manifest.schema.json') or readable straight off disk: manifest.schema.json, permissions.snapshot.json, tv-events.snapshot.json, openapi.snapshot.json.
Every subpath loads from CommonJS as well as ESM
From 1.3.1 each subpath declares require alongside import. Before that, a CJS require of most subpaths typechecked and then threw ERR_PACKAGE_PATH_NOT_EXPORTED at runtime — compile-time green, runtime dead. Module backends are CommonJS, so if you are pinned below 1.3.1, upgrade.
Which tier a symbol carries decides whether it can change under you — see Stability tiers.
The CLI
npx @tv/extension-sdk --helpInvoke the CLI by its full package name
The CLI's bin is named tv-sdk, and inside a project that has the SDK installed npx tv-sdk happens to work. Don't rely on that form: outside such a project npx tv-sdk asks public npm for the name tv-sdk, which is not this SDK. The organisation reserved that name on public npm on 2026-08-29 (version 0.0.1, "Reserved name"), so the old hijack risk is closed — but the reservation is an empty placeholder, so the command still would not run the SDK. Always spell npx @tv/extension-sdk <command>; it resolves through the authenticated @tv:registry you configured above, wherever you run it.
| Command | What it does |
|---|---|
login / logout / whoami | Sign in to the developer realm from the terminal — see CLI sign-in |
init-module <slug> --category=<c> | Scaffold a whole module — the full file list is in Your first module. Outside Tango Vision, add --external --scope=<your-scope> --author="<Your company>", as in the walkthrough |
init-manifest <slug> --category=<c> | Scaffold just a module-manifest.json |
validate <manifest> | Validate a manifest against the schema |
check-exposes <manifest> | Verify the canonical ./Shell federation expose in vite.config.ts |
check-pact <manifest> | Verify your events.subscribes contracts fit the producers' schemas. Repeat --producer=<manifest> for each producing module. |
check-events <snapshot.json> | Fail on breaking changes vs. a committed event-schema snapshot |
snapshot-events <out.json> | Write the event-schema snapshot check-events compares against |
check-api <snapshot.json> | Fail on breaking changes vs. a committed OpenAPI snapshot |
snapshot-api <out.json> | Write the OpenAPI snapshot check-api compares against |
snapshot-permissions <src> <out> | Regenerate the permission catalog snapshot |
write-version | Emit version.json (version + commit + build time) as a postbuild step |
ingest <manifest> | Upsert the manifest into a module registry. Against the platform registry this needs platform.registry:write, held by platform administrators only, so an external developer gets a 403. Against your own sandbox's tv-api (--api=<sandbox apiUrl>, with the token sandbox connect prints) it works: you are the administrator of that one API and database. |
sandbox <subcommand> | Manage sandbox tenants — create, list, connect, extend, reset, delete, and install (requires @tv/extension-sdk 1.14.0 or later) |
init-module, init-manifest, validate, check-exposes, write-version and snapshot-permissions are @stable. The rest — including login and the sandbox family — are @experimental: useful, but the flags may move.
The anatomy of a module
tv-module-hello/
├── module-manifest.json ← the contract
├── src/Shell.tsx ← React, exposed to the shell as "./Shell"
├── src/locales/ ← en.json, ru.json
├── vite.config.ts ← the federation remote
└── package.jsoninit-module generates the frontend only. A backend is optional, a NestJS service you add yourself.
The manifest is the heart of it. It declares your module's id, the permissions it needs, the events it speaks, the Copilot tools it contributes, and where its UI mounts. The platform reads it three times:
- In your CI —
npx @tv/extension-sdk validate module-manifest.json - At publish — the registry rejects an invalid manifest
- At runtime — the Building OS shell composes your module from it
The golden rule
Your module reaches platform state through the SDK, never around it.
No hand-parsed storage keys. No hand-built Keycloak URLs. No hardcoded https://tv-api…. The SDK owns those, so the same code runs in your sandbox and in a customer's production tenant unchanged.
Which SDK call, though, depends on where your code runs — and that catches almost everyone once.
// Inside a federated module. This is the one that works.
import { useSelectedBuildingId } from '@tv/extension-sdk/react';
import { useQuery } from '@tanstack/react-query';
export function WorkOrderList() {
const buildingId = useSelectedBuildingId(); // follows the shell's header selector
return useQuery({
queryKey: ['work-orders', buildingId],
enabled: buildingId !== null,
queryFn: () => fetchWorkOrders(buildingId!),
});
}PlatformContext — usePlatformContext(), useBuilding(), its pre-authenticated api client — travels through React Context, and React Context does not cross a Module Federation boundary. Those hooks throw inside a federated remote, however correctly the shell rendered <PlatformProvider> around it. They are for code running inside the shell bundle, and for your own tests, where you render the provider yourself.
The PlatformContext has the full boundary map: what crosses, what does not, and what to reach for on each side.
Next
→ Your first module builds a working hello-world end to end.
→ Federation and the shell is the one page to read before you touch vite.config.ts. Two of the rules there fail only inside the Building OS shell, where your own CI cannot see them.