ryOS ryOS / Docs
GitHub Launch

API Architecture

ryOS APIs are implemented as Node-style route handlers under api/, served by the standalone Bun API server, 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 (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
S3 compatible)] 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)
│   ├── request-auth.ts        # Header parsing + auth resolution
│   ├── redis.ts               # Redis client factory (Upstash REST + standard Redis/ioredis)
│   ├── storage.ts             # Object storage adapter (S3 compatible)
│   ├── realtime.ts            # Realtime event abstraction (Pusher + local WebSocket)
│   ├── runtime-config.ts      # Client runtime config (origins, realtime, pusher)
│   ├── _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.

Imports

Routes import directly from the focused api/_utils/* modules (there is no barrel re-export module):

import { apiHandler } from "../_utils/api-handler.js";
import { createRedis } from "../_utils/redis.js";
import { getEffectiveOrigin, isAllowedOrigin, setCorsHeaders } from "../_utils/_cors.js";
import { extractAuth, extractAuthNormalized } from "../_utils/auth/index.js";

Domain-specific constants (Redis key prefixes, TTLs) live in their owning module, e.g. api/rooms/_helpers/_constants.ts and api/_utils/auth/_constants.ts.

apiHandler Pattern

Refactored routes use apiHandler() as the entry wrapper:

export default apiHandler<RequestBody>(
  {
    methods: ["POST"],
    auth: "required",
    bodySchema: RequestBodySchema, // Zod schema (implies parseJsonBody)
  },
  async ({ res, user, body, 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
  • Optional bodySchema request-body validation: any object with a safeParse(data) method works, and a Zod schema (z.object(...)) satisfies it directly. The parsed/typed body is exposed as body; invalid bodies short-circuit with 400 { error: "validation_error", issues }. Setting bodySchema implies parseJsonBody.
  • 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

Authentication APIs

EndpointMethodsPurpose
/api/auth/registerPOSTCreate user + issue session cookie
/api/auth/loginPOSTLogin with password
/api/auth/logoutPOSTLogout current session
/api/auth/logout-allPOSTLogout all sessions
/api/auth/sessionGETRestore session from httpOnly cookie
/api/auth/token/verifyPOSTVerify token
/api/auth/token/refreshPOSTRefresh session cookie
/api/auth/password/checkGETCheck if password set
/api/auth/password/setPOSTSet or update password
/api/auth/tokensGETList active tokens
/api/auth/recovery/requestPOSTRequest account recovery code
/api/auth/recovery/resetPOSTReset password with recovery code
/api/auth/email/setPOSTSet recovery email
/api/auth/email/verifyPOSTVerify recovery email
/api/auth/email/removePOSTRemove recovery email
/api/auth/email/statusGETRead recovery email status
/api/auth/account/deletePOSTDelete authenticated account

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
/api/ai/extract-memoriesPOSTExtract durable memories from daily notes
/api/ai/process-daily-notesPOSTProcess recent daily notes into memory records
/api/ai/cursor-run-statusGETRead Cursor cloud-agent run status
/api/ai/cursor-run-followupPOSTSend a follow-up prompt to a Cursor cloud-agent run

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

| /api/listen/sessions/[id]/transfer-host | POST | Transfer session host | | /api/listen/sessions/[id]/assign-dj | POST | Assign the session DJ | | /api/listen/sessions/[id]/remote-command | POST | Submit remote playback command |

IRC APIs

EndpointMethodsPurpose
/api/irc/serversGET, POSTList/create IRC server configs
/api/irc/servers/[id]DELETERemove IRC server config (admin)
/api/irc/servers/[id]/channelsGETBrowse IRC channels

Apple Services APIs

EndpointMethodsPurpose
/api/mapkit-tokenGETMapKit JS token
/api/musickit-tokenGETMusicKit JS token
/api/apple-music-artworkGETApple Music artwork proxy

Analytics & TV APIs

EndpointMethodsPurpose
/api/analytics/eventsPOSTRecord lightweight client analytics events
/api/tv/create-channelPOSTAI-assisted TV channel creation
/api/currency-rateGETCurrency conversion rates

Cloud Sync APIs

EndpointMethodsPurpose
/api/sync/v2/opsPOSTApply a batch of sync ops (per-key last-writer-wins)
/api/sync/v2/changesGETJournal ops after the client cursor (?since=)
/api/sync/v2/snapshotGETFull key-value sync state + current cursor
/api/sync/v2/blobsPOSTBatched content-addressed blob prepare/dedupe + download signing
/api/sync/auto-sync-preferenceGET, PUTCross-device Auto Sync toggle
/api/cron/sync-maintenanceGETCron: content-addressed blob GC + user-record healing (CRON_SECRET)

Telegram APIs

EndpointMethodsPurpose
/api/telegram/link/createPOSTCreate Telegram bot link
/api/telegram/link/disconnectPOSTDisconnect Telegram bot
/api/telegram/link/statusGETCheck Telegram link status
/api/telegram/heartbeat-settingsGET, POSTRead/update Telegram heartbeat DM settings
/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

Model IDs and metadata are defined in src/shared/aiModels.ts. Server provider wiring is in api/_utils/_aiModels.ts.

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.5Default 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.5":
      return openai("gpt-5.5");
    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: ToolLoopAgent.stream()
    
    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(),
    }),
  },
  
  mediaControl: {
    description: "Control music/karaoke/videos/TV playback",
    parameters: z.object({
      target: z.enum(["music", "karaoke", "videos", "tv"]).optional(),
      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[(media:song:{id}:meta
Lightweight)] CONTENT[(media:song:{id}:content
Heavy Data)] SET[(media:song:ids
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 hoursFixed window
Chat AI (anon)3per 24 hoursFixed window
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 TTL1 year (refreshed on each validated request)
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;
  return isLocalhostOrigin(origin);
}

Response Patterns

Handlers use apiHandler() plus res.status().json() / res.status().end(). Rate-limit responses are built inline in route handlers using _rate-limit.ts primitives. Error bodies typically follow { error, code?, details? }.

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

  • API development/runtime scripts use Bun (bun run dev:api, bun run api:start).

Realtime Provider Modes

Realtime delivery is selected at runtime:

ProviderAuth routeTransportNotes
pusher (default)POST /api/pusher/authPusher private/presence channelsUsed by production web deployments
localPOST /api/realtime/ticketStandalone Bun WebSocket + Redis pub/sub bridgeEnabled by REALTIME_PROVIDER=local and REALTIME_WS_PATH

Cloud Sync v2 publishes sync-ops events on per-user sync channels; small remote changes can be applied directly from the realtime payload without an extra HTTP fetch.

Redis Key Patterns

PrefixPurposeExample
rate:*Rate limitingrate:ai:5h:user:{hash}
media:song:*Song metadata/contentmedia:song:abc123:meta
cache:ie:*IE generation cachecache:ie:{hash}:2026:versions
media:applet:share:*Shared appletsmedia:applet:share:xyz
auth:session:*Auth sessionsauth:session:{tokenHash}
chat:rooms:*Chat room metadata/messages/presencechat:rooms:general:messages
sync:backup:user:*Manual backup metadatasync:backup:user:alice:meta
sync:v2:user:{username}:seqSync v2 journal cursor (op counter)sync:v2:user:alice:seq
sync:v2:user:{username}:kvSync v2 key-value state (hash: key -> entry)sync:v2:user:alice:kv
sync:v2:user:{username}:journalSync v2 op journalsync:v2:user:alice:journal
sync:v2:user:{username}:blobsSync v2 content-hash blob registrysync:v2:user:alice:blobs
sync:maintenance:cursorSync maintenance cron scan cursorsync:maintenance:cursor
analytics:api:*Daily API metricsanalytics:api:requests:2026-03-10
analytics:product:*Product analytics metricsanalytics:product:unique-users:2026-03-10

Related Documentation