# Aether API V3 WebSocket Integration Guide

**Last Updated:** 2026-06-12

This guide explains how to implement real-time communication using the **Aether API V3 WebSocket** protocol. V3 introduces granular routing, strict message schemas, and improved multi-tenant isolation compared to previous versions.

---

## 1. Key Improvements (V2 vs V3)

| Feature | WebSocket V2 (Legacy) | WebSocket V3 (Modern) |
| :--- | :--- | :--- |
| **URL Prefix** | `/ws/` or `/ws_redis/` | `/v3/ws/` |
| **Routing** | **Global**: Every client receives every message. | **Granular**: Redis filters messages before sending. |
| **Performance**| Low efficiency at scale (Python filtering). | High efficiency (Redis native pub/sub). |
| **Schema** | Loose JSON objects. | Strict Pydantic-validated models. |
| **Presence** | None / Manual. | Automatic Redis-backed presence sets. |

---

## 2. Connection Strategy

### A. Endpoint URL
The V3 WebSocket path requires both a `group_id` and a `client_id`. Both are treated as **opaque unique strings** by the backend — no specific format is enforced.

```text
wss://[api_domain]/v3/ws/group/{group_id}/client/{client_id}
```

**`group_id`** — identifies the shared channel (e.g., an event ID, a room name, or a Vision ID random string). All clients using the same `group_id` receive group-targeted messages together.

**`client_id`** — uniquely identifies this specific connection. The backend accepts any unique string (UUID, timestamp, Vision ID — no format validation). The **recommended pattern** is a UUID v4 generated once and persisted in `localStorage` so the same identity is reused across page reloads and sessions on that browser.

> Use `ws://` for local development and `wss://` in production (any HTTPS site). The Nginx config must include the Upgrade block — see Section 6.

### B. Authentication
Browsers **cannot** set custom HTTP headers on WebSocket connections. Pass the API Key and account context as **query parameters** instead:

| Parameter | Purpose | Example |
| :--- | :--- | :--- |
| `api_key` | Entry Ticket (machine auth) | `?api_key=<your_app_key>` |
| `jwt` | Visa (user / account context) | `&jwt=<token>` |
| `x_account_id` | Alt account context | `&x_account_id=<account_id>` |

**Full example URL:**
```text
wss://dev-api.oneskyit.com/v3/ws/group/{group_id}/client/{client_id}?api_key=<key>&jwt=<token>
```

### C. Connection Example (TypeScript)
```ts
// client_id: generated once, persisted in localStorage for stable identity across reloads
if (!localStorage.getItem('controller_client_id')) {
    localStorage.setItem('controller_client_id', crypto.randomUUID());
}
const client_id = localStorage.getItem('controller_client_id')!; // UUID v4, e.g. "550e8400-e29b-41d4-a716-446655440000"

const group_id = "event_abc123"; // Any unique string identifying the shared channel
const api_key = import.meta.env.VITE_API_KEY;
const jwt = getSessionToken(); // your JWT helper

const ws_url = `wss://dev-api.oneskyit.com/v3/ws/group/${group_id}/client/${client_id}?api_key=${api_key}&jwt=${jwt}`;

const socket = new WebSocket(ws_url);

