A lightweight WebSocket server for synchronizing video playback across multiple browsers. Built with Rust for performance and reliability.
This server communicates with the FrameTogether browser extension.
You can self-host FrameTogether to run your own instance, or use the default public instance hosted by the author at frametogether.19702038.xyz. The default instance temporarily stores IP addresses only in RAM for rate limiting and does not persist or share them. The author is not liable for any data handling or security issues on self-hosted or third-party instances.
Current Status: Built specifically for MUBI video synchronization. Future versions may support additional platforms.
FrameTogether is an independent project and is not affiliated with, endorsed by, or connected to MUBI or its affiliates in any way.
- Real-time video synchronization.
- Anonymous rooms with auto-generated user names.
- Host-based control so first user controls playback.
- Automatic cleanup of empty rooms.
- Rate limiting and connection management.
- Zero configuration defaults.
- Prometheus metrics endpoint for monitoring.
- Additional video platform support.
Clone the repository:
git clone git@github.com:un1970ix/frametogether-server.git
cd frametogether-serverStart the server:
docker compose up -dThe server will start on port 47642. To use a different port:
PORT=8080 docker compose up -dClone the repository:
git clone git@github.com:un1970ix/frametogether-server.git
cd frametogether-serverRequirements:
- Rust 1.88 or later.
cargo build --release
./target/release/frametogether-serverConfiguration via environment variables:
| Variable | Default | Description |
|---|---|---|
PORT or ADDR |
47642 | Server port or full address. |
MAX_ROOMS |
1000 | Maximum concurrent rooms. |
RATE_LIMIT_PER_SECOND |
30 | Requests per second per IP. |
RUST_LOG |
info | Logging level options are debug, info, warn, or error. |
The server uses WebSocket with JSON messages.
Create Room
{"type": "Create"}Join Room
{"type": "Join", "room_id": "abc123"}Update Video State (Host only.)
{"type": "State", "time": 120.5, "paused": false}Leave Room
{"type": "Leave"}Heartbeat (Keeps connection alive.)
{"type": "Heartbeat"}Room Created
{
"type": "RoomCreated",
"room_id": "abc123"
}Room Joined
{
"type": "RoomJoined",
"room_id": "abc123",
"is_host": true,
"your_name": "Happy Panda",
"participants": []
}Video Sync (Sent to non-hosts.)
{
"type": "Sync",
"time": 120.5,
"paused": false
}User Joined
{
"type": "UserJoined",
"user": {"name": "Clever Fox", "is_host": false}
}User Left
{
"type": "UserLeft",
"user_name": "Clever Fox"
}Error
{
"type": "Error",
"message": "Room not found"
}The server maintains all state in memory:
- No database required.
- Rooms are automatically cleaned up when empty.
- Connection IDs are incremental and never reset.
- Each connection gets a unique animal name per room. (Just for fun, I may add functionality later.)
- Rate limiting per IP address.
- Input validation on all messages.
- Runs as non-root user in Docker.
- Memory-safe Rust prevents common vulnerabilities.
- Maximum room limit prevents resource exhaustion.
- Real IP detection works behind proxies.
Run with debug logging:
RUST_LOG=debug cargo runFormat code:
cargo fmtCheck for issues:
cargo clippyThe included compose.yml provides a production-ready setup:
- Automatic restart on failure.
- Health checks via
/healthendpoint. - Runs as unprivileged user.
For production, place behind a reverse proxy (Such as nginx or Caddy.) for:
- SSL/TLS termination.
- Additional rate limiting.
- Load balancing multiple instances.
For nginx:
location /sync {
proxy_pass http://localhost:47642;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}See the LICENSE file for details.
Pull requests are welcome. Please run cargo fmt and cargo clippy before submitting.