No description
Find a file
2026-07-17 09:35:07 +02:00
crates implement chat server 2026-07-16 21:56:53 +02:00
examples use chrono for DateTime 2026-07-17 09:35:07 +02:00
.gitignore initial commit 2026-07-13 15:41:46 +02:00
.rustfmt.toml initial commit 2026-07-13 15:41:46 +02:00
Cargo.lock use chrono for DateTime 2026-07-17 09:35:07 +02:00
Cargo.toml implement chat server 2026-07-16 21:56:53 +02:00
LICENSE Initial commit 2026-07-13 13:11:46 +00:00
README.md create bevy plugin as well as examples 2026-07-16 16:49:05 +02:00

phoenix-rs

A Rust client for Phoenix Framework Channels over WebSockets.

Overview

phoenix-channel connects to a Phoenix server's channel endpoint, handles the join/leave lifecycle, heartbeats, and message serialisation — so you can focus on your application logic instead of the wire protocol.

It is async-first (Tokio), supports TLS through either rustls or native-tls, and reconnects automatically with exponential backoff. The API supports both strongly typed event streams and raw JSON events.

Workspace

Crate Description
phoenix-channel Core library
phoenix-channel-macros Derive macro for channel Events
phoenix-channel-bevy Bevy plugin and message bridge
simple Runnable async usage example
bevy-example Runnable Bevy usage example
bevy-web-example Browser Bevy usage example

Installation

Add to your Cargo.toml:

[dependencies]
phoenix-channel = { git = "https://git.ganz.gay/jakob/phoenix-rs" }

TLS features

Feature Backend Default
rustls rustls + native roots
native-tls OS TLS stack

To opt into native-tls:

phoenix-channel = { git = "https://git.ganz.gay/jakob/phoenix-rs", default-features = false, features = ["native-tls"] }

WebAssembly

phoenix-channel supports browser WebAssembly through the browser's native WebSocket and timer APIs. Native builds continue to use Tokio and tokio-tungstenite; no API changes or feature flags are required in application code.

Use wasm-bindgen, Trunk, or another browser WebAssembly bundler to expose your application's entry point. Browser security rules still apply: a page served over HTTPS must connect with wss://, TLS is handled by the browser rather than the crate's TLS features, and the server must accept the page's WebSocket origin.

Usage

use phoenix_channel::{ChannelEvent, ClientBuilder, PushResponse};
use serde::Deserialize;

#[derive(Debug, Deserialize, ChannelEvent)]
#[channel(event = "new_msg")]
struct NewMessage {
    body: String,
    user: String,
}

#[tokio::main]
async fn main() -> phoenix_channel::Result<()> {
    // Building starts the background connection task.
    let client = ClientBuilder::new()
        .url("ws://localhost:4000/socket/websocket")
        .build()?;

    // This waits for the server to accept the join.
    let channel = client.channel("room:lobby", serde_json::json!({})).await?;

    // Subscribe before the event can arrive; subscriptions do not replay old events.
    let mut events = channel.on::<NewMessage>();
    tokio::spawn(async move {
        while let Ok(event) = events.recv().await {
            println!("{}: {}", event.payload.user, event.payload.body);
        }
    });

    // Push an event and deserialize the server's response.
    let response: PushResponse<serde_json::Value> = channel
        .push(
            "new_msg",
            serde_json::json!({"body": "hello from rust", "user": "bot"}),
        )
        .await?;
    println!("push result: {:?}", response.response);

    // Leave cleanly before stopping the connection task.
    channel.leave().await?;
    client.disconnect().await?;

    Ok(())
}

The simple example crate contains a runnable client with structured logging and multiple typed subscriptions. Run it against a Phoenix endpoint at the URL used in that file:

RUST_LOG=info cargo run -p simple

The same example runs in a browser with Trunk:

cargo install --locked trunk
rustup target add wasm32-unknown-unknown
cd examples/simple
trunk serve --open

It connects to ws://localhost:4000/socket/websocket by default and writes events to the browser developer console. Override the endpoint at build time when needed:

