ryOS ryOS / Docs
GitHub Launch

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

FunctionDescription
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

EndpointMethodsPurpose
/api/chatPOSTAI chat with streaming and tool calling
/api/speechPOSTText-to-speech synthesis
/api/audio-transcribePOSTSpeech-to-text (Whisper)

Media APIs

EndpointMethodsPurpose
/api/songsGET, POST, DELETESong list and batch operations
/api/songs/[id]GET, POST, DELETESingle song CRUD + lyrics
/api/youtube-searchPOSTYouTube video search
/api/parse-titlePOSTYouTube title parsing

AI Generation APIs

EndpointMethodsPurpose
/api/ie-generatePOSTInternet Explorer time-travel
/api/applet-aiPOSTApplet AI assistant

Communication APIs

EndpointMethodsPurpose
/api/roomsGET, POSTRooms list + create
/api/rooms/[id]GET, DELETERoom detail + delete
/api/rooms/[id]/messagesGET, POSTMessages
/api/rooms/[id]/messages/[msgId]DELETEDelete message (admin)
/api/messages/bulkGETBulk messages
/api/presence/switchPOSTPresence switching
/api/presence/heartbeatGET, POSTGlobal presence (authenticated): list online / heartbeat
/api/rooms/[id]/joinPOSTJoin room
/api/rooms/[id]/leavePOSTLeave room
/api/rooms/[id]/typingPOSTBroadcast typing indicator
/api/usersGETUser search
/api/ai/ryo-replyPOSTAI reply in rooms
/api/share-appletGET, POST, PATCH, DELETEApplet sharing/store

Listen APIs

EndpointMethodsPurpose
/api/listen/sessionsGET, POSTList/create listen-together sessions
/api/listen/sessions/[id]GETFetch session state
/api/listen/sessions/[id]/joinPOSTJoin session
/api/listen/sessions/[id]/leavePOSTLeave session
/api/listen/sessions/[id]/syncPOSTSync DJ playback state
/api/listen/sessions/[id]/reactionPOSTSend emoji reaction

Cloud Sync APIs

EndpointMethodsPurpose
/api/sync/statusGETBackup availability/status metadata
/api/sync/backupGET, POST, DELETEManual backup upload/download/delete
/api/sync/backup-tokenPOSTGenerate upload token for manual backup
/api/sync/domainsGETAggregated logical sync metadata plus per-physical-domain metadata
/api/sync/domains/[domain]GET, PUTDownload or update one logical sync domain across its physical parts
/api/sync/domains/[domain]/attachments/preparePOSTGenerate upload instructions for blob-backed logical sync parts

Telegram APIs

EndpointMethodsPurpose
/api/telegram/link/createPOSTCreate Telegram bot link
/api/telegram/link/disconnectPOSTDisconnect Telegram bot
/api/telegram/link/statusGETCheck Telegram link status
/api/webhooks/telegramPOSTTelegram bot webhook handler
/api/cron/telegram-heartbeatGETScheduled Telegram heartbeat

Utility APIs

EndpointMethodsPurpose
/api/iframe-checkGETCheck/proxy iframe embedding
/api/link-previewGETOpenGraph metadata extraction
/api/stocksGETStock quotes
/api/adminVariousAdmin operations (incl. usage analytics dashboard)

AirDrop APIs

EndpointMethodsPurpose
/api/airdrop/heartbeatPOSTRegister presence for AirDrop availability
/api/airdrop/discoverGETList nearby users available for AirDrop
/api/airdrop/sendPOSTSend file to recipient (max 2MB)
/api/airdrop/respondPOSTAccept 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.

ProviderModelsUse Cases
OpenAIgpt-5.4Default chat, code generation
Anthropicsonnet-4.6 (claude-sonnet-4-6)Complex reasoning
Googlegemini-3-flash, gemini-3.1-pro-previewImage 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

RouteActionDescription
GET /api/songsListSongs with filters
POST /api/songsCreate/ImportNew songs or bulk import
GET /api/songs/[id]ReadSong with lyrics, translations
POST /api/songs/[id]FetchBody: { action: "fetch-lyrics" }
POST /api/songs/[id]TranslateBody: { action: "translate-stream" }
POST /api/songs/[id]FuriganaBody: { action: "furigana-stream" }
DELETE /api/songs/[id]DeleteRemove 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

EndpointBurst LimitDaily/BudgetWindow
Chat AI (auth)15per 5 hoursSliding
Chat AI (anon)3per 24 hoursSliding
Speech TTS10/min50/dayFixed
IE Generate3/min10/5hrFixed
Applet AI (auth text)50per hourFixed
Applet AI (anon text)15per hourFixed
Applet AI (auth image)12per hourFixed
Applet AI (anon image)1per hourFixed
Transcribe10/min50/dayFixed
Parse Title15/min500/dayFixed
YouTube Search20/min200/dayFixed

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

HeaderValuePurpose
AuthorizationBearer <token>Authentication token
X-UsernameUsernameUser identification

Token Configuration

SettingValue
Token TTL90 days
Grace period30 days
Admin bypassUser "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

PrefixPurposeExample
rl:ai:AI rate limitingrl:ai:chat:user123
song:meta:Song metadatasong:meta:abc123
song:content:Song contentsong:content:abc123
ie:cache:IE generation cacheie:cache:hash
applet:share:Shared appletsapplet:share:xyz
chat:token:user:Auth tokenschat:token:user:alice:tok
chat:room:Chat room datachat:room:general
sync:meta:Backup metadatasync:meta:alice
sync:state:{username}:{domain}Redis-backed sync domain payloadssync:state:alice:contacts
sync:state:meta:{username}Redis-backed sync metadata mapsync:state:meta:alice
sync:auto:meta:Blob sync metadata (historical prefix)sync:auto:meta:alice
analytics:daily:Daily API metricsanalytics:daily:2026-03-10
analytics:uv:Unique visitor HLLanalytics:uv:2026-03-10
analytics:ep:Endpoint breakdownanalytics:ep:2026-03-10

Related Documentation