socket.onopen = () => {
    console.log("Connected to Aether WS V3");
};
```

---

## 3. The V3 Message Schema

All messages sent and received over V3 must follow the standardized **WS_Message_V3** structure.

### Message Fields
| Field | Type | Required | Description |
| :--- | :--- | :--- | :--- |
| `version` | string | Auto | Always `"3"`. Set by server. |
| `msg_type` | string | Yes | `'msg'`, `'cmd'`, `'heartbeat'`, `'presence'` |
| `target` | string | Yes | `'direct'`, `'group'`, `'broadcast'`, `'echo'` |
| `from_id` | string | Auto | **Server fills this from the URL path** — do not send. |
| `to_id` | string | Conditional | Target Client ID. Required when `target` is `'direct'`. |
| `group_id` | string | Auto | **Server fills this from the URL path** — do not send. |
| `cmd` | string | No | Specific action keyword (e.g., `'RELOAD'`). |
| `msg` | string | No | Human-readable text content. |
| `payload` | object | No | Flexible key-value data. |
| `sent_at` | string | Auto | ISO 8601 Timestamp. Set by server. |

> **Frontend tip:** Only send `msg_type`, `target`, and whatever content fields you need (`msg`, `cmd`, `payload`, `to_id`). The server enforces `from_id`, `group_id`, and `sent_at` from the connection context, preventing spoofing.

---

## 4. Message Targeting Logic

V3 uses the `target` field to determine which Redis channel to use, ensuring only the intended recipients receive the data.

### A. Group Broadcast
Sends the message to every client connected to the same `group_id`.
```json
{
  "msg_type": "msg",
  "target": "group",
  "msg": "Hello team!"
}
```

### B. Direct Message (DM)
Sends the message to one specific client ID, regardless of their group.
```json
{
  "msg_type": "msg",
  "target": "direct",
  "to_id": "target_client_random_id",
  "msg": "Private message just for you."
}
```

### C. System Broadcast
Sends the message to **every** connected client on the platform (use sparingly).
```json
{
  "msg_type": "cmd",
  "target": "broadcast",
  "cmd": "MAINTENANCE_WARNING"
}
```

### D. Echo
Sends the message back only to the sender (useful for testing round-trip latency).
```json
{
  "msg_type": "msg",
  "target": "echo",
  "msg": "Ping!"
}
```

---

## 5. Specialized Message Types

### Commands (`cmd`)
Used for remote control or orchestration.
```json
{
  "msg_type": "cmd",
  "target": "group",
  "cmd": "RELOAD_UI",
  "payload": { "force": true }
}
```

### Heartbeats (`heartbeat`)
Keep the connection alive and **refresh presence** in the backend. Should be sent every 30-60 seconds.

- The server intercepts `heartbeat` messages and refreshes the Redis presence TTL (1 hour window) before echoing back.
- Without periodic heartbeats, a client idle for >1 hour may disappear from the presence set even while still connected.
- Use `target: 'echo'` so the server sends the heartbeat straight back — useful for measuring round-trip latency.

```json
{
  "msg_type": "heartbeat",
  "target": "echo"
}
```

---

## 6. Infrastructure Requirements (Nginx)

Unlike standard REST endpoints, WebSockets require explicit "Upgrade" handling in the Nginx gateway. If you are deploying to a new server, ensure the following block is present in your Nginx configuration:

```nginx
location /v3/ws {
    proxy_pass http://fastapi_backend;
    proxy_http_version 1.1;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection "upgrade";
    proxy_set_header Host $http_host;
    proxy_read_timeout 2100s; # Match your app's max heartbeat/session time
}
```

---

## 7. Common Pitfalls & Troubleshooting

- **HTTP 404 Errors**: This almost always means Nginx is missing the `location /v3/ws` block and is trying to serve the request as a static file from the disk.
- **HTTP 400 Errors**: Check your `Host` header. Nginx routes requests based on the `server_name` directive. If you connect to an IP or a non-standard hostname (like `localhost`), ensure it is explicitly listed in your Nginx config.
- **Connection Drops**: If the connection drops exactly after 60 seconds, check your Nginx `proxy_read_timeout`. It should be set high (e.g., `2100s`) to allow for long-lived WebSocket sessions.

---

## 8. Migration Guide (V2 to V3)

If you are upgrading from the legacy V2 WebSocket (`/ws/group/...`):

1.  **Change the URL**: Prepend `/v3/` to your WebSocket path.
2.  **Wrap your JSON**: In V2, you might have sent `{"msg": "hi"}`. In V3, this must be `{"msg_type": "msg", "target": "group", "msg": "hi"}`.
3.  **Use unique string IDs**: Both `group_id` and `client_id` in the path are opaque strings — any unique value works (timestamp, UUID, Vision ID random string). Just don't use raw database integer IDs. For `to_id` in direct messages, use whatever `client_id` that target client registered with.
4.  **Listen for `msg_type`**: Update your frontend handlers to switch logic based on the `msg_type` field instead of proprietary keys.
