Getting started
This guide takes you from zero to a registered Tango Vision module.
What you need
- A developer account — register here (self-service)
- Node 22+ and pnpm 10 — pnpm is the platform standard; every scaffolder and template targets it
- Access to the private registry
https://npm.k8s.tangovision.dev/ - A sandbox API key — see Developer account → Get a sandbox API key
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 the token your registration email gave you, then install:
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 | ModuleManifest types + Zod validator + PlatformContext types | @stable |
@tv/extension-sdk/manifest | Manifest schema, validator, scaffolders, checkExposes | @stable |
@tv/extension-sdk/context | PlatformContext runtime types | @stable |
@tv/extension-sdk/events | Event catalog, envelope, subjects | @stable |
@tv/extension-sdk/api | PlatformApiClient types | @stable |
@tv/extension-sdk/react | <PlatformProvider>, usePlatformContext(), useBuilding(), useOptionalBuilding(), useCurrentUser() | @stable |
@tv/extension-sdk/nestjs | @ModuleCapability(), @RequiresLicense() decorators | @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 |
Also published as plain JSON you can consume directly: manifest.schema.json, permissions.snapshot.json, tv-events.snapshot.json, openapi.snapshot.json.
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: no tv-sdk package exists on the public npm registry, so outside such a project npx tv-sdk asks public npm for an unclaimed name — anyone who publishes a package called tv-sdk would get their code executed on your machine. 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 |
|---|---|
init-module <slug> --category=<c> | Scaffold a whole module: manifest, vite.config.ts, src/Shell.tsx, package.json |
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 |
check-events <snapshot.json> | Fail on breaking changes vs. a committed event-schema snapshot |
check-api <snapshot.json> | Fail on breaking changes vs. a committed OpenAPI snapshot |
write-version | Emit version.json (version + commit + build time) as a postbuild step |
ingest <manifest> | Upsert the manifest into the platform registry |
sandbox <subcommand> | Manage sandbox tenants — create, list, connect, extend, reset, delete |
copilot | Interactive AI assistant that scaffolds and wires a module for you |
init-module, init-manifest, validate, check-exposes, write-version are @stable. The rest are @experimental — useful, but the flags may move.
The anatomy of a module
my-module/
├── module-manifest.json ← the contract
├── frontend/ ← React, exposes a federated "Shell"
│ └── src/Shell.tsx
└── backend/ ← optional NestJS service
└── src/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 talks to the platform only through
PlatformContext.
No localStorage. No manual tokens. No hand-built API URLs. The context gives you a pre-authenticated, tenant-scoped API client. This is what lets the same code run in your sandbox and in a customer's production tenant unchanged.
import { usePlatformContext, useBuilding } from '@tv/extension-sdk/react';
import { useQuery } from '@tanstack/react-query';
export function WorkOrderList() {
const { api } = usePlatformContext(); // already authenticated + scoped
const building = useBuilding(); // the active building
return useQuery({
queryKey: ['work-orders', building.id],
queryFn: () => api.get(`/api/v1/buildings/${building.id}/work-orders`),
});
}Next
→ Your first module builds a working hello-world end to end.