syaOS syaOS / Docs
GitHub Launch

Chat Rooms API

Real-time chat rooms API powered by Pusher WebSockets and Redis (Upstash). Supports public and private rooms with presence tracking, token-based authentication, and AI-powered replies.

Endpoints

All actions are performed on a single endpoint using the action query parameter.

MethodActionDescriptionAuth Required
GETgetRoomsList all accessible roomsNo
GETgetRoomGet a single room by IDYes
GETgetMessagesGet messages for a roomNo
GETgetBulkMessagesGet messages for multiple roomsNo
GETgetRoomUsersGet active users in a roomYes
GETgetUsersSearch users by usernameNo
GETcheckPasswordCheck if user has password setYes
GETcleanupPresenceClean up expired presence (admin)Yes (admin)
GETdebugPresenceDebug presence data (admin)Yes (admin)
POSTcreateRoomCreate a new roomYes
POSTjoinRoomJoin a roomNo
POSTleaveRoomLeave a roomNo
POSTswitchRoomSwitch between roomsNo
POSTsendMessageSend a message to a roomYes
POSTgenerateRyoReplyGenerate AI reply from RyoYes
POSTcreateUserCreate a new user accountNo
POSTgenerateTokenGenerate auth token for userYes
POSTrefreshTokenRefresh an existing tokenNo
POSTverifyTokenVerify token validityNo
POSTauthenticateWithPasswordLogin with username/passwordNo
POSTsetPasswordSet user passwordYes
POSTclearAllMessagesClear all messages (admin)Yes (admin)
POSTresetUserCountsReset all user counts (admin)Yes (admin)
POSTlistTokensList user's active tokensYes
POSTlogoutAllDevicesLogout from all devicesYes
POSTlogoutCurrentLogout current sessionYes
DELETEdeleteRoomDelete a roomYes
DELETEdeleteMessageDelete a message (admin)Yes (admin)

Authentication

The API uses token-based authentication with the following headers:

X-Username: <username>
Authorization: Bearer <token>

Token Lifecycle

  • Generation: Tokens are generated on user creation or via generateToken
  • Expiration: Tokens expire after 90 days (USER_EXPIRATION_TIME)
  • Grace Period: Expired tokens can be refreshed within 30 days (TOKEN_GRACE_PERIOD)
  • Refresh: Use refreshToken to get a new token before/after expiration

Password Authentication

Users can optionally set a password for account recovery:

// Set password (requires auth)
POST /api/chat-rooms?action=setPassword
Body: { password: "min8chars" }

// Login with password
POST /api/chat-rooms?action=authenticateWithPassword
Body: { username: "alice", password: "min8chars" }

Minimum password length: 8 characters

TypeScript Types

Room Types

type RoomType = "public" | "private";

interface Room {
  id: string;
  name: string;
  type: RoomType;
  createdAt: number;
  userCount: number;
  members?: string[]; // Only for private rooms
}

interface RoomWithUsers extends Room {
  users: string[];
}

Message Types

interface Message {
  id: string;
  roomId: string;
  username: string;
  content: string;
  timestamp: number;
}

interface BulkMessagesResult {
  messagesMap: Record<string, Message[]>;
  validRoomIds: string[];
  invalidRoomIds: string[];
}

User Types

interface User {
  username: string;
  lastActive: number;
}

Room Management

Creating Rooms

Public Rooms (admin only):
POST /api/chat-rooms?action=createRoom
Headers: { X-Username: "ryo", Authorization: "Bearer <token>" }
Body: { name: "general", type: "public" }
Private Rooms (any authenticated user):
POST /api/chat-rooms?action=createRoom
Headers: { X-Username: "alice", Authorization: "Bearer <token>" }
Body: { type: "private", members: ["alice", "bob"] }

Private room names are auto-generated as @alice, @bob.

Listing Rooms

GET /api/chat-rooms?action=getRooms&username=alice

// Response
{
  "rooms": [
    { "id": "abc123", "name": "general", "type": "public", "userCount": 5, ... },
    { "id": "def456", "name": "@alice, @bob", "type": "private", "userCount": 2, "members": ["alice", "bob"], ... }
  ]
}

