API Architecture
ryOS APIs are implemented as Vercel-style Node.js route handlers under api/. They run on Vercel serverless functions or via the standalone Bun API server for self-hosted deployments, with shared apiHandler and api/_utils primitives providing consistent CORS, method routing, auth, and error handling patterns.
Architecture Overview
graph TB
subgraph Client["Client Layer"]
UI[React UI]
Hooks[Custom Hooks]
end
subgraph API["API Runtime (Vercel Node.js + standalone Bun server)"]
Handler[apiHandler + _api/_utils]
ChatAPI[chat]
SongAPI[songs]
SpeechAPI[speech]
RoomsAPI[rooms]
MessagesAPI[rooms/messages]
ListenAPI[listen/sessions]
PresenceAPI[presence]
UsersAPI[users]
IEAPI[ie-generate]
AppletAPI[applet-ai]
end
subgraph AI["AI Providers"]
OpenAI[OpenAI]
Anthropic[Anthropic]
Google[Google]
end
subgraph Storage["Storage"]
Redis[(Redis
Upstash REST / Standard)]
ObjectStorage[(Object Storage
Vercel Blob / S3)]
end
subgraph Realtime["Real-time"]
RealtimeProvider[Pusher / Local WS]
end
UI --> Hooks
Hooks --> API
Handler --> ChatAPI
Handler --> SongAPI
Handler --> RoomsAPI
Handler --> ListenAPI
API --> AI
API --> Storage
API --> Realtime
RoomsAPI --> RealtimeProvider
API Directory Structure
api/
├── _utils/ # Shared API primitives
│ ├── api-handler.ts # Shared handler wrapper (CORS, methods, auth, errors)
│ ├── middleware.ts # Consolidated re-exports + request-context helpers
│ ├── request-auth.ts # Header parsing + auth resolution
│ ├── redis.ts # Redis client factory (Upstash REST + standard Redis/ioredis)
│ ├── storage.ts # Object storage adapter (Vercel Blob + S3-compatible)
│ ├── realtime.ts # Realtime event abstraction (Pusher + local WebSocket)
│ ├── runtime-config.ts # Client runtime config (origins, realtime, pusher)
│ ├── constants.ts # Shared constants (REDIS_PREFIXES, TTL, limits)
│ ├── _cors.ts # CORS helpers (incl. wildcard subdomain patterns)
│ ├── _ssrf.ts # SSRF-safe fetch/URL validation
│ ├── _rate-limit.ts # Rate limiting primitives
│ ├── _validation.ts # Input validation helpers
│ ├── _logging.ts # Structured request logging
│ ├── _analytics.ts # Lightweight per-day API usage analytics
│ ├── _sse.ts # SSE helpers
│ ├── _memory.ts # AI conversation memory helpers
│ ├── _song-service.ts # Song storage service
│ ├── _hash.ts # Hashing utilities
│ ├── _url.ts # URL utilities
│ ├── og-share.ts # OpenGraph share image generation
│ ├── auth/ # Token/password/auth helpers
│ ├── contacts.ts # Contacts sync helpers
│ ├── heartbeats.ts # Heartbeat scheduling helpers
│ ├── ryo-conversation.ts # Unified conversation preparation (web + Telegram)
│ ├── _cookie.ts # Auth cookie helpers
│ ├── telegram-format.ts # Telegram message formatting
│ ├── song-library-state.ts # Song library state helpers
│ └── telegram*.ts # Telegram bot integration helpers
├── airdrop/ # AirDrop file sharing endpoints
├── auth/ # Auth endpoints
├── rooms/ # Chat room endpoints
├── listen/ # Listen-together session endpoints
├── songs/ # Song library endpoints
├── messages/ # Bulk message endpoints
├── presence/ # Presence switching + heartbeat endpoints
├── users/ # User search endpoints
├── ai/ # AI helper endpoints
├── chat/ # Chat endpoint with tool definitions
├── sync/ # Cloud sync endpoints (backup + logical sync domains)
├── telegram/ # Telegram bot link/disconnect endpoints
├── cron/ # Scheduled tasks (telegram heartbeat)
├── webhooks/ # Webhook handlers (telegram)
└── [endpoint].ts # Individual top-level endpoints (chat, speech, ie-generate, etc.)
Middleware Utilities
The API layer standardizes route behavior through apiHandler plus shared api/_utils/ modules. This keeps CORS handling, method routing, auth resolution, and error handling consistent across refactored endpoints. CORS supports wildcard subdomain patterns (e.g. .example.com) via API_ALLOWED_ORIGINS.
Consolidated Imports
import {
// Route wrapper
apiHandler,
// Redis
createRedis,
// CORS
getEffectiveOrigin,
isAllowedOrigin,
setCorsHeaders,
// Auth
extractAuth,
extractAuthNormalized,
isAdmin,
// Constants
REDIS_PREFIXES,
TTL,
RATE_LIMIT_TIERS,
} from "../_utils/middleware.js";
apiHandler Pattern
Refactored routes use apiHandler() as the entry wrapper:
export default apiHandler<RequestBody>(
{
methods: ["POST"],
auth: "required",
parseJsonBody: true,
},
async ({ res, user, logger, startTime }) => {
logger.info("Request accepted", { username: user?.username });
logger.response(200, Date.now() - startTime);
res.status(200).json({ success: true });
}
);
apiHandler provides:
- Shared CORS + origin checks
- Method allow-list enforcement (
405 on unsupported methods) - Optional/required auth resolution via
request-auth.ts - Shared Redis/logger/request context for handlers
- Consistent
500 handling for uncaught exceptions
Specialized route wrappers
Routes should default to apiHandler(). If an endpoint needs multipart upload, webhook verification, or cron-only auth behavior, it should use a specialized wrapper that keeps the same shared entry flow instead of introducing a separate request-context pattern.
Auth Utilities
| Function | Description |
|---|
resolveRequestAuth(req, redis, { required, allowExpired }) | Resolves optional/required auth and validates token |
extractAuthNormalized(req) | Normalized header extraction (Authorization, X-Username) |
validateAuth(redis, username, token, options) | Low-level token validation + expiry/grace handling |
isAdmin(redis, username, token) | Admin check helper |
> See Also: API Design Guide for comprehensive patterns and examples.
API Endpoint Inventory
Core APIs
| Endpoint | Methods | Purpose |
|---|
/api/chat | POST | AI chat with streaming and tool calling |
/api/speech | POST | Text-to-speech synthesis |
/api/audio-transcribe | POST | Speech-to-text (Whisper) |
Media APIs
| Endpoint | Methods | Purpose |
|---|
/api/songs | GET, POST, DELETE | Song list and batch operations |
/api/songs/[id] | GET, POST, DELETE | Single song CRUD + lyrics |
/api/youtube-search | POST | YouTube video search |
/api/parse-title | POST | YouTube title parsing |
AI Generation APIs
| Endpoint | Methods | Purpose |
|---|
/api/ie-generate | POST | Internet Explorer time-travel |
/api/applet-ai | POST | Applet AI assistant |
Communication APIs
| Endpoint | Methods | Purpose |
|---|
/api/rooms | GET, POST | Rooms list + create |
/api/rooms/[id] | GET, DELETE | Room detail + delete |
/api/rooms/[id]/messages | GET, POST | Messages |
/api/rooms/[id]/messages/[msgId] | DELETE | Delete message (admin) |
/api/messages/bulk | GET | Bulk messages |
/api/presence/switch | POST | Presence switching |
/api/presence/heartbeat | GET, POST | Global presence (authenticated): list online / heartbeat |
/api/rooms/[id]/join | POST | Join room |
/api/rooms/[id]/leave | POST | Leave room |
/api/rooms/[id]/typing | POST | Broadcast typing indicator |
/api/users | GET | User search |
/api/ai/ryo-reply | POST | AI reply in rooms |
/api/share-applet | GET, POST, PATCH, DELETE | Applet sharing/store |
Listen APIs
| Endpoint | Methods | Purpose |
|---|
/api/listen/sessions | GET, POST | List/create listen-together sessions |
/api/listen/sessions/[id] | GET | Fetch session state |
/api/listen/sessions/[id]/join | POST | Join session |
/api/listen/sessions/[id]/leave | POST | Leave session |
/api/listen/sessions/[id]/sync | POST | Sync DJ playback state |
/api/listen/sessions/[id]/reaction | POST | Send emoji reaction |
Cloud Sync APIs
| Endpoint | Methods | Purpose |
|---|
/api/sync/status | GET | Backup availability/status metadata |
/api/sync/backup | GET, POST, DELETE | Manual backup upload/download/delete |
/api/sync/backup-token | POST | Generate upload token for manual backup |
/api/sync/domains | GET | Aggregated logical sync metadata plus per-physical-domain metadata |
/api/sync/domains/[domain] | GET, PUT | Download or update one logical sync domain across its physical parts |
/api/sync/domains/[domain]/attachments/prepare | POST | Generate upload instructions for blob-backed logical sync parts |
Telegram APIs
| Endpoint | Methods | Purpose |
|---|
/api/telegram/link/create | POST | Create Telegram bot link |
/api/telegram/link/disconnect | POST | Disconnect Telegram bot |
/api/telegram/link/status | GET | Check Telegram link status |
/api/webhooks/telegram | POST | Telegram bot webhook handler |
/api/cron/telegram-heartbeat | GET | Scheduled Telegram heartbeat |
Utility APIs
| Endpoint | Methods | Purpose |
|---|
/api/iframe-check | GET | Check/proxy iframe embedding |
/api/link-preview | GET | OpenGraph metadata extraction |
/api/stocks | GET | Stock quotes |
/api/admin | Various | Admin operations (incl. usage analytics dashboard) |
AirDrop APIs
| Endpoint | Methods | Purpose |
|---|
/api/airdrop/heartbeat | POST | Register presence for AirDrop availability |
/api/airdrop/discover | GET | List nearby users available for AirDrop |
/api/airdrop/send | POST | Send file to recipient (max 2MB) |
/api/airdrop/respond | POST | Accept or decline incoming transfer |
Frontend API Client Layer
Client-side API access is centralized in src/api/:
src/api/core.ts - shared request wrapper, auth headers, error normalization
AI Provider Abstraction
Supported Models (AI SDK 6.0)
The API uses Vercel AI SDK 6.0 with structured outputs for type-safe responses.
| Provider | Models | Use Cases |
|---|
| OpenAI | gpt-5.4 | Default chat, code generation |
| Anthropic | sonnet-4.6 (claude-sonnet-4-6) | Complex reasoning |
| Google | gemini-3-flash, gemini-3.1-pro-preview | Image generation, title parsing |
Structured Outputs: Used for deterministic responses like song title parsing (/api/parse-title).
Model Selection
// api/_utils/_aiModels.ts
export const getModelInstance = (model: SupportedModel): LanguageModel => {
switch (model) {
case "gpt-5.4":
return openai("gpt-5.4");
case "sonnet-4.6":
return anthropic("claude-sonnet-4-6");
case "gemini-3-flash":
return google("gemini-3-flash-preview");
case "gemini-3.1-pro-preview":
return google("gemini-3.1-pro-preview");
}
};
Chat API
Streaming Architecture
sequenceDiagram
participant Client
participant API as /api/chat
participant AI as AI Provider
participant Tools as Tool Handlers
Client->>API: POST (messages, model)
API->>AI: streamText()
loop Stream Response
AI-->>API: Token/Tool Call
alt Tool Call
API->>Tools: Execute tool
Tools-->>API: Tool result
API->>AI: Continue with result
else Text Token
API-->>Client: SSE chunk
end
end
API-->>Client: Stream complete
Tool Calling System
The chat API provides tools for system control:
tools: {
launchApp: {
description: "Launch ryOS application",
parameters: z.object({
appId: z.enum(["finder", "textedit", "ipod", ...]),
initialData: z.unknown().optional(),
}),
},
ipodControl: {
description: "Control iPod playback",
parameters: z.object({
action: z.enum(["toggle", "play", "pause", "next", "previous", ...]),
title: z.string().optional(),
artist: z.string().optional(),
}),
},
generateHtml: {
description: "Generate HTML applet",
parameters: z.object({
title: z.string(),
icon: z.string(),
code: z.string(),
}),
},
// ... more tools
}
System Prompts
// _api/_utils/_aiPrompts.ts
export const CORE_PRIORITY_INSTRUCTIONS = `
You are Ryo, an AI assistant in ryOS...
`;
export const RYO_PERSONA_INSTRUCTIONS = `
Ryo's personality and background...
`;
export const CODE_GENERATION_INSTRUCTIONS = `
HTML applet generation rules...
`;
export const TOOL_USAGE_INSTRUCTIONS = `
VFS and tool usage patterns...
`;
Song API
Split Storage Architecture
flowchart TB
subgraph "Request"
REQ[API Request]
end
subgraph "Redis Storage"
META[(song:meta:*
Lightweight)]
CONTENT[(song:content:*
Heavy Data)]
SET[(song:all
ID Set)]
end
REQ --> |List/Search| META
REQ --> |Full Song| META
META --> |UUID Lookup| CONTENT
REQ --> |All IDs| SET
Endpoints
| Route | Action | Description |
|---|
GET /api/songs | List | Songs with filters |
POST /api/songs | Create/Import | New songs or bulk import |
GET /api/songs/[id] | Read | Song with lyrics, translations |
POST /api/songs/[id] | Fetch | Body: { action: "fetch-lyrics" } |
POST /api/songs/[id] | Translate | Body: { action: "translate-stream" } |
POST /api/songs/[id] | Furigana | Body: { action: "furigana-stream" } |
DELETE /api/songs/[id] | Delete | Remove song |
Speech APIs
Text-to-Speech
// _api/speech.ts
const providers = {
openai: {
models: ["tts-1", "tts-1-hd"],
voices: ["alloy", "echo", "fable", "onyx", "nova", "shimmer"],
},
elevenlabs: {
models: ["eleven_multilingual_v2", "eleven_turbo_v2"],
voices: ["custom voice IDs"],
},
};
// Dual provider support
if (model === "elevenlabs") {
return generateElevenLabsSpeech(text, voiceId, modelId);
} else {
return generateSpeech({
model: openai.speech("tts-1"),
text,
voice,
});
}
Audio Transcription
// _api/audio-transcribe.ts
const transcription = await openai.audio.transcriptions.create({
file: audioFile,
model: "whisper-1",
});
Rate Limiting
Counter-Based Limiting
// _api/_utils/_rate-limit.ts
export async function checkCounterLimit({
key,
windowSeconds,
limit,
}: CounterLimitArgs): Promise<CounterLimitResult> {
// Atomic increment
const newCount = await redis.incr(key);
// Set expiry on first request
if (newCount === 1) {
await redis.expire(key, windowSeconds);
}
const ttl = await redis.ttl(key);
if (newCount > limit) {
return {
allowed: false,
count: newCount,
remaining: 0,
resetSeconds: ttl,
};
}
return {
allowed: true,
remaining: limit - newCount,
};
}
Rate Limit Configurations
| Endpoint | Burst Limit | Daily/Budget | Window |
|---|
| Chat AI (auth) | 15 | per 5 hours | Sliding |
| Chat AI (anon) | 3 | per 24 hours | Sliding |
| Speech TTS | 10/min | 50/day | Fixed |
| IE Generate | 3/min | 10/5hr | Fixed |
| Applet AI (auth text) | 50 | per hour | Fixed |
| Applet AI (anon text) | 15 | per hour | Fixed |
| Applet AI (auth image) | 12 | per hour | Fixed |
| Applet AI (anon image) | 1 | per hour | Fixed |
| Transcribe | 10/min | 50/day | Fixed |
| Parse Title | 15/min | 500/day | Fixed |
| YouTube Search | 20/min | 200/day | Fixed |
Authentication
Token Validation
// _api/_utils/auth/_validate.ts
export async function validateAuth(
redis: RedisLike,
username: string,
authToken: string,
options: { allowExpired?: boolean; refreshOnGrace?: boolean } = {}
): Promise<AuthValidationResult> {
// 1. Check active token
const userKey = getUserTokenKey(username, authToken);
const exists = await redis.exists(userKey);
if (exists) {
// Refresh TTL on use
await redis.expire(userKey, USER_TTL_SECONDS);
return { valid: true, expired: false };
}
// 2. Check grace period for expired tokens
if (options.allowExpired) {
const lastTokenKey = getLastTokenKey(username);
const lastTokenData = await redis.get(lastTokenKey);
// ... grace period logic
}
return { valid: false };
}
Auth Headers
| Header | Value | Purpose |
|---|
Authorization | Bearer <token> | Authentication token |
X-Username | Username | User identification |
Token Configuration
| Setting | Value |
|---|
| Token TTL | 90 days |
| Grace period | 30 days |
| Admin bypass | User "ryo" |
CORS Handling
// api/_utils/_cors.ts
export function isAllowedOrigin(origin: string | null): boolean {
if (!origin) return false;
// Always allow Tailscale origins
if (isTailscaleOrigin(origin)) return true;
const env = getRuntimeEnv();
const configuredOrigins = getConfiguredAllowedOrigins();
// Explicit self-host allowlist (API_ALLOWED_ORIGINS) takes precedence.
// Supports wildcard subdomain patterns like "*.example.com".
if (configuredOrigins.allowAll) return true;
if (configuredOrigins.origins.has(normalizedOrigin)) return true;
if (configuredOrigins.subdomainSuffixes.some(s => hostname.endsWith(s))) return true;
// Fallback to environment-based checks
if (env === "production") return origin === PROD_ALLOWED_ORIGIN;
if (env === "preview") return isVercelPreviewOrigin(origin);
return isLocalhostOrigin(origin);
}
Response Patterns
Response Helpers
The middleware provides standardized response helpers for consistent API responses:
// JSON response with optional CORS and extra headers
jsonResponse(data, status = 200, origin?, extraHeaders?)
// Error response with optional code and details
errorResponse(message, status = 400, origin?, code?, details?)
// Success response (includes { success: true })
successResponse(data = {}, status = 200, origin?)
// Rate limit response with retry headers
rateLimitResponse(origin, limit, resetSeconds, scope?)
Example Usage:
// Success with data
return jsonResponse({ items: data }, 200, origin);
// Success response
return successResponse({ message: "Created" }, 201, origin);
// Error with code
return errorResponse("Validation failed", 400, origin, "VALIDATION_ERROR", { field: "email" });
// Rate limit exceeded
return rateLimitResponse(origin, 30, 60, "burst");
Error Response Format
All error responses follow a consistent format:
{
"error": "Human-readable error message",
"code": "MACHINE_READABLE_CODE",
"details": { "field": "value" }
}
Rate limit errors include additional fields:
{
"error": "rate_limit_exceeded",
"limit": 30,
"retryAfter": 45,
"scope": "burst"
}
SSE Streaming Response
const stream = new ReadableStream({
async start(controller) {
for await (const chunk of aiStream) {
controller.enqueue(`data: ${JSON.stringify(chunk)}\n\n`);
}
controller.enqueue("data: [DONE]\n\n");
controller.close();
},
});
return new Response(stream, {
headers: {
"Content-Type": "text/event-stream",
"Cache-Control": "no-cache",
"Connection": "keep-alive",
},
});
Runtime Configuration
Route modules export Vercel-compatible runtime metadata:
export const runtime = "nodejs";
export const maxDuration = 60; // seconds (80 for AI endpoints)
- On Vercel, these exports configure serverless execution.
- On the standalone Bun server,
scripts/api-standalone-server.ts loads and executes the same _api/* handlers via a request/response adapter. - API development/runtime scripts use Bun (
bun run dev:api, bun run api:start).
Redis Key Patterns
| Prefix | Purpose | Example |
|---|
rl:ai: | AI rate limiting | rl:ai:chat:user123 |
song:meta: | Song metadata | song:meta:abc123 |
song:content: | Song content | song:content:abc123 |
ie:cache: | IE generation cache | ie:cache:hash |
applet:share: | Shared applets | applet:share:xyz |
chat:token:user: | Auth tokens | chat:token:user:alice:tok |
chat:room: | Chat room data | chat:room:general |
sync:meta: | Backup metadata | sync:meta:alice |
sync:state:{username}:{domain} | Redis-backed sync domain payloads | sync:state:alice:contacts |
sync:state:meta:{username} | Redis-backed sync metadata map | sync:state:meta:alice |
sync:auto:meta: | Blob sync metadata (historical prefix) | sync:auto:meta:alice |
analytics:daily: | Daily API metrics | analytics:daily:2026-03-10 |
analytics:uv: | Unique visitor HLL | analytics:uv:2026-03-10 |
analytics:ep: | Endpoint breakdown | analytics:ep:2026-03-10 |
Related Documentation