proto_channel

Copy Markdown

Typed Protobuf layer over Phoenix.Channel.

Two independently usable pieces that compose:

  • ProtoChannel — a macro that lets a channel declare event ⇄ Protobuf message pairs at compile time, so handlers exchange typed structs instead of raw {:binary, bytes} payloads.
  • ProtoChannel.Serializer — a Phoenix.Socket.Serializer that frames every socket frame as a protobuf Envelope (kinds: PUSH, REPLY, BROADCAST).

Goals

  • A typed-struct boundary inside channels: pattern-match request and reply structs at the edge, get compile-time field-name safety, and let dialyzer type-check handle_proto/3 end-to-end.
  • An all-binary wire format — no JSON, no per-event ad-hoc encoding.
  • Stay small: each piece is opt-in and can be paired with hand-written counterparts.

Installation

def deps do
  [
    {:proto_channel, "~> 0.1.0"}
  ]
end

Docs: https://hexdocs.pm/proto_channel.

Channel usage

defmodule MyAppWeb.MyChannel do
  use ProtoChannel

  alias MyApp.{Request, Response, Notice}

  proto_message "ping", request: Request, reply: Response
  proto_push "notice", Notice
  proto_broadcast "notice", Notice

  @impl Phoenix.Channel
  def join("room:" <> _, _payload, socket), do: {:ok, socket}

  @impl ProtoChannel
  def handle_proto("ping", %Request{} = req, socket) do
    push(socket, "notice", %Notice{text: req.text})
    broadcast(socket, "notice", %Notice{text: req.text})
    {:reply, {:ok, %Response{text: req.text}}, socket}
  end
end

proto_message generates the handle_in/3 clause that decodes the inbound bytes into a %Request{}, dispatches to handle_proto/3, and encodes the reply back to bytes. proto_push and proto_broadcast generate typed wrappers around Phoenix.Channel.push/3, broadcast/3, broadcast!/3, broadcast_from/3, and broadcast_from!/3 — each declared event accepts only its declared struct. To bypass the wrappers, call Phoenix.Channel.push/3 etc. directly.

handle_proto/3 callback

@callback handle_proto(
            event :: String.t(),
            request :: struct(),
            socket :: Phoenix.Socket.t()
          ) :: result()

Supported return shapes:

  • {:reply, {:ok | :error, %Reply{}}, socket}

  • {:noreply, socket} / {:noreply, socket, timeout | :hibernate}

  • {:stop, reason, socket} / {:stop, reason, {:ok | :error, %Reply{}}, socket}

Compile-time validation

Duplicate event names within the same macro family raise ArgumentError. Every referenced module must use Protobuf (verified via __message_props__/0), so typos and accidentally pointing at a plain struct fail at compile time rather than at runtime over the wire.

Serializer usage

socket "/socket", MyAppWeb.UserSocket,
  websocket: [serializer: [{ProtoChannel.Serializer, "~> 2.0.0"}]],
  longpoll: false

Every frame is wrapped in a protobuf Envelope (defined in priv/proto/wire.proto). Payloads must be {:binary, bytes}; empty maps are tolerated for Phoenix's join acks.