ETF Wire Protocol

ETF is Libero’s BEAM-native wire protocol. It uses Erlang’s External Term Format for bytes on the network, then relies on generated code to preserve the typed contract between BEAM and JavaScript.

The important idea is that ETF can encode atoms and tuples, but it does not know which Gleam module a custom type came from. Libero adds that missing identity in generated code before values cross the wire.

Pros And Cons

Pros:

Cons:

Request Envelope

A request envelope is encoded as a three-item tuple:

#(module, request_id, payload)

Where:

Callers that use Libero’s low-level request helpers should call encode_request and decode_request rather than assemble this tuple directly. Rally’s generated protocol modules own their frame shape and use the generated ETF codec module’s encode and decode entrypoints.

Server Frames

Server-to-client messages are framed before the ETF payload:

FrameShape
Responsetag byte 0, 32-bit request ID, ETF payload
Pushtag byte 1, ETF payload

Callers that use Libero’s low-level server-frame helpers should call decode_server_frame and pattern match on the decoded frame. They should not inspect tag bytes or slice request IDs themselves.

Custom Type Identity

Gleam custom types compile to BEAM atoms and tuples. Two modules can both define a constructor named Loaded, and both would have the same bare BEAM tag: loaded. That is not enough identity for a wire protocol.

ETF values therefore use generated wire tags. For each constructor that crosses the wire, Libero computes a 10-character hash from the constructor’s source identity:

module path + constructor name + field types

Generated encoder functions translate the normal BEAM shape into the wire shape:

encode_status(loaded) ->
    'a1b2c3d4e5'.

encode_item({item, Id, Name}) ->
    {'f6a7b8c9d0', Id, Name}.

Generated decoder functions translate the wire shape back into the normal BEAM shape before framework code sees it:

decode_status('a1b2c3d4e5') ->
    loaded.

decode_item({'f6a7b8c9d0', Id, Name}) ->
    {item, Id, Name}.

The hash is opaque on the wire. The generated code knows which source type the hash belongs to, so error messages can still refer to the source-level type.

For the full uniqueness model, see Wire Type Identity.

Built-In Values

ETF preserves BEAM values that JSON cannot represent directly, including atoms, tuples, bit arrays, and the difference between integers and floats.

Libero still needs generated field information for some values. JavaScript, for example, does not preserve the difference between 2 and 2.0. Generated encoders carry field type hints so a whole-number Float can still be encoded as an ETF float.

Safe Decode

Use generated boundary helpers or decode_safe for untrusted ETF input. On the BEAM, safe ETF decoding uses [safe, used] to block new atom creation and reject trailing bytes after the decoded term.

Safe ETF decoding does not by itself limit input size, nesting depth, or every BEAM runtime term class. libero/etf/wire.set_strict_data_terms(True) enables an extra BEAM validator that rejects pids, refs, ports, and functions after decode. It is disabled by default because generated typed decoding already walks legitimate payloads.

libero/etf/wire.set_js_term_depth_limit(512) enables the optional recursive term depth cap in the generated JavaScript ETF decoder. 0 disables the cap, which is the default.

Protocol Helpers

ETF helpers live in libero/etf/wire.

ConceptHelper
Encode an outbound request envelopeencode_request
Decode an inbound request envelopedecode_request
Encode a response frameencode_response
Decode a server framedecode_server_frame
Encode a push frameencode_push
Encode SSR flagsencode_flags
Decode SSR flagsdecode_flags_typed

ETF also exposes lower-level response and push frame decoders for tests and internal callers. decode_server_frame is the unified helper for callers using Libero’s low-level frame API.

Search Document