PHOENIX_SOCKET_URL=wss://example.com/socket/websocket \
  trunk serve --open

When Trunk and Phoenix use different origins, configure the Phoenix socket's check_origin list to accept the Trunk development server (normally http://localhost:8080).

Bevy

The phoenix-channel-bevy crate makes a joined channel available as a Bevy resource and forwards registered typed channel payloads through Bevy's message system:

App::new()
    .add_plugins(PhoenixChannelPlugin::new(channel))
    .add_phoenix_event::<NewMessage>()
    .add_systems(Update, read_messages)
    .run();

See examples/bevy for a complete headless Bevy app. Run it with:

cargo run -p bevy-example

The separate examples/bevy-web example uses Bevy's browser schedule runner and the browser-compatible phoenix-channel transport. Run it with Trunk:

cd examples/bevy-web
trunk serve --open

Set PHOENIX_SOCKET_URL before starting Trunk to override the default endpoint. Pages served over HTTPS must connect to a wss:// endpoint.

Socket authentication

Socket-level parameters are added to the WebSocket handshake URL. Phoenix's vsn=2.0.0 parameter is always included.

use phoenix_channel::ClientBuilder;

fn authenticated_client() -> phoenix_channel::Result<phoenix_channel::Client> {
    ClientBuilder::new()
        .url("wss://example.com/socket/websocket")
        .connect_params(serde_json::json!({ "token": "signed-token" }))
        .build()
}

Raw events

Typed events are convenient when the payload schema is stable. For dynamic event names or payloads, subscribe to the raw stream instead:

async fn receive(channel: &phoenix_channel::Channel) {
    let mut events = channel.subscribe_raw();

    while let Ok(event) = events.recv().await {
        println!("{}: {}", event.event, event.payload);
    }
}

Key Types

Type Description
ClientBuilder Configures and builds a Client
Client Connection handle — join channels, disconnect
Channel Joined channel — push messages, subscribe to events, leave
ChannelEvent Trait associating a payload type with its Phoenix event name
EventEnvelope A typed inbound payload and its Phoenix message metadata
PushResponse<T> Typed server reply to a pushed message
Error Unified error type

Lifecycle and reconnection

ClientBuilder::build
        │
        ▼
connect WebSocket ─── failure ──▶ wait with backoff ──┐
        │                                             │
        ▼                                             │
send phx_join                                         │
        │                                             │
        ▼                                             │
      Joined ◀──── reconnect and rejoin ◀─────────────┘
        │
        ├── heartbeat every 30 s
        ├── push and receive application events
        └── leave() ──▶ Closed

Unexpected disconnects trigger exponential backoff with light jitter, capped at 32 seconds by default (1 s → 2 s → 4 s → … → 32 s). Active joined channels are rejoined after the socket reconnects. A normal WebSocket close does not reconnect.

Pushes, joins, and leaves wait up to 10 seconds for a Phoenix reply. push_no_reply skips that wait; it confirms only that the message was queued locally, not that the server accepted it.

Error handling

Most operations return phoenix_channel::Result<T>. The variants callers most commonly need to distinguish are:

  • Error::JoinRejected(payload) when the server rejects phx_join.
  • Error::PushRejected(payload) when a push or leave receives an error reply.
  • Error::PushTimeout(duration) when no reply arrives in time.
  • Error::NotJoined when an event is pushed before joining or after leaving.
  • Error::ConnectionClosed when the transport closes during an operation.

Typed event receivers return EventReceiveError separately. In particular, Lagged(count) means the receiver fell behind the bounded event queue; it can call recv again and continue.

API documentation

Build and open the complete Rust API documentation locally:

cargo doc -p phoenix-channel --open

Dependencies

Crate Purpose
tokio Async runtime
tokio-tungstenite WebSocket transport
serde / serde_json Message serialisation
futures-util Stream/sink combinators
tracing Structured logging
thiserror Error types
url Endpoint URL handling

License

MIT — see LICENSE.