Skip the setup. Relay Cloud handles hosting, SSL, and ops for you. WebSockets ready in 60 seconds. Try free →

Getting Started

Get Relay running and publishing your first real-time event in under 5 minutes.

Prerequisites

1

Run the Server

Choose how you want to run Relay on your machine or server.

docker run -d -p 6001:6001 \
  -e RELAY_APP_KEY=my-key \
  -e RELAY_APP_SECRET=my-secret \
  relayhq/relay:latest

The server is now listening on ws://localhost:6001.

Download the latest binary from GitHub Releases for your platform, then run:

./relay-server
git clone https://github.com/DarkNautica/Relay.git && cd Relay
go build -o relay-server .
./relay-server
2

Connect the JavaScript Client

Install the Relay JavaScript SDK:

npm install @relayhq/relay-js

Then connect to your Relay server and subscribe to a channel:

import Relay from '@relayhq/relay-js';

const relay = new Relay('my-key', {
    host: 'localhost',
    port: 6001,
    encrypted: false,
});

relay.on('connected', () => console.log('Connected to Relay!'));
relay.on('disconnected', () => console.log('Disconnected.'));
relay.on('error', (err) => console.error('Error:', err));

const channel = relay.subscribe('my-channel');
channel.bind('my-event', (data) => {
    console.log('Received:', data);
});
3

Publish from Your Backend

Relay is wire-compatible with the Pusher protocol, so you can use any Pusher-compatible server SDK to trigger events.

Install

composer require relayhq/relay-php

config/broadcasting.php

'relay' => [
    'driver' => 'pusher',
    'key' => env('RELAY_APP_KEY'),
    'secret' => env('RELAY_APP_SECRET'),
    'app_id' => env('RELAY_APP_ID', 'app'),
    'options' => [
        'host' => env('RELAY_HOST', 'localhost'),
        'port' => env('RELAY_PORT', 6001),
        'scheme' => 'http',
    ],
],

.env

BROADCAST_CONNECTION=relay
RELAY_APP_KEY=my-key
RELAY_APP_SECRET=my-secret

Publish an event

broadcast(new OrderShipped($order));
const Pusher = require('pusher');

const pusher = new Pusher({
    appId: 'app',
    key: 'my-key',
    secret: 'my-secret',
    host: 'localhost',
    port: 6001,
    useTLS: false,
});

await pusher.trigger('my-channel', 'my-event', {
    message: 'Hello!'
});

Gemfile

gem 'pusher'

config/initializers/pusher.rb

Pusher.app_id = 'app'
Pusher.key = 'my-key'
Pusher.secret = 'my-secret'
Pusher.host = 'localhost'
Pusher.port = 6001
Pusher.encrypted = false

Trigger an event

Pusher.trigger('my-channel', 'my-event', { message: 'Hello!' })

Install

pip install pusher

Trigger an event

import pusher

client = pusher.Pusher(
    app_id='app',
    key='my-key',
    secret='my-secret',
    host='localhost',
    port=6001,
    ssl=False
)

client.trigger('my-channel', 'my-event', {'message': 'Hello!'})
4

Private Channels

Private channels restrict access to authenticated users. Channel names must be prefixed with private-. When a client subscribes to a private channel, Relay sends an authentication request to your backend.

Auth endpoint: Your backend must expose a POST /broadcasting/auth endpoint that validates the user and returns a signed auth token.

Subscribe on the client

const privateChannel = relay.subscribe('private-orders');
privateChannel.bind('new-order', (data) => {
    console.log('New order:', data);
});

Laravel auth setup

Laravel handles this automatically via the BroadcastServiceProvider. Define your channel authorization in routes/channels.php:

// routes/channels.php
Broadcast::channel('private-orders', function ($user) {
    return $user->canViewOrders();
});

Make sure broadcasting is enabled in config/app.php by uncommenting the BroadcastServiceProvider:

// config/app.php — providers array
App\Providers\BroadcastServiceProvider::class,
5

Presence Channels

Presence channels extend private channels with awareness of who is currently subscribed. They are ideal for showing online indicators, typing notifications, or collaborative features. Channel names must be prefixed with presence-.

Subscribe on the client

const presenceChannel = relay.subscribe('presence-chat-room');

presenceChannel.bind('relay:subscription_succeeded', (members) => {
    console.log('Online now:', members.count);
    members.each((member) => {
        console.log(member.id, member.info);
    });
});

presenceChannel.bind('relay:member_added', (member) => {
    console.log('Joined:', member.id);
});

presenceChannel.bind('relay:member_removed', (member) => {
    console.log('Left:', member.id);
});

Authorize on the backend (Laravel)

// routes/channels.php
Broadcast::channel('presence-chat-room', function ($user) {
    return ['id' => $user->id, 'name' => $user->name];
});

Returning an array (instead of true) tells Relay to treat this as a presence channel and share the returned data with all subscribers.

Common Issues

BROADCAST_CONNECTION vs BROADCAST_DRIVER

In Laravel 11+, the environment variable was renamed from BROADCAST_DRIVER to BROADCAST_CONNECTION. If events are not reaching Relay, make sure your .env uses the correct key for your Laravel version:

# Laravel 11+
BROADCAST_CONNECTION=relay

# Laravel 10 and earlier
BROADCAST_DRIVER=relay

CSRF Exemption for Auth Endpoint

The Relay JavaScript client sends a POST request to /broadcasting/auth. If you receive a 419 status code, add the route to your CSRF exceptions:

// app/Http/Middleware/VerifyCsrfToken.php
protected $except = [
    'broadcasting/auth',
];

Event Name Prefixes

Laravel automatically prefixes broadcast event names with the fully qualified class name, e.g. App\Events\OrderShipped. If you are listening for events by their raw name on the client, prefix the listener with a dot to bypass the namespace:

// Listen for the raw event name (no App\Events\ prefix)
channel.bind('.my-event', (data) => {
    console.log(data);
});
Ready to skip the server setup? Relay Cloud gives you a production WebSocket server instantly. Start free, upgrade when you grow. Start free →