Tapaculo
A lightweight Rust server that handles real-time and turn-based communication for any multiplayer client interactions.
Features
- Pluggable PubSub: Swap between in-memory and Redis backends without code changes
- JWT Authentication: Secure token-based auth with session tracking and reconnection support
- Room Management: Capacity limits, player tracking, and lifecycle events
- Real-time WebSocket: Bidirectional communication with automatic cleanup
- Rate Limiting: Built-in spam prevention and abuse protection
- Message History: Optional replay for new joiners
- User Metadata: Associate custom data with players
- Token Endpoint: Optional
POST /tokenroute for server-side JWT minting - Custom Room State: Store typed game state per room, accessible from any handler
- Typed Messages: Type-safe message handling with serde
- Production Ready: Comprehensive error handling, logging, and reconnection logic
Quick Start
[]
= "1.2"
Basic Server
use *;
async
Use Cases
Chess Server (2 players, turn-based)
use *;
use ;
async
;
Chat Server (group messaging)
new
.with_auth
.with_pubsub
.with_room_settings
.with_limits
.on_message
.listen
.await
Core Concepts
1. Room Management
Rooms are isolated multiplayer sessions with configurable limits:
let room_settings = RoomSettings ;
new
.with_room_settings
// ...
2. Broadcast Filtering
Send messages to specific players:
// To all players in room
ctx.broadcast.await?;
// To all OTHER players (exclude sender)
ctx.broadcast_to_others.await?;
// Custom filter
ctx.broadcast_filtered.await?;
// Direct message to one player — works across nodes with Redis
ctx.send_to.await?;
broadcast and broadcast_to_others use a single publish to the room topic regardless of player count. send_to and broadcast_filtered route via per-user topics and work correctly in multi-node Redis deployments.
3. Room Lifecycle Events
React to room state changes:
;
new
.with_event_handler
// ...
4. Room State Queries
Access room information:
// Get all members in room
let members = ctx.get_room_members.await;
// Check if specific user is in room
if ctx.has_member.await
// Get room info
if let Some = ctx.get_room_info.await
// Get message history
let history = ctx.get_message_history.await;
5. Rate Limiting
Prevent spam and abuse:
let limits = MessageLimits ;
new
.with_limits
// ...
6. Message Validation
Validate messages before processing:
new
.on_message_validate
// ...
7. Typed Message Handlers
Type-safe message processing:
new
.
// ...
8. User Metadata
Store custom player data:
// Set metadata
let metadata = PlayerMetadata ;
ctx.set_user_metadata.await?;
// Get metadata
if let Some = ctx.get_user_metadata.await
9. Custom Room State
Store typed game state per room, accessible from any handler:
// Set initial state when first player joins
ctx.set_custom_state.await?;
// Update state atomically
ctx..await?;
// Read state
if let Some = ctx..await
10. Reconnection Support
Sessions persist across connections:
// Generate token with session ID
let auth = new;
let token = auth.sign_access?;
// Later, reconnect with same session ID
let new_token = auth.sign_access?;
// Access session ID in handler
ctx.session_id; // "session-abc"
PubSub Backends
In-Memory (Development)
let pubsub = new;
// or with custom buffer size
let pubsub = with_buffer;
Redis (Production)
[]
= { = "1.2", = ["redis-backend"] }
let pubsub = new?;
// With custom retry configuration
let config = BackoffConfig ;
let pubsub = with_config?;
Authentication
Creating Tokens
let auth = new;
// Access token
let access_token = auth.sign_access?;
// Refresh token
let refresh_token = auth.sign_refresh?;
// Refresh access token
let new_access = auth.refresh_access?;
Token Endpoint
Enable a built-in POST /token route so clients can obtain JWTs without a separate auth service:
new
.with_auth
.with_pubsub
.with_token_endpoint // enable POST /token
.with_token_ttl // optional: 30 min TTL (default 3600)
.listen
.await
Clients POST { "user_id": "...", "room_id": "..." } and receive { "token": "..." }:
const res = await ;
const = await res.;
const ws = ;
Note: The
/tokenendpoint performs no authorization itself — add your own middleware or proxy-level auth before exposing it publicly.
Client Connection
const token = "eyJ0eXAiOiJKV1QiLCJhbGc...";
const ws = ;
ws ;
// Send message
ws.;
Custom HTTP Routes
Use into_router() to merge the WebSocket handler with your own axum routes:
let app = new
.with_auth
.with_pubsub
.on_message
.into_router
.route;
let listener = bind.await?;
serve.await?;
Examples
See the examples/ directory for complete implementations:
chess_server.rs: 2-player turn-based game with room capacity enforcementchat_server.rs: Group chat with message history and typing indicatorscustom_state.rs: Per-room typed game stateserver_with_redis.rs: Multi-node deployment with Redis pubsub
Run examples:
Architecture
┌──────────────────────────────────────────────┐
│ WebSocket Connections │
│ (JWT Auth + Session Tracking) │
└──────────┬───────────────────────────────────┘
│
┌──────────▼──────────────────────────────────┐
│ Server (Room Management) │
│ • Rate Limiting │
│ • Message Validation │
│ • Event Handlers │
│ • User Metadata │
└──────────┬──────────────────────────────────┘
│
┌──────────▼──────────────────────────────────┐
│ PubSub Backend │
│ ┌────────────────┬──────────────────────┐ │
│ │ InMemoryPubSub│ RedisPubSub │ │
│ │ (Single Node) │ (Distributed) │ │
│ └────────────────┴──────────────────────┘ │
└─────────────────────────────────────────────┘
Production Checklist
- Use Redis backend for multi-server deployments
- Enable rate limiting to prevent spam
- Set appropriate room size limits
- Configure empty room timeouts
- Use strong JWT secrets (env variables)
- Add message validation for your use case
- Implement custom event handlers for game logic
- Enable message history if needed
- Set up structured logging (tracing)
- Monitor rate limit bans
- Handle reconnections gracefully
License
MIT
Contributing
Contributions welcome! Please open an issue or PR.