DTE server
@vzn/vx-dte-server is the relay a distributed run
needs: CI runners take no inbound connection, so the run and its workers
all dial out to it, and it passes their messages on. You host your own.
It runs on Cloudflare’s free plan, or on Bun or Docker anywhere your CI
can reach. It keeps no secret and stores no file.
-
Deploy the relay.
The button copies the relay into a new repository of yours and deploys it. It asks for one var,
ALLOW(step 2). The relay’s URL is the Worker’s, such ashttps://vx-dte-server.<you>.workers.dev.From
packages/vx-dte-serverin a checkout of the vx repository, withALLOWfilled inwrangler.jsonc’svarsfirst:Terminal window bun installbunx wrangler loginbunx wrangler deployFrom a checkout of the vx repository:
Terminal window bun packages/vx-dte-server/src/bun.ts \--allow https://token.actions.githubusercontent.com:acme/app:ci.yml \--allow https://token.actions.githubusercontent.com:acme/app:vx-worker.yml \--audience https://relay.example.com --port 8080--audienceis the relay’s public URL. Put it behind HTTPS.Terminal window docker build -t vx-dte-server packages/vx-dte-serverdocker run -p 8080:8080 vx-dte-server \--allow https://token.actions.githubusercontent.com:acme/app:ci.yml \--allow https://token.actions.githubusercontent.com:acme/app:vx-worker.yml \--audience https://relay.example.comOr run it with the other two servers using Docker Compose: the
dteservice, on port 8080, withDTE_ALLOWandDTE_AUDIENCE. -
Say who may use it:
ALLOW. One entry per workflow,issuer:owner/repo:workflow, space separated. List both the workflow that runsvx runand the workers’ workflow; every other token is refused.wrangler.jsonc "vars": {"ALLOW": "https://token.actions.githubusercontent.com:acme/app:ci.yml https://token.actions.githubusercontent.com:acme/app:vx-worker.yml"}- GitHub Actions:
https://token.actions.githubusercontent.com:acme/app:ci.yml - GitLab:
https://gitlab.com:group/project:.gitlab-ci.yml
On Bun and Docker each entry is one
--allow. GitLab names a project by its full path,group/subgroup/project. - GitHub Actions:
-
Check it answers. A request with no token is refused by name:
Terminal window curl https://relay.example.com/v1/sessions/$(uuidgen)/socket{"error":"no bearer token"}A Cloudflare relay with an empty
ALLOWsays so instead:this relay's ALLOW is empty: it admits no run. -
Name it in
dte()..vx/workspace.ts dte({relay: 'https://relay.example.com',workers: { small: { max: 8, cpu: 4, memory: '16GB' } },adapter: github(),})Each side proves itself with its job’s OIDC token for the relay’s URL, so no secret is stored. On GitHub Actions both workflows need
permissions: { id-token: write }; on GitLab the orchestrator and worker jobs ask for one:id_tokens: { VX_DTE_TOKEN: { aud: https://relay.example.com } }.
What to know
Section titled “What to know”- The relay carries orders and status, never files: outputs go through your remote cache.
- On Cloudflare each run is one Durable Object, and the relay fits the free plan: websocket messages count 20 to one Durable Object request, against 100,000 a day, and it writes no row. A deploy, or a restart on Bun and Docker, forgets the runs in flight: vx-dte joins each again and the work goes on.
- The wire is Switchboard v1, an open protocol: any server that speaks it can stand in for this relay.
AUDIENCE(Cloudflare) is unset by default: tokens must name the URL the relay is reached at. Set it only when they name another one.- A run is bound to the issuer and repository that started it
(
repository_idon GitHub,project_idon GitLab): a worker of another repository cannot join it, even oneALLOWadmits. - A participant whose socket drops may dial back within 10 s. A run ends when the orchestrator leaves, or drops and does not dial back in time.
- On Bun, 10 000 sockets over 100 runs take 41 MB over an idle relay (measured 2026-10-24, 4 cores).
Settings
Section titled “Settings”| Setting | Cloudflare | Bun and Docker | What it sets |
|---|---|---|---|
| who may take part | ALLOW var |
--allow, repeated |
issuer:owner/repo:workflow entries |
| audience | AUDIENCE var, unset: the relay’s URL |
--audience |
the aud every token must name |
| listen | --port (8080), --host (0.0.0.0) |
where it listens |
Point vx at it: dte()
Section titled “Point vx at it: dte()”dte({ relay, workers, adapter }) from @vzn/vx-dte sends the run’s
tasks to workers your CI starts. The adapter starts them: github(),
gitlab(), docker(), or local() (workers in git worktrees, to try it
on one machine). vx dte init writes the run’s workflow and the
workers’ (ci.yml and vx-worker.yml on GitHub). The
distributed runs page walks through a run.
The routes are Switchboard v1: one websocket per
participant, GET /v1/sessions/<session>/socket, with the subprotocol
switchboard.v1 and the job’s OIDC token as a bearer token. To mount the
relay in a server of your own, createRelay() returns a fetch handler,
serveRelay() serves it on Bun.serve, and createVerifier() checks the
CI tokens against parseAllow(ALLOW). The
README
lists the whole API.