Skip to content
vxvx
GitHubBlueskydev.toRSS

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.

  1. Deploy the relay.

    Deploy to Cloudflare

    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 as https://vx-dte-server.<you>.workers.dev.

  2. Say who may use it: ALLOW. One entry per workflow, issuer:owner/repo:workflow, space separated. List both the workflow that runs vx run and 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.

  3. 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 ALLOW says so instead: this relay's ALLOW is empty: it admits no run.

  4. 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 } }.


  • 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_id on GitHub, project_id on GitLab): a worker of another repository cannot join it, even one ALLOW admits.
  • 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).
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

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.

Turborepo, Nx and other product names are trademarks of their owners. vx is not affiliated with or endorsed by them.