Skip to content

funnel

Gathers many HTTP senders into one MoQT track. Senders POST messages to a track’s URL; the funnel orders them into the track, one group each, names each one’s sender from its credential, and publishes the track as MoQT. Each record is committed to a qumo-ledger track before it is published. That ledger is in memory by default, or durable with LEDGER_URI, and it also serves the track’s history. The funnel publishes through a relay it dials as a client, so subscribers reach its tracks on the relay they already use; it can also serve them on a listener of its own. It does not join the relay mesh as a peer.

It suits small, frequent messages from many senders that subscribers want as one stream, such as chat, reactions or presence.

Usage

qumo funnel

Takes no flags or arguments — configured entirely through environment variables.

Example

$ RELAY_URL=moqt://relay:4433 RELAY_SIGNING_KEY=funnel-key.jwk RELAY_PUBLISH=room \
  LEDGER_URI=file:///var/lib/qumo QUMO_AUTH_KEYS=keys.json qumo funnel
	Ingest  : :8090
	Relay   : moqt://relay:4433
	Serve   : none
	Ledger  : file:///var/lib/qumo
	Auth    : keys.json

Without LEDGER_URI, records are kept in memory and are lost when the process exits. Without QUMO_AUTH_KEYS, every sender is accepted. Without RELAY_URL, the funnel serves subscribers itself on :4433.

A sender posts to the track’s URL, /tracks/ followed by the broadcast path and the track name:

$ curl -X POST localhost:8090/tracks/room/123/chat \
    -H "Authorization: Bearer $ALICE" \
    -H "Idempotency-Key: 7f9c2b" \
    -d '{"text": "hello"}'
{"group":"e000001-g00000000","wallclock":1791370000000000000}

$ curl localhost:8090/tracks/room/123/chat?limit=50 -H "Authorization: Bearer $VIEWER"
{"records":[{"group":"e000001-g00000000","wallclock":1791370000000000000,"sender":"alice","payload":{"text":"hello"}}]}

Subscribers consume the track chat of the broadcast /room/123. Any number of senders post into the same track, and a subscriber receives all of them on one subscription.

Requests

EndpointBodyResult
POST /tracks/{broadcast path}/{track name}The record’s payload, one JSON value in UTF-8201 once the record is committed. Creates the track when it does not exist.
PUT /tracks/{broadcast path}/{track name}—Creates the track ahead of its first record, so subscribers can wait on it: 201, or 204 when it exists.
GET /tracks/{broadcast path}/{track name}—A page of records, oldest first: the newest, or those before ?before=<group>, at most ?limit= (default 50, at most 200). before in the answer is the cursor for the next older page. A redacted record keeps its group and wallclock with "redacted": true, and has no sender or payload.
DELETE /tracks/{broadcast path}/{track name}?group=<group>—Redacts that record (qumo-ledger ingest): commits and publishes a redaction, {"redacts": "<group>"}, then deletes the record’s payload. 201 with the redaction’s group, or 204 when the record was redacted already; 400 for a missing or malformed group or a redaction itself, 404 for no such track or group. Without QUMO_AUTH_KEYS it is open to anyone, like every other request.

The last segment of the URL is the track name; the segments before it are the broadcast path.

Each record is stored as {"sender": …, "payload": …}: the payload as it was posted, and the sender its credential names (below). A record whose credential names no sender has no sender.

A record sent with an Idempotency-Key header is stored once per track: a retry with the same key is answered as the first was and stores nothing. A track remembers its 1024 most recent keys.

With FUNNEL_SENDER_LIMIT and FUNNEL_TRACK_LIMIT, records are limited per sender and per track; past either, a record is answered 429 with a Retry-After.

Credentials

With a key set (QUMO_AUTH_KEYS), every request carries a qumo credential as Authorization: Bearer, the same kind the relay admits sessions with and verified against the same key set. Its scopes (auth) name the tracks it reaches:

  • Posting (POST, PUT) needs a post scope matching the broadcast path and the track name. The record’s sender is the credential’s sub, whatever the payload claims; a credential without sub posts with no sender. A publish scope does not post: it sends through a relay, so a credential meant for posting can’t publish into the relay directly, and a publisher’s can’t post.
  • Reading history (GET) needs a fetch scope matching the broadcast path and the track name.
  • Redacting (DELETE) needs a redact scope matching the broadcast path and the track name. The redaction carries the credential’s sub as its sender. No other scope redacts, so a sender’s post credential can’t take anyone’s record out; an app that lets senders redact their own records checks the record and redacts with a credential it keeps to itself. Redactions are published live and kept in history, so a sub there is visible to every subscriber; sign redact credentials without sub to keep moderators anonymous.
{"sub": "alice",
 "scopes": [{"actions": ["post"], "broadcast": {"exact": "room/123"}, "track": {"exact": "chat"}},
            {"actions": ["fetch"], "broadcast": {"prefix": "room/123"}}],
 "iat": 1791370000, "nbf": 1791370000, "exp": 1791370600}

