# Hosting a Tableau extension on ext-host

> The three URL spaces (private capability, public, and Tableau Public snapshot), the reserved synthesized paths, and the per-copy license runtime.

Canonical: https://ext.tableauops.com/knowledge/hosting-a-tableau-extension
Updated: 2026-09-18
Author: Eric Summers


A Tableau extension is just a web page Tableau loads in an iframe, so hosting it means putting that page on a real HTTPS URL that Tableau will accept. ext-host does that, plus the parts that are annoying to do yourself: it generates the `.trex`, serves your files with correct MIME types, gives you a one-switch local/hosted dev loop, and answers "is anyone actually using it?" from server-side load counts.

## What hosting gives you

Create an account, create an extension, drop in your `index.html` and assets (or a zip; a single top-level folder is stripped), and download the generated `.trex`. The manifest is built from the extension's settings, so you never hand-write the XML or hand-edit the URL between environments. Files are served straight from storage with the right content types, and every load is counted server-side, because loads happen inside Tableau's embedded browser where ordinary page analytics cannot see them.

## The three URL spaces

An extension can be reached through three different URL shapes, each with different access rules:

| Space | Shape | Who can load it |
|---|---|---|
| Capability | `/x/:token/` | Anyone with the per-grant token. One token per share, revocable at any time. |
| Public | `/u/:username/:slug/` | Anyone, once the extension is marked public. A pretty, tokenless URL. |
| Snapshot | `/w/:token` | A frozen Tableau Public capture: the extension's own code with the Extensions API mocked from recorded data. |

The **capability URL** (`/x/:token/`) is the default. The token is the credential, there is no login, and deleting the share kills every copy of that link at its next load. This is what you send someone privately, and what you revoke when they should no longer have it.

The **public URL** (`/u/:username/:slug/`) exists only for extensions you have explicitly marked public. It needs no per-viewer token, so anyone you point at it can load the extension without a TableauOps account.

The **snapshot URL** (`/w/`) is for Tableau Public, which will not run network extensions at all. It serves the extension's real code with the Extensions API swapped for a mock driven by a captured dataset, so a Web Page object on a Public dashboard can render the viz standalone.

## Reserved synthesized paths

A few paths under the capability and public spaces are not files you upload: the host synthesizes them on request. Do not name a real file any of these:

- `manifest.trex`: the generated Tableau manifest, built from the extension's settings.
- `_exthost.js`: the per-copy runtime client (the license check, below).
- `_preview`: the preview harness shell.
- `_frame`: the entry page with the Extensions API swapped for the mock.
- `_license`: this subscriber's license payload (capability space only).

## Each copy gets its own license runtime

The most important reserved path is `_exthost.js`, the runtime served (never stored) at `/x/:token/_exthost.js`. Your extension imports `license()` from it, and that function fetches `_license` for this specific grant. Two properties make it hold. The fetch is same-origin, and the host sets no CORS headers, so a copy re-hosted on someone else's server is cross-origin and the browser refuses to hand it the license response, breaking a stolen copy for free. And the license is a data dependency, not a boolean gate: the payload carries the thresholds or field mappings the extension genuinely needs, so you cannot simply delete a `if (!licensed) return` line. Revocation is instant, because the license lives on the grant, and deleting the share stops the extension at its next load everywhere.

## FAQ

### What is the difference between the /x/ and /u/ URLs?

`/x/:token/` is a private capability URL: the token is the credential, and revoking the share kills the link. `/u/:username/:slug/` is a public, tokenless pretty URL that only exists once you mark the extension public, so anyone can load it without an account.

### Can I host this on Tableau Public?

Tableau Public does not run network extensions. Use the `/w/` snapshot: the host records a dataset and serves your extension's real code with the Extensions API mocked, so a Web Page object on a Public dashboard renders the viz with frozen sample data. Dashboard filters do not reach it.

### Why does a re-hosted copy of my extension stop working?

The license fetch is same-origin with no CORS headers. Served from ext-host it works; copied to another origin, the browser blocks the cross-origin response, so the extension cannot load its license payload. Because the payload is load-bearing data rather than a deletable flag, the copy breaks.

### Do I write the .trex myself?

No. ext-host generates it from the extension's settings and serves it at the reserved `manifest.trex` path, with the `<source-location>` pointed at the hosted copy. You download it and load it into Tableau.


---
Try it live: Host an extension — https://ext.tableauops.com/login
