Your first module
We'll build a minimal module that mounts a page in the Building OS shell and reads data through the platform context. Budget: 30 minutes.
1. Scaffold
Start from the reference module — it's the canonical, known-good shape:
# Clone the platform repo (or just the reference module directory)
git clone git@github.com:tangovision/tv-platform.git
cp -r tv-platform/modules/tv-module-example my-module
cd my-module2. Write the manifest
module-manifest.json:
{
"$schema": "https://npm.k8s.tangovision.dev/@tv/extension-sdk/1.0.0/manifest.schema.json",
"id": "@acme/module-hello",
"name": "Hello Module",
"version": "1.0.0",
"minCoreVersion": ">=2.0.0",
"category": "operations",
"permissions": [
{ "subject": "building.spaces", "actions": ["read"] }
],
"events": {
"publishes": [],
"subscribes": []
},
"ui": {
"routes": [{ "path": "/hello" }],
"navigation": [
{ "label": "Hello", "path": "/hello", "section": "operations" }
]
}
}3. Validate it
npx tv-sdk validate module-manifest.json✓ Manifest is valid (version 1.0.0)If it's wrong, the validator tells you the exact field:
✗ permissions[0].subject: must be one of [building.spaces, building.elements, ...]Wire this into your CI so a broken manifest never leaves your machine:
# .github/workflows/manifest.yml
- run: npx @tv/extension-sdk validate module-manifest.json4. Build the frontend
Your module exposes a federated Shell. Inside it, consume the platform context:
// frontend/src/App.tsx
import { usePlatformContext, useBuilding } from '@tv/extension-sdk/react';
import { useQuery } from '@tanstack/react-query';
export default function App() {
const { api } = usePlatformContext();
const building = useBuilding();
const { data: spaces } = useQuery({
queryKey: ['spaces', building.id],
queryFn: () => api.get(`/api/v1/buildings/${building.id}/spaces`),
});
return (
<div>
<h1>Hello from {building.name}</h1>
<p>{spaces?.length ?? 0} spaces</p>
</div>
);
}No tokens, no URLs you have to assemble. The api client is pre-authenticated and scoped to the active tenant.
5. (Optional) Declare a backend capability
If your module has a NestJS backend:
import { ModuleCapability, RequiresLicense } from '@tv/extension-sdk/nestjs';
@ModuleCapability({ id: 'hello.greeting', version: '1.0.0' })
@Controller('api/v1/buildings/:buildingId/hello')
export class HelloController {
@RequiresLicense('@acme/module-hello')
@Get()
greet() {
return { message: 'hello' };
}
}6. Test against the mock context
import { createMockPlatformContext } from '@tv/extension-sdk/testing';
import { PlatformProvider } from '@tv/extension-sdk/react';
import { render } from '@testing-library/react';
import App from './App';
const ctx = createMockPlatformContext({
user: { ...defaultUser, roles: ['manager'] },
});
render(
<PlatformProvider value={ctx}>
<App />
</PlatformProvider>,
);You test against the exact same context shape production uses — just filled with fake data. No "works on my machine, breaks in prod" surprises.
7. Run it in a sandbox
Before you publish, exercise it against a real (isolated) building:
What you just learned
- The manifest is your contract; validate it in CI.
- The
PlatformContextis your only door to platform state. - The mock context makes tests match production.
- The reference module is the shape to copy.
Next: dig into the PlatformContext and events.