Skip to content

On-demand sandboxes ​

A sandbox is a Tango Vision tenant of your own: its own PostgreSQL database, a seeded building graph, and a dedicated tv-api stack — at its own subdomain. You develop against it instead of production.

What is isolated, and what is shared ​

"Isolated" is a word worth spelling out, because the interesting question is what a mistake in your sandbox can reach.

Isolated:

  • The database. Each sandbox gets its own database with its own login role, on a dedicated Postgres server that holds no production data. That role cannot open another sandbox's database, nor (tv-platform#822) the server's postgres database or, where the provisioner keeps its state on the same server, that state database.
  • The pod. Your sandbox's tv-api is its own Deployment, in an isolated namespace (dev) separate from the namespace production runs in, with its own Secret holding its own token.
  • The network path to production. NetworkPolicies stand between that namespace and production. The sandbox pod is also configured with the production MQTT broker off (WIFI_SENSING_MQTT_ENABLED=false) and platform sign-up off, set as container env so it cannot inherit otherwise.
  • The shell that loads your module. Third-party module code runs only in a sandbox shell bound to your sandbox, never in the production shell — see See your module in the sandbox shell.

Shared:

  • The Kubernetes cluster. Sandboxes are namespaces on the same cluster as everything else, not a separate cluster. The quotas below exist because of this.
  • One Keycloak realm. Every sandbox authenticates against the single sandboxes realm (never the production realm). Separation inside it is meant to be by the tenant_id claim matching your sandbox's organizationId, not by a realm per developer.
  • The provisioner and its own metadata database, which holds sandbox records and API keys for everybody.

Sign-in is not separated yet (as of 2026-09-26)

Today every sandbox accepts a valid login from the shared sandboxes realm, whichever sandbox the login was issued for: the tenant_id check described above is not enforced yet. A fix that makes each sandbox accept only its own organisation's logins is in progress, and this note goes when it ships. Until then, do not rely on sign-in to keep other sandbox users out of your sandbox, and keep only synthetic data in it.

So: your sandbox's database role cannot read another sandbox's database, and a mistake in it does not reach production. It is not a private cluster, and it is not a security boundary you should test on purpose — that is what the terms ask of you.

Synthetic data only. Put no real company data into a sandbox (tenant lists, floor plans, anything about real people) unless we have agreed it with you in writing. Sandboxes are shared infrastructure, they expire, and they come with no durability guarantee.

No general outbound internet. Sandbox pods cannot make arbitrary outbound calls. One consequence bites early: the sandbox's Copilot cannot reach an external LLM, so AI features that call out to a provider will not work in a sandbox. Develop them against a mock and see Custom LLM endpoint.

Why ​

  • Realism. It's seeded with a parameterized building — mall, office or university — with spaces, elements, and telemetry, so your module renders against real-shaped data.
  • Disposability. Reset it when you've made a mess; it expires automatically.

Create one — from the CLI ​

The SDK wraps the whole lifecycle, so you never have to hand-roll curl. Sign in once (CLI sign-in) and every sandbox command authenticates itself from your OS keyring:

bash
npx @tv/extension-sdk login

npx @tv/extension-sdk sandbox create \
  --name="acme-cafm-dev" \
  --type=mall \
  --storeys=3 \
  --area-sqm=20000 \
  --seed-telemetry-days=14 \
  --ttl-days=30

In CI, or anywhere non-interactive, use a portal-minted API key instead of logging in — an explicit credential always wins over the stored session:

bash
export TV_API_TOKEN=$SANDBOX_API_KEY   # tvk_... key from the portal
npx @tv/extension-sdk sandbox list

The CLI targets https://sandbox-api.k8s.tangovision.dev by default; --api=<url> (or TV_SANDBOX_API_URL) points it elsewhere.

