Skip to content

Architecture

This page is a map of the originchats-osl source for contributors. It shows where each part of the server lives, what happens at startup, and the path a WebSocket command takes from arrival to response.

OSL compiles the whole of src/ into one Go program. main.osl pulls every other module in with import, and all modules share one namespace, so a function defined in any file can be called from any other.

Path Responsibility
src/main.osl Command line (init and use), startup order, background threads, HTTP routes, and the WebSocket server. It’s the only file that imports std: packages.
src/config/ Defaults, loading, validation, automod rule compilation, and config_update merging.
src/db/ Everything on disk: one directory per resource (users/, channels/, messages/, threads/, and so on), plus shared storage in storage.osl and transactions.osl.
src/db/setup/ The init wizard and the initial database contents.
src/db/migrations/ Converters for older storage layouts.
src/db/shards/, src/db/message_index/ Numbered JSONL message files and their rebuildable index. See Persistence.
src/api/index.osl Parses frames, checks access, dispatches commands, and routes responses.
src/api/access.osl Tables of which commands need which permission, which are blocked during a timeout, and which work before login.
src/api/connections.osl Connect and disconnect hooks and the heartbeat.
src/api/handlers/<group>/ One file per protocol command, such as handlers/webhooks/webhook_create.osl.
src/api/helpers/<area>/ Rules shared between handlers: assets, auth, automod, channels, link embeds, messages, notifications, polls, rate limits, reactions, reports, roles, server maintenance, slash commands, threads, uploads, users, voice, and webhooks.
src/slash/ The built-in slash commands. Each file registers itself at import time.
src/plugins/ OSL plugin hooks and the JavaScript plugin runtime.
src/attachments/, src/avatars/, src/assets/ HTTP upload and download for attachments, local account avatars, server icons, emoji, and stickers. attachments/storage.osl holds the local and S3 backends.
src/webhooks/ Incoming webhook HTTP endpoints.
src/messages/ The HTTP message page at /messages/{channel}/{message}.
src/pages/ Public HTML pages: /, /info, /user/{username}, and /invite/{code}.
src/utils/ Response builders (objErr, channelResponse), broadcast helpers, and logging.

Tests (*.test.osl) and benchmarks sit next to the code they cover. See Development workflow.

main.osl runs top to bottom. For use <directory>, it:

  1. Checks the directory, changes into it, and loads .env.
  2. Loads and validates config.json, then resolves the server icon and banner.
  3. Imports the database, slash command, plugin, API, and HTTP modules. Built-in slash commands register here.
  4. Creates the S3 client and exits if S3 storage is selected but not configured.
  5. Undoes any interrupted file transaction (dbRecover).
  6. Loads users, roles, channels, webhooks, access settings, and unread state into memory (dbSeedMemory).
  7. Builds mention history if needed (mentionsMigrate), then migrates attachment details if needed (attachmentMigrate).
  8. Loads saved timeouts and JavaScript plugins.
  9. Starts background threads: timeout expiry (every second), the Rotur ban check (every hour), four link preview workers, and the WebSocket heartbeat (every 30 seconds).
  10. Registers HTTP routes, attaches the WebSocket server at /, and listens.

A failure in step 1, 2, or 4 to 7 stops the server. A missing or unreadable core file such as users.json in step 6 stops it with Missing required database file: <path> or Invalid JSON in <path>: .... See Updating for what the migrations do.

  1. Connect. onConnect sends the handshake frame.
  2. Parse. onMessage parses the frame as JSON. Invalid JSON gets Invalid message format.
  3. Check access. handleCmd calls apiAccessError. It requires a login (except for auth, login, register, and application_submit), current membership, and the command’s permission from COMMAND_PERMISSIONS or COMMAND_ALTERNATIVES.
  4. Check timeouts. If the command is in TIMEOUT_BLOCKED and the user is timed out, the reply is a rate_limit frame.
  5. Plugins. JavaScript plugins can block or rewrite the request (jsPluginPre), and OSL plugins wrap the dispatch (pluginHandle).
  6. Dispatch. dispatchCmd matches cmd to a handler. An unknown command returns no frames.
  7. Handle. The handler validates the request, checks resource rules, changes state, and returns a list of frames. If it throws, the text becomes an error frame.
  8. Refresh. Commands in METADATA_REFRESH_COMMANDS, such as role changes, trigger metadataRefresh.
  9. Route. Each frame gets the request’s listener. A frame marked global: true goes to the caller, then loses its listener and goes to every other logged-in connection that can see its channel and isn’t severed from the caller. Other frames go to the caller only.
  10. Follow-up. linkEmbedSchedule queues link previews for new messages, and the request is logged.

Handlers never send the caller’s copy of a broadcast themselves. They return it, and the router does the rest.

Handlers stay thin by calling shared helpers:

  • Message handlers share messageSchema and schemaMessageId for request fields, and messageContext for channel and thread access.
  • channelResponse builds {cmd, channel} responses and adds thread_id for thread messages.
  • Pinning and unpinning share messagePinChange.
  • Bans, timeouts, and severs share userBanChange, timeoutApply, and userSeverChange between WebSocket handlers and slash commands. These own owner protection, audit entries, and disconnects.
  • connectionActor turns a connection into the actor recorded in audit entries.
  • refreshChannels sends each connection an updated channel list after a channel change.
Kept on disk and reloaded at startup Kept in memory only
Config, users, roles, channels, threads, messages, polls, unreads, mention history, webhooks, reports, audit log, attachment details, access settings, timeouts, custom emoji and stickers Open connections, presence, client and device details, voice channel members, upload tokens, pending slash calls from bots, and bot-registered slash commands

Users, roles, channels, webhooks, and access settings are read from memory and written through on every change. Messages are read from disk through bounded caches. See Persistence.

Login completion, disconnects, status changes, and permission refreshes take the presence_events lock, so presence frames go out in order. The list of connections per user is guarded by the connections lock. Saving users uses its own users lock and rollback.

Release the connections lock and any storage lock before sending frames. Only the outer presence_events lock should be held while sending, to keep ordering. Public clients and devices lists are computed from userConns each time, not stored in a second index.