Users only see public rooms and private rooms they're members of.

Joining/Leaving Rooms

// Join
POST /api/chat-rooms?action=joinRoom
Body: { roomId: "abc123", username: "alice" }

// Leave
POST /api/chat-rooms?action=leaveRoom
Body: { roomId: "abc123", username: "alice" }

// Switch (optimized for changing rooms)
POST /api/chat-rooms?action=switchRoom
Body: { previousRoomId: "abc123", nextRoomId: "def456", username: "alice" }

Deleting Rooms

  • Public rooms: Admin only
  • Private rooms: Any member can leave; room is deleted when ≤1 member remains
DELETE /api/chat-rooms?action=deleteRoom&roomId=abc123
Headers: { X-Username: "ryo", Authorization: "Bearer <token>" }

Messages

Sending Messages

POST /api/chat-rooms?action=sendMessage
Headers: { X-Username: "alice", Authorization: "Bearer <token>" }
Body: { roomId: "abc123", username: "alice", content: "Hello world!" }
Rate Limiting (public rooms only):
  • Short burst: 3 messages per 10 seconds
  • Long burst: 20 messages per 60 seconds
  • Minimum interval: 2 seconds between messages
Content Validation:
  • Maximum length: Configured via MAX_MESSAGE_LENGTH
  • Profanity filtering with URL preservation
  • HTML escaping for XSS prevention
  • Duplicate message detection

Getting Messages

// Single room (default limit: 20, max: 500)
GET /api/chat-rooms?action=getMessages&roomId=abc123&limit=50

// Multiple rooms
GET /api/chat-rooms?action=getBulkMessages&roomIds=abc123,def456

AI-Powered Replies (Ryo)

Generate responses from the AI assistant "Ryo":

POST /api/chat-rooms?action=generateRyoReply
Headers: { X-Username: "alice", Authorization: "Bearer <token>" }
Body: {
  roomId: "abc123",
  prompt: "@ryo what do you think?",
  systemState: {
    chatRoomContext: {
      recentMessages: "alice: Hello\nbob: Hi there",
      mentionedMessage: "What's the weather like?"
    }
  }
}

Uses Google's Gemini 2.5 Flash model.

Real-time Events

The API uses Pusher for real-time WebSocket communication.

Channels

Channel PatternDescription
chats-publicPublic room events (all users)
chats-{username}Private events for specific user
room-{roomId}Room-specific message events

Events

EventPayloadDescription
room-created{ room: Room }New room created
room-updated{ room: Room }Room data changed (user count, etc.)
room-deleted{ roomId: string }Room deleted
room-message{ roomId: string, message: Message }New message in room
message-deleted{ roomId: string, messageId: string }Message deleted
rooms-updated{ rooms: Room[] }Batch room updates

Subscribing to Events

import Pusher from 'pusher-js';

const pusher = new Pusher(PUSHER_KEY, { cluster: PUSHER_CLUSTER });

// Subscribe to public rooms
const publicChannel = pusher.subscribe('chats-public');
publicChannel.bind('room-created', (data) => console.log('New room:', data.room));

// Subscribe to room messages
const roomChannel = pusher.subscribe('room-abc123');
roomChannel.bind('room-message', (data) => console.log('New message:', data.message));

// Subscribe to private updates (for logged-in user)
const userChannel = pusher.subscribe('chats-alice');
userChannel.bind('room-created', (data) => console.log('Invited to room:', data.room));

Redis Data Structure

Key Prefixes

PrefixDescription
chat:room:{roomId}Room object (JSON)
chat:messages:{roomId}Message list (Redis List, max 100)
chat:users:{username}User object (JSON)
chat:room:users:{roomId}Room user set (deprecated, use presence)
chat:presence:{roomId}:{username}Individual presence key
chat:presencez:{roomId}Room presence ZSET (score = timestamp)
chat:roomsSet of all room IDs

TTL Values