SubcommandWhat it does
createProvision a sandbox. --dry-run generates the seed locally without calling the service. --type=university is valid since 1.14.0 (requires @tv/extension-sdk 1.14.0 or later): the service and the seed templates have always accepted it, but the CLI's own parser rejected it up to and including 1.13.0. On an older CLI, create a university sandbox through the API below.
listList the sandboxes you own
connect <id>Print the sandbox record, then its own tv-api URL and administrator token as TV_API_URL=… / TV_API_TOKEN=… settings for server-side code only (a script, a local backend; never browser code). Do not export them in the terminal you use for sandbox commands: list and create read them as the provisioner address and your key, and would talk to your sandbox instead. SDK 1.15.0 printed them as export lines.
extend <id> --days=NExtend the TTL. --days is required, an integer 1–7.
reset <id>Wipe and re-seed without deleting
delete <id>Delete immediately
install <manifest>Register your module in your own sandbox and licence it, then print the URL to open. Since 1.14.0 (requires @tv/extension-sdk 1.14.0 or later) — see See your module in the sandbox shell.

<id> (and install --sandbox=<id>) is the sandbox id, sbx_…: create prints it, and sandbox list shows the id of every sandbox you own. It is not the slug in the sandbox's URL.

Shaping knobs beyond the basics: --spaces-min / --spaces-max (default 10/30), --equipment-density=sparse|realistic|dense, --occupancy-profile=24x7|office-hours|peak-evenings, --ttl-days=N (1–30, default 14), and --seed=<string> for a deterministic build. Add --format=json to any subcommand to script it. create, list and connect print each sandbox's slug and organizationId in their normal output too (SDK 1.15.1 and later; 1.15.0 showed them only with --format=json), and create ends with the line to send us for sandbox logins.

The subdomain is a slug the service derives as <your-org>-<name, first 16 chars>-<6 random chars>, so it is prefixed with your org and never collides with another org's identically-named sandbox:

https://dev-a1b2c3d4e5f6-acme-cafm-dev-k2x7p9.sandbox.k8s.tangovision.dev

Your org slug is dev-<12 hex characters>: each developer account is an organisation of its own. That is also the boundary for "your" sandboxes and the unit the quotas below are counted against. Two developers who work on one sandbox: see Two developers, one sandbox.

Create one — from the API ​

bash
curl -X POST https://sandbox-api.k8s.tangovision.dev/api/v1/sandboxes \
  -H "Authorization: Bearer $SANDBOX_API_KEY" \
  -H "content-type: application/json" \
  -d '{
    "name": "acme-cafm-dev",
    "buildingType": "mall",
    "storeys": 3,
    "areaSqm": 20000,
    "ttlDays": 14
  }'

Response — 202 Accepted. Provisioning runs in the background (a database, a seeded building graph, and a dedicated tv-api stack take a few minutes at realistic parameters), so the record comes back in provisioning status:

json
{
  "sandboxId": "sbx_...",
  "name": "acme-cafm-dev",
  "slug": "dev-a1b2c3d4e5f6-acme-cafm-dev-k2x7p9",
  "status": "provisioning",
  "apiUrl": "https://dev-a1b2c3d4e5f6-acme-cafm-dev-k2x7p9.sandbox.k8s.tangovision.dev",
  "consoleUrl": null,
  "apiToken": "...",
  "organizationId": null,
  "buildingId": null,
  "createdAt": "2026-05-30T...",
  "expiresAt": "2026-06-13T...",
  "extensionsUsed": 0,
  "extensionsRemaining": 2,
  "failureReason": null
}

apiToken is the sandbox's own credential — keep it; it authenticates as an administrator of this sandbox's API, and of nothing else. organizationId and buildingId stay empty until provisioning finishes.

consoleUrl is the UI bound to this sandbox, and it is null unless a sandbox shell is deployed. When one is, it is the sandbox host itself: the shell is served at / and the API stays on /api and /socket.io, so the UI and its backend share an origin. A sandbox shell is a separate deployment from the production one on purpose — module code has full trust in the shell that loads it, so your module runs only in a shell bound to your own sandbox.

