> Source: https://nexusatlas.dev/docs/2.0.0/concepts/feed-gateway — Nexus Atlas developer documentation (Concepts). Converted from the HTML page; the page is canonical. This is the 2.0.0 release line; the current line is 3.0.0 (https://nexusatlas.dev/docs/concepts/feed-gateway).

# The feed gateway: one feed out, nothing in shipped · a separate package

An outsider who must watch one mission — a partner, an agency, a client — gets a video or telemetry feed over a channel every browser already speaks, with a token that is theirs, for exactly the feeds it names, until it expires. They never touch the fleet. The feed gateway is a package of its own that sits beside the network: a gateway app at the site, a feed server with its auth hook and player page, and a token issuer. The network contributes one capability flag and one multicast option. The procedure is [Share a feed outside the fleet](https://nexusatlas.dev/guides/feed-gateway).

## The shape

A **gateway** inside the fleet subscribes to a multicast group and reads it off the atlas interface, like any other subscriber. It **publishes outward** — SRT with a passphrase for video (WHIP where the gateway's `ffmpeg` is 8 or newer), an outbound WSS for the telemetry document — to a **feed server**. Viewers connect to the server, never to field equipment: WHEP in the browser for live video, LL-HLS for many viewers or networks that allow only HTTPS, WSS with JSON for positions and events, WSS read-only for raw MAVLink (off by default), RTSP for a partner's own ground-control software. The gateway opens no port; the server never reaches the gateway; a stop is a message the gateway collects over its own outbound connection. One uplink stream per feed, whatever the number of viewers.

## Two modes, one configuration

**Mode 1**: a node that already subscribes — a ground-station laptop with an uplink — runs the gateway app beside `atlasd`. No new record, no new link; the limit is that the laptop is a full member, often an operator. **Mode 2**: a dedicated host joins with its own record flagged `sink`, keeps links to at least two fleet nodes, and has a second leg to the internet that the host forwards nothing to; only the gateway app bridges, outward. Both run the same `feedgate` binary with the same file; what makes mode 2 is on the `atlasd` side.

## What the network adds: the sink flag

A record may carry `sink = true` (`fleet add-node --sink`, `fleet set-caps --sink true`). Peers of a sink accept its handshake, probes, adverts and multicast subscriptions, and drop every data packet that originates from it — unicast or multicast, at the first hop or at any later hop where a relay frame names it as the source — counting `caps.sink_dropped` (and journalling it once a minute per sink), exactly as [classes are clamped per hop](https://nexusatlas.dev/docs/2.0.0/concepts/identity#capabilities). It is never a transit hop whatever its `relay` flag says, no multicast tree is rooted at it, and it signs nothing. The sink holds back what its own TUN offers as well, counted `caps.sink_local_dropped`, so the host's own chatter never reaches its links — and it answers no ping and reaches no dashboard through the tunnel; manage it over its own leg. `fleet check` warns about a sink record that also names classes, `relay = true`, `config_author = true` or `dashboard = "fleet"`: a sink compiles to no relay, no class and no authorship whatever else the record says, and a sink that names no classes gets none. The flag is in the signed record, so it holds fleet-wide, survives the gateway's restart, and one `fleet revoke` removes the host without touching an operational node.

```
$ atlasd fleet add-node --name gw-site1 --node-id 7f0c… --transport-key … --signing-key … \
    --allowed-ip 10.77.0.40/32 --role gateway --sink
$ atlasd fleet set-caps gw-site1 --classes Video      # a mistake: a sink sends nothing
$ atlasd fleet check
  warning: node `gw-site1`: sink = true with classes [Video] — a sink sends no data, so its classes are ignored (write classes = [])
fleet `field-1` seq 3: 5 records, 0 error(s), 1 warning(s), signature unsigned
```

## What the network adds: two parents for the last hop

The distribution tree uses one parent per source and group; a sink may ask for two with `[multicast] redundant_parents = 2`. It advertises the wish in its LSA, and every node, from the same LSDB and the same fleet list, picks the same second parent: among the sink's live two-way neighbours other than its tree parent, one that already carries the stream on the base tree — the source, or a node on some subscriber's path — and may forward, the one closest to the source. That node forks one extra copy to the sink; the sink's reverse-path check accepts either parent and the multipath dedup window drops whichever copy comes second, so a dead parent costs no gap. Nothing else changes: no node is pulled onto the tree for the sink (no such neighbour, no second parent), a sink never forwards so no loop can form, and the only extra copy anywhere is on the second parent's link to the sink. The field is honoured only for a record flagged sink — every peer ignores it for anyone else. The snapshot shows `multicast.redundant_parents`, `redundant_honoured` (the wish took: this node is a sink asking for 2) and `alt_parent_accepted` (packets taken from the second parent, growing once the primary path fails). Cutting the gateway's tree link under live video: one parent loses 1.4–2.4 s of picture; two parents lose no packet, with a largest frame gap of 67 ms at 25 fps.

```
[multicast]
enabled = true
groups = ["239.20.0.1", "239.20.0.2"]      # identical on every node
subscribe = ["239.20.0.1", "239.20.0.2"]
redundant_parents = 2                       # honoured only on a record with sink = true
```

## Tokens, issuers, the kill switch

A token is a JWT-compatible compact JWS signed with Ed25519 (`alg` `EdDSA`): claims `iss`, `sub` (the viewer id), `name`, `feed`, `ch` (the channels: `video`, `telemetry`, `positions`, `mavlink`, `recording`, `rtsp`), `iat`, `exp`, `jti`. A link opens exactly the feeds it names, and nothing else: a single-feed token carries `feed` and `ch`; a multi-feed token carries `feeds` instead, each entry with its own channels, because channels are granted per feed. One token per viewer either way, and revoking it cuts every feed it names. The link is `/view#t=<token>`, the landing page that lists the link's feeds; `/watch/<feed>#t=<token>` opens one feed directly. The token rides the URL fragment, which the browser never sends, so it reaches no server log. The server's auth hook verifies it on every connection (and every HLS request). The server trusts one or more issuer keys: its own local issuer — a console behind a local password or the customer's SSO header from a trusted proxy, and `feedgate-server issue` for offline minting — and any other issuer by public key, whose signed, sequence-numbered revocation list it polls (`revocation_poll_secs`) at that issuer's `GET /api/v1/revocations`; the key itself is published at `GET /api/v1/issuer`. Revocation is per token, per viewer on a feed, or per feed: at the server's own issuer the cut is immediate, from another issuer it lands within the poll. An expired link is cut within about a second of `exp`. The player asks `GET /api/v1/feeds/<feed>/token` whether its link is still accepted whenever the picture stalls, so an expiry, revocation or stop shows at once instead of after the browser's ICE timeout. The player draws the viewer's name, the feed and the time across the picture as an always-on watermark — an overlay in the player, not burnt into the video, which a technically able viewer can strip from their own screen — and states the latency. Recording on the server is optional per feed and off unless turned on.

```
{ "iss": "feeds.nexusatlas.net", "sub": "agency-desk", "name": "Agency Desk",
  "feeds": [ { "id": "uav1", "ch": ["video", "telemetry"] },
             { "id": "dog2", "ch": ["video"] } ],
  "iat": 1792998000, "exp": 1793005200, "jti": "…" }
```

## A code on the link

A link can also require a code, off by default: a six-digit PIN the server generates, or a password the operator types. The code is shown once beside the link and is meant to travel over a different channel than the link. The token then carries `"lock": "pin"` or `"lock": "pw"`, and the media server, the telemetry socket and the token-check endpoint each refuse a locked token on their own. The player first calls `POST /api/v1/unlock` with `{token, code}` and gets back a derived unlocked token: the same claims without `lock`, the same `jti` — so revoking the link still cuts it — and `exp` = min(the original `exp`, now + `unlocked_session_hours`), 12 hours by default. The server keeps only an Argon2id hash of the code, never the code; after five wrong tries the viewer waits with a growing back-off, and after twenty failed codes the link locks until the operator resets it.

## The map of the mesh nodes

With the operator's say-so on both the gateway and the link, the viewer also sees a map of the mesh nodes beside the picture: each node's name and last position with the age of its fix, and the vehicle's last two minutes of track. It takes `share_positions = true` on the feed's gateway and the `positions` channel on the link; both default off, so fleet positions are never shared by accident. On the `/view` page the viewer composes the link's feeds on one screen — side by side, picture-in-picture, a 2×2 grid, or the video beside the map.

## Hosted, or inside the customer's network

The server package is self-contained: one compose file brings up the media server, the auth hook, the local token issuer with its console, the player page and the telemetry fan-out; it needs nothing from `nexusatlas.net`. The hosted instance is `feeds.nexusatlas.net` with a public certificate and the package's own issuer; a customer's instance runs on their hardware with their hostname, a certificate from their own CA, and the issuer keys they choose to trust. The gateway is told which CA to trust with `ca_file`.

## Boundaries

- Inside the fleet a group is read by every relay on its tree, with or without a gateway; a feed meant for outsiders should carry nothing the fleet would not show them.

- No channel carries anything toward the fleet, raw MAVLink included; a partner who needs to fly is a member, not a viewer.

- Tokens are bearer secrets; short expiries, revocation and, where the link requires one, a code sent by another channel are the controls.

- In mode 1 the public side touches a full member — fine for a demo, not for a season.

- A sink sends nothing at all: no ping replies, no backfill gap requests, no dashboard reads through the tunnel. Manage the gateway host over its own leg.

- The second parent must already carry the stream for someone else; Atlas never pulls a node onto the tree for the gateway. Without such a neighbour the gateway has one parent and a dead parent is a re-root of 1.4–2.4 s.

- The watermark is an overlay in the player; per-viewer burn-in would need a transcode per viewer, which the media server does not do.

- The gateway host's "forward nothing between the legs" is an operator step (a sysctl and a forward-drop rule), not something the package enforces.
