Arifa Server is a lightweight realtime WebSocket gateway built with actix-web and actix-ws.
It uses NATS through arifa-nats for realtime pub/sub fan out and Redis for WebSocket session state.
The gateway provides:
- WebSocket connections
- JWT authentication
- Per user realtime channels
- NATS based message routing
- Cross service notification delivery
- Connection/disconnection lifecycle events
- Session heartbeat tracking
- Automatic stale session eviction
- Multiple gateway node support
A typical architecture looks like this:
flowchart TB
A[Other Services<br/>API / Backend / Jobs] -- "publish (arifa.route)" --> B[arifa-server]
C[WebSocket Client] <--> B
B -- "publish (arifa.internal)" --> D[Other Services<br/>Lifecycle Events]
B <--> E[(Redis<br/>session state)]
B <--> F[(NATS)]
There are two kinds of consumers of this gateway, and they talk to it differently:
flowchart LR
subgraph Frontend["Frontend / mobile client"]
direction TB
C1[Open WebSocket] --> C2[Send auth frame] --> C3[Receive WsMessages]
end
subgraph Backend["Backend service"]
direction TB
S1[Build a WsMessage] --> S2[Wrap in NotifyEnvelope] --> S3["Publish to arifa.route (NATS)"]
end
Frontend -. "wss://.../ws/connect" .-> Gateway[arifa-server]
Backend -. "NATS" .-> Gateway
- If you're building a client (web/mobile app): connect over
wss://to/ws/connect, send the auth frame, then listen for incomingWsMessageJSON objects. See WebSocket connection. - If you're building a backend service that wants to push realtime updates to a user: construct a
WsMessage, wrap it in aNotifyEnvelope, and publish it to thearifa.routeNATS subject. See Routing messages to WebSocket clients. - If your service needs to issue the JWTs clients use to authenticate: see Issuing JWTs.
- If you're operating the gateway itself: see Installing a prebuilt release, Configuration, and Production deployment.
The main WebSocket endpoint is:
/ws/connect
A client connects using:
ws://<host>:<port>/ws/connect
or, in production:
wss://<host>:<port>/ws/connect
The connection lifecycle:
flowchart TD
A[Connect] --> B[Wait for auth<br/>first message, 10s timeout]
B -->|invalid / timeout / bad JSON| Z[Close socket]
B -->|valid JWT| C[Subscribe to User::user_id]
C --> D[Authenticated]
D --> E[Receive realtime messages]
E --> F[Disconnect]
F --> G[Unsubscribe]
F --> H[Remove heartbeat entry]
F --> I[Emit disconnected event]
The first WebSocket message must be an authentication message.
The server expects:
{
"type": "auth",
"token": "<your-jwt>"
}Authentication must happen within 10 seconds, checked every second.
The connection is closed if:
- the first message isn't received within 10 seconds
- the message contains invalid JSON
typeisn'tauth- the JWT is invalid
- the JWT is expired
- the JWT doesn't satisfy the configured authentication requirements
After successful authentication, the server sends:
{
"type": "authenticated",
"user_id": "123"
}The user is then subscribed to:
User::<user_id>
For example: User::123.
arifa-server doesn't issue tokens, it only verifies them. Any service that authenticates WebSocket clients, or that needs to hand a client a token to connect with, has to mint a JWT that satisfies the validator below.
This is the actual verifier used by the gateway:
use jsonwebtoken::{Algorithm, DecodingKey, Validation, decode, errors::Error as JwtError};
use serde::{Deserialize, Serialize};
use std::env;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Claims {
pub sub: String,
pub exp: usize,
#[serde(default)]
pub iss: Option<String>,
#[serde(default)]
pub aud: Option<String>,
}
#[derive(Clone)]
pub struct JwtAuth {
decoding_key: DecodingKey,
validation: Validation,
}
impl JwtAuth {
pub fn from_env() -> Result<Self, JwtError> {
let secret = env::var("ARIFA_JWT_SECRET").expect("ARIFA_JWT_SECRET must be set");
let algorithm = env::var("ARIFA_JWT_ALGORITHM").unwrap_or_else(|_| "HS256".to_string());
let algorithm = match algorithm.as_str() {
"HS256" => Algorithm::HS256,
"HS384" => Algorithm::HS384,
"HS512" => Algorithm::HS512,
_ => panic!("Unsupported ARIFA_JWT_ALGORITHM"),
};
let mut validation = Validation::new(algorithm);
if let Ok(issuer) = env::var("ARIFA_JWT_ISSUER") {
validation.set_issuer(&[issuer]);
}
if let Ok(audience) = env::var("ARIFA_JWT_AUDIENCE") {
validation.set_audience(&[audience]);
}
Ok(Self {
decoding_key: DecodingKey::from_secret(secret.as_bytes()),
validation,
})
}
pub async fn is_token_valid(&self, token: &str) -> Result<Claims, JwtError> {
let token_data = decode::<Claims>(token, &self.decoding_key, &self.validation)?;
Ok(token_data.claims)
}
}| Requirement | Comes from | Notes |
|---|---|---|
| Signing secret | ARIFA_JWT_SECRET |
Symmetric (HMAC). The issuer needs the raw secret value, byte for byte. No public/private key split. |
| Algorithm | ARIFA_JWT_ALGORITHM |
One of HS256, HS384, HS512. Must match exactly, or decode fails. Defaults to HS256 if unset. |
sub claim |
Claims.sub |
Required, must be a string (the user id). Missing or wrong type means deserialization fails even with a valid signature. |
exp claim |
Claims.exp |
Required. jsonwebtoken checks this automatically against the current time. Omit it and the token gets rejected. |
iss claim |
ARIFA_JWT_ISSUER (optional) |
Only enforced if this env var is set on the gateway. If set, the token's iss must match exactly. |
aud claim |
ARIFA_JWT_AUDIENCE (optional) |
Only enforced if this env var is set on the gateway. If set, the token's aud must match exactly. |
Minimum claims payload (matching the .env example in Configuration):
{
"sub": "123",
"exp": 1755331200,
"iss": "mtaa",
"aud": "mtaa-api"
}iss/aud are only strictly required if the gateway has ARIFA_JWT_ISSUER / ARIFA_JWT_AUDIENCE set, but it's good practice to include them even when optional, since the gateway's config can change independently of the issuer.
use jsonwebtoken::{encode, EncodingKey, Header, Algorithm};
use serde::Serialize;
use std::time::{SystemTime, UNIX_EPOCH};
#[derive(Serialize)]
struct Claims {
sub: String,
exp: usize,
iss: String,
aud: String,
}
fn issue_token(user_id: &str, secret: &str) -> String {
let exp = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_secs() as usize
+ 3600; // 1 hour
let claims = Claims {
sub: user_id.to_string(),
exp,
iss: "mtaa".to_string(),
aud: "mtaa-api".to_string(),
};
let header = Header::new(Algorithm::HS256); // match ARIFA_JWT_ALGORITHM
encode(&header, &claims, &EncodingKey::from_secret(secret.as_bytes())).unwrap()
}import jwt from "jsonwebtoken";
const token = jwt.sign(
{ sub: "123" }, // user_id, must serialize as a string
process.env.ARIFA_JWT_SECRET,
{
algorithm: "HS256", // match ARIFA_JWT_ALGORITHM
expiresIn: "1h", // sets exp automatically
issuer: process.env.ARIFA_JWT_ISSUER, // e.g. "mtaa"
audience: process.env.ARIFA_JWT_AUDIENCE, // e.g. "mtaa-api"
}
);import jwt
import time
token = jwt.encode(
{
"sub": "123",
"exp": int(time.time()) + 3600,
"iss": "mtaa",
"aud": "mtaa-api",
},
ARIFA_JWT_SECRET,
algorithm="HS256",
)- Shared secret across services. Because this uses HMAC (symmetric signing), every service that issues tokens needs the raw
ARIFA_JWT_SECRET. Treat it as shared infrastructure and rotate it everywhere at once if it's ever exposed. If you need issuers to not hold the raw secret, switch to an asymmetric algorithm (RS256/ES256) where issuers hold a private key and the gateway only holds the public key. - No clock skew leeway by default.
jsonwebtoken's validator has zero leeway onexpunless explicitly configured. If the issuing service's clock drifts from the gateway's, tokens can get rejected right at the edge of expiry. submust be a string. If your user IDs are numeric internally, serialize them as strings in the JWT ("sub": "123", not"sub": 123).Claims.subis typed asStringand won't coerce a JSON number.
Messages delivered to WebSocket clients use the following structure:
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum MessageScope {
Broadcast,
Private,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum MessageKind {
Feeds,
DirectMessage,
Event,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WsMessage {
pub scope: MessageScope,
pub kind: MessageKind,
pub node_id: Option<String>,
pub payload: serde_json::Value,
}scope determines who the message is intended for: broadcast or private.
kind identifies the type of realtime event: feeds, direct_message, or event.
node_id identifies the originating server/node. It's optional, null or a string like "node-1". The gateway doesn't require it to be present for delivery.
payload is a free form serde_json::Value. Its shape is entirely up to the sending service; the gateway doesn't interpret it.
{
"scope": "broadcast",
"kind": "feeds",
"node_id": "node-1",
"payload": {}
}| Field | Type | Required | Description |
|---|---|---|---|
scope |
string | Yes | broadcast or private |
kind |
string | Yes | feeds, direct_message, or event |
node_id |
string/null | No | Originating node identifier |
payload |
object/value | Yes | Application specific data |
{
"scope": "broadcast",
"kind": "feeds",
"node_id": "node-1",
"payload": {
"product_id": "123",
"action": "created",
"title": "New product",
"price": 1500
}
}Useful for realtime feed updates: new/updated/removed listings, nearby items, feed refresh events. payload shape is controlled by the sending application.
{
"scope": "private",
"kind": "direct_message",
"node_id": "node-1",
"payload": {
"message_id": "msg-123",
"sender_id": "42",
"recipient_id": "123",
"message": "Hello",
"created_at": "2026-08-14T15:30:00Z"
}
}The gateway doesn't interpret payload, it just transports it.
{
"scope": "private",
"kind": "event",
"node_id": "node-1",
"payload": {
"event": "order_updated",
"order_id": "order-123",
"status": "completed"
}
}Useful for order updates, payment notifications, shop notifications, and other application specific events.
Other services communicate with arifa-server through NATS, publishing to arifa.route.
flowchart LR
A[Other service] -- "publish NotifyEnvelope" --> B["arifa.route (NATS subject)"]
B --> C[arifa-server inbound task]
C -- "arifa.publish(channel, message)" --> D["User::user_id"]
D --> E[WebSocket client]
The routing envelope (NotifyEnvelope):
{
"channel": "User::123",
"message": {
"scope": "private",
"kind": "direct_message",
"node_id": "node-1",
"payload": {
"message_id": "msg-123",
"sender_id": "42",
"recipient_id": "123",
"message": "Hello"
}
}
}channel determines which WebSocket subscribers receive the message. message is a full WsMessage as described above.
Authenticated users are automatically subscribed to User::<user_id>, for user 123 that's User::123. To message that user, publish:
{
"channel": "User::123",
"message": {
"scope": "private",
"kind": "direct_message",
"node_id": "api-1",
"payload": {
"message_id": "msg-123",
"sender_id": "42",
"recipient_id": "123",
"message": "You have a new message"
}
}
}The gateway receives this on arifa.route and republishes message onto User::123. Any active WebSocket session subscribed to that channel receives it.
{
"channel": "User::123",
"message": {
"scope": "broadcast",
"kind": "feeds",
"node_id": "api-1",
"payload": {
"action": "new_product",
"product_id": "product-123"
}
}
}The meaning of broadcast vs private is defined by your application protocol. The gateway's actual routing mechanism is the NATS channel supplied in the NotifyEnvelope.
arifa-server uses two main NATS subjects.
sequenceDiagram
participant S as Other service
participant N as NATS (arifa.route)
participant G as arifa-server
participant U as User::<user_id>
participant C as WebSocket client
S->>N: publish NotifyEnvelope
N->>G: deliver envelope
G->>U: arifa.publish(channel, message)
U->>C: deliver WsMessage
Used for gateway lifecycle events. The gateway publishes an event when sessions connect and disconnect:
{ "event": "user.connected", "session_id": "session-123", "user_id": "123", "online_users": 42 }{ "event": "user.disconnected", "session_id": "session-123", "user_id": "123", "online_users": 41 }Other services subscribe to arifa.internal to monitor connection activity.
The NATS bridge is two background tasks sharing one plain async_nats::Client.
flowchart LR
A[WebSocket Connected/Disconnected] --> B[InternalEvent]
B --> C[mpsc queue]
C --> D[Outbound bridge task]
D -->|stamps online_users count| E["arifa.internal"]
- receives internal
InternalEvents from an mpsc queue - calculates the current
online_users()count - adds the count to the event
- serializes and publishes it to
arifa.internal
Subscribes to arifa.route, receives a NotifyEnvelope, and republishes the WsMessage payload onto the target channel (e.g. User::123). This lets services completely separate from the gateway send realtime notifications.
Redis is used for WebSocket session state.
REDIS_URL=redis://127.0.0.1:6379For production, use your Redis provider's connection URL, e.g.:
REDIS_URL=redis://username:password@host:6379flowchart TD
A[WebSocket connected] --> B[Heartbeat registered]
B --> C[Ping/Pong activity]
C --> D[Heartbeat timestamp updated]
D --> E["Sweeper checks sessions (every 15s)"]
E -->|active| C
E -->|no pong for 60s| F[Evict session]
The sweeper runs every 15 seconds; a session is considered stale after 60 seconds without a pong. The server also handles Ping, Pong, and Close frames directly. When a session disconnects, its heartbeat entry gets removed.
Clients should respond to server Pings with Pong (or send their own Pings) to stay connected.
- Rust toolchain (stable), only needed if building from source
- A running NATS server
- A running Redis server
rustc --version
cargo --version
nats-server --version
redis-server --versionIf you don't want to build from source, grab a prebuilt binary from the Releases page. The example below installs v0.1.4, swap the version and asset name for your platform.
1. Pick the asset for your platform:
| Platform | Asset |
|---|---|
| Linux x86_64 | arifa-server-v0.1.4-linux-x86_64.tar.gz |
| Linux aarch64 | arifa-server-v0.1.4-linux-aarch64.tar.gz |
| macOS x86_64 | arifa-server-v0.1.4-macos-x86_64.tar.gz |
| macOS aarch64 | arifa-server-v0.1.4-macos-aarch64.tar.gz |
2. Download with wget and verify the checksum:
VERSION=v0.1.4
ASSET=arifa-server-${VERSION}-linux-x86_64.tar.gz # change per platform
wget "https://github.com/anomalous254/arifa-server/releases/download/${VERSION}/${ASSET}"
wget "https://github.com/anomalous254/arifa-server/releases/download/${VERSION}/${ASSET}.sha256"
sha256sum -c "${ASSET}.sha256"3. Extract and install:
tar -xzf "${ASSET}"
chmod +x arifa-server
sudo mv arifa-server /usr/local/bin/arifa-server4. Confirm it works and see usage:
Since /usr/local/bin is typically on your PATH, you can just run:
arifa-server --version
arifa-server --helpIf you'd rather run it directly from the extracted folder without installing it system wide, prefix it with ./:
./arifa-server --version
./arifa-server --help
./arifa-server --info5. Start the server:
Once NATS_URL, REDIS_URL, and your JWT env vars are set (see Configuration), start it with no flags:
arifa-server
# or, if running from the extracted folder:
./arifa-servergit clone https://github.com/anomalous254/arifa-server.git
cd arifa-server
cargo build --release --bin arifa-server
./target/release/arifa-serverThe build output isn't on your PATH by default, so run it with ./target/release/arifa-server (or ./arifa-server if you've copied/renamed the binary into your working directory), the same way as the prebuilt release usage above.
arifa-server [OPTIONS]| Option | Description |
|---|---|
-h, --help |
Show help and exit |
-V, --version |
Show version and exit |
-F, --info |
Show info about the server and developer |
| (none) | Start the server normally |
An unrecognized flag exits with status code 2 and a usage hint.
# show version
arifa-server --version
./arifa-server -V
# show help
arifa-server --help
./arifa-server -h
# show server + developer info
arifa-server --info
./arifa-server -F
# start the server (no flags)
arifa-server
./arifa-serverCreate a .env file in the project root:
NATS_URL=nats://127.0.0.1:4222
REDIS_URL=redis://127.0.0.1:6379
NODE_ID=node-1
ARIFA_NODE_ID=node-1
ARIFA_HOST=0.0.0.0
ARIFA_PORT=8080
ARIFA_JWT_SECRET=your-secret
ARIFA_JWT_ALGORITHM=HS256
ARIFA_JWT_ISSUER=mtaa
ARIFA_JWT_AUDIENCE=mtaa-api
ARIFA_AUTH_ENABLED=true| Variable | Description | Example |
|---|---|---|
NATS_URL |
NATS server URL | nats://127.0.0.1:4222 |
REDIS_URL |
Redis connection URL | redis://127.0.0.1:6379 |
NODE_ID |
Gateway node identifier | node-1 |
ARIFA_HOST |
HTTP/WebSocket bind address | 0.0.0.0 |
ARIFA_PORT |
HTTP/WebSocket port | 8080 |
ARIFA_JWT_SECRET |
JWT signing secret, also required by any service issuing tokens, see Issuing JWTs | your-secret |
ARIFA_JWT_ALGORITHM |
JWT algorithm, must match on the issuing side too | HS256 |
ARIFA_JWT_ISSUER |
Expected JWT issuer (optional, enforced if set) | mtaa |
ARIFA_JWT_AUDIENCE |
Expected JWT audience (optional, enforced if set) | mtaa-api |
ARIFA_AUTH_ENABLED |
Enable JWT authentication | true |
flowchart LR
A[nats-server] --> C[arifa-server]
B[redis-server] --> C
C --> D["cargo run --bin arifa-server"]
# terminal 1
nats-server
# terminal 2
redis-server
# terminal 3
cargo run --bin arifa-server
# or, for a release build:
cargo build --release --bin arifa-server
./target/release/arifa-serverExpected startup output:
server starting host=0.0.0.0 port=8080
The server panics on boot (by design, via .expect(...)) if NATS_URL, REDIS_URL, the NATS connection, or JWT env vars are missing/invalid. The panic message names the exact missing variable.
Connect to:
ws://localhost:8080/ws/connect # local dev
wss://your-domain.com/ws/connect # production
-
Open the WebSocket connection.
-
Send the auth frame as your first message, within 10 seconds:
{ "type": "auth", "token": "<JWT>" }See Issuing JWTs if you need to know how
<JWT>is generated. -
On success, you receive:
{ "type": "authenticated", "user_id": "123" }You're now subscribed to
User::123and receive anyWsMessagerouted there. -
Respond to server
Pings withPong(or send your ownPings) to keep the heartbeat alive.
If auth fails (bad JSON, wrong type, invalid/expired token, or timeout), the server closes the socket with a policy close code and a descriptive reason string.
If another service publishes this to arifa.route:
{
"channel": "User::123",
"message": {
"scope": "private",
"kind": "event",
"node_id": "api-1",
"payload": {
"event": "notification",
"title": "New notification",
"message": "You have a new notification"
}
}
}...the WebSocket client receives just the inner message object (the WsMessage). The NotifyEnvelope is internal NATS routing and is never sent to the client:
{
"scope": "private",
"kind": "event",
"node_id": "api-1",
"payload": {
"event": "notification",
"title": "New notification",
"message": "You have a new notification"
}
}let message = WsMessage {
scope: MessageScope::Private,
kind: MessageKind::DirectMessage,
node_id: Some("api-1".to_string()),
payload: serde_json::json!({
"message_id": "msg-123",
"sender_id": "42",
"recipient_id": "123",
"message": "Hello"
}),
};Wrap it in the routing envelope and publish the serialized JSON to arifa.route:
{
"channel": "User::123",
"message": {
"scope": "private",
"kind": "direct_message",
"node_id": "api-1",
"payload": {
"message_id": "msg-123",
"sender_id": "42",
"recipient_id": "123",
"message": "Hello"
}
}
}flowchart TB
N[(NATS)]
N --- G1[arifa-server-1<br/>node-1]
N --- G2[arifa-server-2<br/>node-2]
G1 --- C1[Clients]
G2 --- C2[Clients]
Multiple arifa-server instances can run simultaneously. A service publishing to arifa.route doesn't need to know which gateway node currently owns the user's WebSocket connection. NATS provides the cross node messaging layer, and whichever node holds the relevant subscription delivers the message.
Authentication failures close the WebSocket connection. Possible causes:
- authentication timeout
- malformed JSON
- wrong message type
- missing token
- invalid JWT
- expired JWT
- invalid JWT claims
The server uses a WebSocket policy close code with a descriptive reason. Treat the exact reason string as diagnostic information, not a stable API contract.
AppState::shutdown() stops the background Arifa router/forwarding tasks. The bundled main() wires this up for you on Ctrl+C (SIGINT):
flowchart TD
A[SIGINT / Ctrl+C] --> B[app_state.shutdown]
B --> C[Arifa background tasks stop]
C --> D[handle.stop true]
D --> E[HTTP/WebSocket server exits]
If you run arifa-server under a process manager that sends SIGTERM instead (systemd, Docker, Kubernetes), make sure that signal reaches the same shutdown path. start_server() doesn't call AppState::shutdown() automatically on its own.
Use wss:// (not ws://) in production, and put a reverse proxy in front of the gateway for TLS termination:
flowchart TB
I[Internet] --> P[Reverse proxy<br/>HTTPS/WSS + TLS termination]
P --> G1[arifa-server-1]
P --> G2[arifa-server-2]
G1 --- N[(NATS)]
G2 --- N
G1 --- R[(Redis)]
G2 --- R
Example systemd unit (/etc/systemd/system/arifa-server.service):
[Unit]
Description=arifa-server
After=network.target
[Service]
Type=simple
User=arifa
WorkingDirectory=/opt/arifa-server
EnvironmentFile=/opt/arifa-server/.env
ExecStart=/usr/local/bin/arifa-server
Restart=on-failure
RestartSec=2
KillSignal=SIGTERM
TimeoutStopSec=10
[Install]
WantedBy=multi-user.targetsudo systemctl daemon-reload
sudo systemctl enable --now arifa-server
sudo systemctl status arifa-server
journalctl -u arifa-server -fKillSignal=SIGTERM makes sure systemctl stop sends the signal your process needs to catch and forward into app_state.shutdown().
Example nginx config for WebSocket + TLS:
server {
listen 443 ssl http2;
server_name api.example.com;
ssl_certificate /etc/letsencrypt/live/api.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/api.example.com/privkey.pem;
location /ws/connect {
proxy_pass http://127.0.0.1:8080/ws/connect;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
# WebSocket connections are long lived, don't let the proxy
# time out an idle but alive session
proxy_read_timeout 3600s;
proxy_send_timeout 3600s;
}
}Obtain the certificate with, e.g., certbot --nginx -d api.example.com.
Clients then connect to wss://api.example.com/ws/connect instead of talking to port 8080 directly.
Point the reverse proxy at more than one arifa-server instance (different ports or hosts) for horizontal scale and failover, see Multi-node architecture. All nodes share the same NATS cluster, so a message published to arifa.route reaches the user regardless of which node holds their connection.
- Never commit
.envorARIFA_JWT_SECRETto git. - Use your platform's secret store (systemd
EnvironmentFilewith restricted permissions, Docker secrets, KubernetesSecret, or your cloud provider's secrets manager) instead of plain files where possible. - Use a long, random
ARIFA_JWT_SECRETand rotate it if it's ever exposed. Remember, every service issuing tokens (see Issuing JWTs) needs this same secret, so rotation has to be coordinated across all of them.
- Keep NATS and Redis on a private network, don't expose them to the public internet.
- Enable NATS auth (
nats-serversupports token/user/TLS auth) for anything beyond local development. - Use a managed or clustered Redis in production for durability if session state loss on restart matters to you.
-
wss://only, TLS terminated at the proxy or the app - Strong, unique
ARIFA_JWT_SECRET, not committed to source control - NATS and Redis restricted to a private network, with auth enabled
- Process supervisor configured to forward
SIGTERM(systemd/Docker/K8s) - Reverse proxy timeouts tuned for long lived WebSocket connections
- Multiple nodes behind the proxy if you need horizontal scale or zero downtime deploys
- Logs shipped somewhere durable (
journalctl, or your log aggregator of choice)
Client → arifa-server (auth):
{ "type": "auth", "token": "<JWT>" }arifa-server → Client (auth success):
{ "type": "authenticated", "user_id": "123" }arifa-server → Client (realtime message):
{ "scope": "private", "kind": "event", "node_id": "api-1", "payload": {} }Service → arifa-server (NATS subject arifa.route):
{
"channel": "User::123",
"message": { "scope": "private", "kind": "event", "node_id": "api-1", "payload": {} }
}arifa-server → Services (NATS subject arifa.internal):
{ "event": "user.connected", "session_id": "session-123", "user_id": "123", "online_users": 42 }
{ "event": "user.disconnected", "session_id": "session-123", "user_id": "123", "online_users": 41 }sequenceDiagram
participant C as Client
participant G as arifa-server
participant S as Other service
participant N as NATS (arifa.route)
C->>G: Connect /ws/connect
C->>G: {"type":"auth","token":"<JWT>"}
G->>G: Validate JWT
G->>C: {"type":"authenticated","user_id":"123"}
G->>G: Subscribe User::123
Note over S: Service creates notification
S->>S: Build WsMessage
S->>N: Publish NotifyEnvelope
N->>G: Deliver envelope
G->>C: Publish WsMessage to User::123
C->>C: Process payload
The gateway acts as a transport and routing layer; application services remain responsible for constructing the actual business payload.
| Name | Peter Nyando |
| nyandopeter2@gmail.com | |
| GitHub | github.com/anomalous254 |
| Repo | github.com/anomalous254/arifa-server |
See the repository license for licensing information.