Key TypeTTL
Room presence24 hours (86400s)
User/Token90 days (7776000s)
Token grace period30 days (2592000s)

Rate Limiting Keys

PrefixDescription
rl:chat:b:s:{roomId}:{username}Short burst counter (10s window)
rl:chat:b:l:{roomId}:{username}Long burst counter (60s window)
rl:chat:b:last:{roomId}:{username}Last message timestamp
rl:block:{action}:{identifier}Rate limit block (24h)

Presence

User presence is tracked using Redis Sorted Sets (ZSETs) with timestamps as scores.

How It Works

  1. When a user joins/sends a message, their presence is updated:
   ZADD chat:presencez:{roomId} {timestamp} {username}
  1. Active users are retrieved by pruning expired entries:
   ZREMRANGEBYSCORE chat:presencez:{roomId} 0 {cutoffTimestamp}
   ZRANGE chat:presencez:{roomId} 0 -1
  1. Users are considered "offline" after 24 hours of inactivity

Presence Operations

// Set/refresh presence
await setRoomPresence(roomId, username);
await refreshRoomPresence(roomId, username);

// Remove presence
await removeRoomPresence(roomId, username);

// Get active users
const users = await getActiveUsersInRoom(roomId);

// Refresh room user count
const count = await refreshRoomUserCount(roomId);

Admin Cleanup

GET /api/chat-rooms?action=cleanupPresence
Headers: { X-Username: "ryo", Authorization: "Bearer <token>" }

Example Usage

Complete Flow: Create User and Send Message

// 1. Create user
const createRes = await fetch('/api/chat-rooms?action=createUser', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ username: 'alice', password: 'securepass123' })
});
const { user, token } = await createRes.json();

// 2. Get rooms
const roomsRes = await fetch('/api/chat-rooms?action=getRooms&username=alice');
const { rooms } = await roomsRes.json();

// 3. Join a room
await fetch('/api/chat-rooms?action=joinRoom', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ roomId: rooms[0].id, username: 'alice' })
});

// 4. Send a message
const msgRes = await fetch('/api/chat-rooms?action=sendMessage', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'X-Username': 'alice',
    'Authorization': `Bearer ${token}`
  },
  body: JSON.stringify({
    roomId: rooms[0].id,
    username: 'alice',
    content: 'Hello everyone!'
  })
});
const { message } = await msgRes.json();

// 5. Subscribe to real-time updates
const pusher = new Pusher(PUSHER_KEY, { cluster: PUSHER_CLUSTER });
const channel = pusher.subscribe(`room-${rooms[0].id}`);
channel.bind('room-message', (data) => {
  console.log('New message:', data.message);
});

Creating a Private Room

const res = await fetch('/api/chat-rooms?action=createRoom', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'X-Username': 'alice',
    'Authorization': `Bearer ${token}`
  },
  body: JSON.stringify({
    type: 'private',
    members: ['alice', 'bob', 'charlie']
  })
});
const { room } = await res.json();
// room.name = "@alice, @bob, @charlie"

Token Refresh Flow

// When token is about to expire or has expired (within grace period)
const res = await fetch('/api/chat-rooms?action=refreshToken', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({
    username: 'alice',
    oldToken: expiredToken
  })
});
const { token: newToken } = await res.json();

Error Responses

All errors return JSON with an error field:

{ "error": "Error message" }
StatusDescription
400Bad request (missing/invalid parameters)
401Unauthorized (invalid/missing token)
403Forbidden (insufficient permissions)
404Not found (room/user doesn't exist)
409Conflict (username already taken)
429Too many requests (rate limited)
500Internal server error

Configuration

Environment Variables

VariableDescription
REDIS_KV_REST_API_URLUpstash Redis REST URL
REDIS_KV_REST_API_TOKENUpstash Redis REST token
PUSHER_APP_IDPusher application ID
PUSHER_KEYPusher public key
PUSHER_SECRETPusher secret key
PUSHER_CLUSTERPusher cluster region

Runtime Configuration

export const runtime = "nodejs";
export const maxDuration = 15; // seconds

Related