organizationId is the sandbox's tenant id. Every sandbox shares one Keycloak realm, and a user is meant to sign in to your sandbox only if their tenant_id claim equals this value (not enforced yet: see the note under What is isolated, and what is shared). It is what an operator needs in order to wire a login up, and it is unique per sandbox rather than per sandbox name.

Poll the detail endpoint until status leaves provisioning:

bash
curl -H "Authorization: Bearer $SANDBOX_API_KEY" \
  https://sandbox-api.k8s.tangovision.dev/api/v1/sandboxes/sbx_...

The five statuses are provisioning, ready, failed, expired and deleted. ready means the sandbox is usable (buildingId and the final apiUrl are filled in); failed carries a failureReason and is terminal. The CLI (sandbox create) polls for you every 5 seconds and exits when the sandbox is ready — or gives up after 15 minutes, at which point provisioning carries on server-side and sandbox list will show you the result.

Lifecycle ​

ActionEndpointReturns
ListGET /api/v1/sandboxes200 { sandboxes: [...] }
Read oneGET /api/v1/sandboxes/:id200 — this is the one you poll
CreatePOST /api/v1/sandboxes202, status provisioning
Extend TTLPOST /api/v1/sandboxes/:id/extend, body { "days": 7 }201
ResetPOST /api/v1/sandboxes/:id/reset201, or 409 if not ready
DeleteDELETE /api/v1/sandboxes/:id204, idempotent

days on extend is required and must be an integer 1–7; omitting it is a 400. Note that this is a different bound from --ttl-days at create (1–30), so --days=14 is rejected.

What reset does

It drops and recreates the sandbox's database, runs the same migrate-and-seed step as create with the parameters the sandbox was created with, and restarts the sandbox's API onto the new database — so you get a fresh building graph under the same id, URLs and token. The building id changes, because the graph is regenerated. Expect the API to be unavailable for a minute or so while it restarts.

If the re-seed fails, the sandbox moves to failed rather than staying ready with an empty database; delete it and create a new one.

Quotas ​

To keep the shared cluster healthy:

  • 3 concurrently-active sandboxes per org (provisioning and ready count; expired and deleted do not)
  • 2 TTL extensions per sandbox, of up to 7 days each
  • 30 days maximum TTL at create — so 44 days is the effective ceiling for one sandbox

Nothing reminds you before a sandbox expires, and an expired sandbox cannot be extended: its database and host are deleted. Note the expires date at create, and extend in time.

modules is not accepted at create time: a non-empty array is a 400 with the code MODULES_NOT_SUPPORTED. It used to be accepted and quietly thrown away, so a sandbox came up without the modules you asked for and nothing said so. Install a module into a sandbox once it is ready, with sandbox install (SDK 1.14.0 or later). The error's own message still says that command is not released; that text is out of date.

Breaching a quota is a 403 carrying a machine-readable code — QUOTA_CONCURRENCY when you are at the concurrent limit, QUOTA_EXTENSIONS when a sandbox has used both extensions. Malformed request bodies are a 400; unknown fields are rejected rather than ignored.

What's inside a fresh sandbox ​

  • An organization + site + building seeded by @tv/sandbox-seed (a private package; its BuildingType enum is exactly mall | office | university)
  • Storeys, spaces, elements, and points sized by your parameters
  • Up to 30 days of backfilled telemetry (configurable via seedTelemetryDays)
  • The standard tv-api schema, so every platform endpoint works

Your module calls the sandbox's tv-api the same way it calls production's: relative /api/… paths, with authorizationHeader() from @tv/extension-sdk/context on each request — see Calling the API from a module. Structurally, the sandbox is production.

For Tango Vision staff: the console ​

Staff can also create sandboxes from the staff console, which needs a staff sign-in; invited developers use the CLI above. Its Sandboxes page (under the admin nav) takes an API key, then New sandbox:

FieldMeaning
NameDisplay name; the subdomain is derived from it
Building typemall, office or university
Storeys1–50
Area (m²)Total gross area
TTL (days)1–30; auto-cleanup after

The row starts as provisioning and flips to ready once the database is seeded — from under a minute for a small building to a few minutes at realistic parameters.

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