posts into chat of /room/123 as alice, and reads any track of /room/123 and the broadcasts beneath it. It posts into no other track.

A path_auth credential is read as the scopes it amounts to: its sub reads any track at or beneath its path. Its pub is publishing, so it posts nothing.

The credential is checked on every request: once it expires or its key leaves the key set, it is refused. A missing or invalid credential answers 401 with WWW-Authenticate: Bearer, and one that does not cover the broadcast or the track 403.

Publishing through a relay

With RELAY_URL, the funnel dials the relay as a client and announces its broadcasts on that session; the relay subscribes to them when its subscribers do. The session needs a credential that may publish there. The funnel either uses the ?jwt= in RELAY_URL, or, with RELAY_SIGNING_KEY and RELAY_PUBLISH, signs a fresh one for every session, granting publish on RELAY_PUBLISH and every path beneath it, from a key the relay’s key set trusts (qumo auth keygen, ideally confined with -prefix). A credential lasts at most an hour, so one in RELAY_URL admits sessions only until then; a long-running funnel signs its own.

When a session ends, the funnel dials again after two seconds and announces its broadcasts anew.

What a subscriber receives

Each record is one group holding one frame, the record as it was stored, {"sender": …, "payload": …}. A record is sent only after it is committed, in commit order. A group’s sequence is one more than the sequence of the ledger group that stores the record. A new subscriber starts at the track’s latest record.

A subscriber reaches a track once it has been created, by its first record or by a PUT. A subscription to any other track name of the broadcast is refused as not found.

At startup the funnel lists its store once and publishes every track it recorded before, with its latest record, so subscribers reach them across a restart before anyone records again.

Configuration

VariableDefaultDescription
FUNNEL_ADDR:8090HTTP listen address for records and history.
FUNNEL_SENDER_LIMIT(unset)Records each sender may send into a track, as RATE,BURST: per second, and at once, e.g. 1,5. Unset is no limit.
FUNNEL_TRACK_LIMIT(unset)Records a track takes from all senders together, as RATE,BURST, e.g. 20,40. Unset is no limit.
RELAY_URL(unset)The relay to publish through, dialed as a client: https://relay:4433 for WebTransport or moqt://relay:4433 for native QUIC. It may carry a credential as ?jwt=.
RELAY_SIGNING_KEY / RELAY_PUBLISH(unset)A signing key file and the path its credentials grant publishing at, e.g. room. Set together; the funnel signs a fresh relay credential for every session in place of ?jwt=.
RELAY_CA_FILE(unset)PEM certificate to trust as the relay’s root, instead of the system roots.
RELAY_TLS_INSECUREfalseSkip relay TLS verification, for a self-signed dev relay.
FUNNEL_SERVE_ADDR:4433 without RELAY_URL, none with itMoQT listen address for subscribers that dial the funnel directly.
LEDGER_URI(unset)Where records are stored. The scheme selects the backend: file:///var/lib/qumo is a directory (file:ledger for a relative one); postgres://user@host:26257/qumo is a table (ledger_objects, or ?table=) in PostgreSQL or CockroachDB; s3://bucket/prefix?region=… is a bucket of S3, or of an S3-compatible service with &endpoint=http://host:9000, with credentials from AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY; unset or empty is memory. A bare path or any other scheme is an error.
CERT_FILE / KEY_FILEcerts/server.crt / certs/server.keyTLS certificate and key for the funnel’s own MoQT listener.
CORS_ALLOWED_ORIGINS(unset)Comma-separated origins allowed to call the HTTP endpoints from a browser and to open WebTransport (default: same-origin only; * allows any).
QUMO_AUTH_KEYS(unset)The key set senders’ and readers’ credentials are verified against, as the relay reads it: a file, or an https URL. Unset accepts every request, with no sender.
QUMO_AUTH_KEYS_CACHE(unset)A file a downloaded key set is kept in between runs.
QUMO_RELAY_TOKEN(unset)Bearer token sent to a key-set URL.

Limits

  • Plain HTTP. The HTTP listener does not terminate TLS; put it behind something that does, since credentials travel in its requests.
  • Memory is not bounded. With no LEDGER_URI, every record stays in memory until the process exits. Use it for development.
  • One process per track. Records of a track are ordered within one process; two processes recording into the same track are not coordinated.
  • Idempotency keys do not survive a restart. A retry that reaches a restarted funnel is stored again.
  • A relay session ends when its credential does. Subscribers through the relay see the track end and subscribe again when the funnel redials, at most hourly with a signed credential.
  • A quiet track answers late. A subscription to a track with no record yet is answered when the first record arrives.
  • Recorded tracks are not served over HLS. Records carry no duration, which the hls renderers need.

See also

  • relay — the relay the funnel publishes through.
  • rtmp — ingest media over RTMP.
  • hls — the other command built on qumo-ledger.