> ## Documentation Index
> Fetch the complete documentation index at: https://tesser.sh/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Share gateway

> Open shared boxes at your own domain, behind Cloudflare Access, for people without the CLI

On Tesserc's cloud, every shared box already answers at
`https://box-<id>.tesser.review/`, and every other port at
`https://box-<id>--<port>.tesser.review/`, with nothing to set up. Only the
org's active members can open these addresses. The first visit sends them to
tesser.sh to sign in with their Tesserc account, and the sign-in lasts 12
hours on that address. Setting up a share gateway with `tesser gateway set`
moves the org's boxes off `tesser.review` and onto your own domain.

A share gateway gives every shared box its own address on your domain, such
as `https://box-<id>.share.example.com/`, that a teammate, a designer, or a
phone opens in a browser with no Tesserc login. The box's first declared port
is served there, and every other port at
`https://box-<id>--<port>.share.example.com/`, so each port is its own site
the way `localhost:<port>` is.

The gateway is a Cloudflare Worker you deploy into your own Cloudflare
account, behind a Cloudflare Access application. Access decides who may open
a box. Each shared box dials the Worker over an outbound websocket, so
nothing reaches into the network the boxes run in, and the Worker holds no
secret.

Opening a sleeping box's address in a browser wakes it, with a page that
shows the progress and turns into the app when it answers. Only a person
opening the page wakes the box. A request from curl or a script gets a 503
response saying the box is asleep, and a page that loads without someone
clicking or typing the address shows a Wake it button instead. Traffic through the gateway
keeps the box awake. A box is served only while it has a live share: `tesser
share <box>` prints the box's gateway address next to the review link, and
`tesser share <box> --revoke`, or the share expiring, takes it off within a
few seconds.

An org running in its own AWS account can run the gateway inside its VPC
instead, behind its VPN; see [Serve shared boxes on your own
domain](https://tesser.sh/docs/self-hosting.md#serve-shared-boxes-on-your-own-domain).

## Pick the host

Boxes hang off one host, `<host>`, and each box takes one label in front of
it. Cloudflare's free certificate covers a zone's apex and one level below
it, so:

* A zone used only for the gateway, such as `examplereview.com`, gives boxes
  `box-<id>.examplereview.com` with the free certificate.
* A host one level down in your main zone, such as `share.example.com`, puts
  boxes two levels deep. Their certificate needs [Advanced Certificate
  Manager](https://developers.cloudflare.com/ssl/edge-certificates/advanced-certificate-manager/)
  with Total TLS on the zone.

## Set it up

You need a Cloudflare zone for `<host>` and [Zero
Trust](https://developers.cloudflare.com/cloudflare-one/) on the same account
(the free plan covers 50 users).

1. Point `<host>` and `*.<host>` at Cloudflare with two proxied DNS records.
   The address does not matter; the Worker answers every request:

   | Type | Name | Content | Proxy |
   | - | - | - | - |
   | AAAA | `<host>` | `100::` | Proxied |
   | AAAA | `*.<host>` | `100::` | Proxied |

2. In Zero Trust, add a self-hosted Access application on `*.<host>` with the
   policies that say who may open shared boxes, such as your company's email
   domain. Leave `<host>` itself outside it: boxes dial in there, and the
   Worker checks their tickets. Copy the application's **audience tag**.

3. Deploy the Worker from a project of its own:

   ```sh theme={"theme":"css-variables"}
   npm install @tesser-sh/gateway
   ```

   ```ts index.ts theme={"theme":"css-variables"}
   export { default, GatewayBoxes } from "@tesser-sh/gateway";
   ```

   ```jsonc wrangler.jsonc theme={"theme":"css-variables"}
   {
     "name": "tesser-gateway",
     "main": "index.ts",
     "compatibility_date": "2026-08-31",
     "compatibility_flags": ["enable_request_signal"],
     "workers_dev": false,
     "routes": [
       { "pattern": "<host>/__tesser/*", "zone_name": "<zone>" },
       { "pattern": "*.<host>/*", "zone_name": "<zone>" }
     ],
     "durable_objects": { "bindings": [{ "name": "BOXES", "class_name": "GatewayBoxes" }] },
     "migrations": [{ "tag": "v1", "new_sqlite_classes": ["GatewayBoxes"] }],
     "vars": {
       "GATEWAY_HOST": "<host>",
       "TESSER_URL": "https://api.tesser.sh",
       "TESSER_ORG": "<org id>",
       "ACCESS_TEAM": "<team>.cloudflareaccess.com",
       "ACCESS_AUD": "<audience tag>"
     }
   }
   ```

   ```sh theme={"theme":"css-variables"}
   npx wrangler deploy
   ```

4. Point the org at it. Only an org admin can:

   ```sh theme={"theme":"css-variables"}
   tesser gateway set --url https://<host> \
     --access-team <team>.cloudflareaccess.com --access-aud <audience tag>
   ```

`https://<host>/__tesser/health` answers `ok` once the Worker is live. Share
a box with `tesser share <box>` and open the gateway address it prints.

## How it works

* **Boxes dial out.** While a box is shared, Tesserc hands it a ticket it
  signs for your gateway, and the box keeps one websocket open to
  `wss://<host>/__tesser/box`. The Worker checks the ticket against
  `https://api.tesser.sh/.well-known/jwks.json`, so a box can only ever
  answer for itself. A revoke tells the box to hang up.
* **One Durable Object per box** holds that socket and carries every request
  and websocket for the box over it, with flow control on bodies in both
  directions.
* **Viewers come through Access.** The Worker checks the
  `Cf-Access-Jwt-Assertion` header on every request against your team's
  keys and the application's audience tag.
* **Waking needs no secret.** When a viewer opens a sleeping box, the Worker
  passes the viewer's Access token to Tesserc, which checks it against the
  team and audience you set and wakes the box.

The dev server sees what it sees on a laptop: each request arrives as
`http://localhost:<port>`, so Vite's `allowedHosts` and Next's
`allowedDevOrigins` need nothing added. The gateway adds a small script to
each page that sends the page's `fetch`, `XMLHttpRequest`, `WebSocket`, and
`EventSource` calls, links, and forms aimed at `localhost:<port>` through the
gateway, and redirects to `localhost:<port>` come back as gateway addresses.
Image, stylesheet, and other resource URLs aimed at `localhost:<port>` are
rewritten the same way, including ones the page's scripts add later.

The ports a page can reach this way are the box's own declared ports plus
the ports it reaches through its dependencies: each `[deps]` port on the box,
and the ports of the boxes behind them, followed down the dependency chain.
That chain only passes through the boxes of the person who shared the box
and the org's shared instances. A port served by another member's own box is
not reachable, and the gateway answers 404 for it. After a `tesser wire`
changes the box's dependencies, the reachable ports update on their own.

Each port's site keeps its own cookies, where `localhost` shares them across
ports, and a third-party sign-in only returns to a gateway address if the
provider allows it as a callback.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.