Skip to content

See your module in the sandbox shell

Your module is a federation remote. It is only really working when a shell loads it, and the shell that loads yours is a sandbox shell: the same Building OS image, deployed separately and bound to your sandbox. Your module never loads into the production shell — module JavaScript runs with the full trust of whatever shell loads it, so third-party code gets a shell of its own.

Since 1.14.0

tv-sdk sandbox install and the dev:remote / serve:remote scripts require @tv/extension-sdk 1.14.0 or later. A module scaffolded by 1.13.0 or earlier has neither script — rescaffold, or add them by hand as shown in step 1.

1. Serve your remote from your machine

@originjs/vite-plugin-federation emits remoteEntry.js in a build, not in the dev server. pnpm dev therefore serves an app with no remote at all, and a shell pointed at it gets a 404 that the browser reports as a CORS failure. Two terminals, two commands:

bash
# terminal 1 — rebuild on every change
pnpm dev:remote     # vite build --watch

# terminal 2 — serve the build output
pnpm serve:remote   # vite preview --port 6001 --strictPort

Your remote is then at:

http://localhost:6001/assets/remoteEntry.js

6001 is the scaffold's pinned preview port (the dev port plus 1000). strictPort is deliberate: without it vite hops to the next free port and moves the remote out from under the registry row that points at it.

If your scaffold has no dev:remote / serve:remote, the equivalents are vite build --watch and vite preview --port 6001 --strictPort, with preview: { port: 6001, strictPort: true, cors: true } in vite.config.ts.

2. Install the module into your sandbox

bash
npx @tv/extension-sdk sandbox install module-manifest.json \
  --sandbox=sbx_... \
  --remote-entry=http://localhost:6001/assets/remoteEntry.js

Two calls against your own sandbox's tv-api, in order:

  1. POST /api/v1/registry/modules/ingest with your manifest and the remote entry URL — this is the registry row the shell renders from.
  2. POST /api/v1/licenses on the seeded building — the shell renders from licence state, so a registered but unlicensed module is simply absent. An existing licence answers 409, which the command treats as success, so it is safe to re-run.

--sandbox=<id> resolves the API URL, the token, the seeded building and the console URL in one lookup. Without it, the command uses the TV_API_URL / TV_API_TOKEN pair that tv-sdk sandbox connect prints.

Both endpoints need platform-administrator rights, which you have inside your own sandbox: the sandbox's tv-api accepts its own token as an administrator of that one API and that one database, and of nothing else (tv-platform#795). If you read an older SDK note saying this call is expected to fail with 401/403, that note predates the change.

Moving where you serve from is a re-run of this command, not a shell rebuild.

3. Open it

<your sandbox host>/sandbox/<your module slug>

The sandbox host is the consoleUrl on your sandbox record (the same host as apiUrl: the shell is served at /, the API stays on /api and /socket.io, so the UI and its backend share an origin). consoleUrl is null when no sandbox shell is deployed for your sandbox.

The sandbox shell mounts one generic /sandbox/:slug route driven by your registry row's remoteEntryUrl, federationName and exposeName — the production shell's /<slug> routes are not involved.

4. Sign in

The sandbox shell authenticates against the sandboxes Keycloak realm. There is no self-service sign-up there: the Tango Vision team creates an account for you in that realm, with the tenant_id claim set to your sandbox's organizationId. Ask at developers@tango.vision and give them the organizationId from your sandbox record.

What to expect, and what not to

  • Use Chrome or Firefox, not Safari. Safari's handling of a page on a public HTTPS origin loading a script from http://localhost blocks the remote. Chrome may ask for local network access permission the first time; allow it.
  • Take your token from the platform context, not from localStorage['access_token']. The sandbox shell runs with the legacy token bridge off: a shell that mirrored a live access token into a key any script can read would be handing it to a stranger's JavaScript. Modules that read that key directly will not authenticate in a sandbox shell; modules that use the SDK's PlatformContext are unaffected. See The PlatformContext.
  • The remote loads only if two separate gates admit it — the browser's CSP and the shell's own origin allowlist. Both are configured to admit localhost in a sandbox shell. A remote served from somewhere else needs both changed, which is a request to us, not a setting on your side.
  • No licence gate on this route, but login still is required: the route sits inside the shell's protected area.
  • Your module's own backend is not deployed by any of this. install registers a frontend remote and licences it. A backend runs wherever you run it, and reaches the sandbox's tv-api over the network like any other client.

The rules for loading a module from localhost

Everything above, as a checklist. If the page is blank, one of these is not true.

  1. Only a sandbox shell loads from localhost. The production shell's Content-Security-Policy does not admit http://localhost, and its generic module route is switched off. A sandbox shell admits loopback origins and mounts any module in your sandbox's registry at /sandbox/<slug>.
  2. Loopback only. http://localhost:<port> and http://127.0.0.1:<port> are admitted. A LAN address, a tunnel URL or any other host is not, however it is reached.
  3. Serve the build, not the dev server. vite dev does not emit assets/remoteEntry.js; a federation remote exists only in a build. Run pnpm dev:remote and pnpm serve:remote side by side.
  4. The URL is fixed: http://localhost:6001/assets/remoteEntry.js. The preview runs with strictPort, so a busy port is an error rather than a silent move to another one, and with CORS on.
  5. The registry row must point at it. tv-sdk sandbox install with --remote-entry=http://localhost:6001/assets/remoteEntry.js ingests the manifest into your own sandbox and grants it a licence on the seeded building. The slug is lowercase kebab-case, at most 64 characters, and the remote exposes ./Shell.
  6. Chrome or Firefox, not Safari. The shell is an HTTPS page loading a script from HTTP loopback; Safari blocks that. Chrome may ask once for permission to reach devices on your local network: allow it.
  7. It loads in your browser only. localhost is your machine. A colleague opening the same URL gets a load error, not your module. To show it to someone else, host the built bundle somewhere the sandbox shell admits and ask us to allow that host.
  8. Authenticate through the SDK, never localStorage['access_token']. The sandbox shell does not write the token there. React context does not cross a federation boundary either, so inside the remote use the hooks that work there, such as useSelectedBuildingId().
  9. Your backend runs on your machine. The shell's policy lets your frontend call http://localhost:<port>, so a local backend works from the browser. The platform cannot reach it: the sandbox has no outbound internet and no route to your machine, so Copilot cannot call an MCP endpoint on localhost.
  10. Share i18next and react-i18next both or neither, and keep the use-sync-external-store/shim alias from the scaffold. Breaking either fails only inside a shell, where your own CI cannot see it.

When the page is blank

What you seeUsual cause
404 on remoteEntry.js, reported as CORSpnpm dev:remote not running, or you started pnpm dev instead — the dev server emits no remote
The remote loads from a different portstrictPort missing; vite moved the port and the registry row still points at 6001
Blank route, no network call for the remoteThe module is registered but not licensed — re-run sandbox install, which does both
Signed in, but every API call is 401The module reads localStorage['access_token']; move it to the platform context
A second React crash on mountThe use-sync-external-store/shim alias is missing — see Federation and the shell

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