Skip to content

Data schemas

The server stores internal user IDs but serializes identities for clients. Unless noted otherwise, user fields in public messages are usernames.

interface PublicUser {
username: string;
roles: string[];
color: string | null;
nickname?: string;
status?: { status: 'online' | 'idle' | 'dnd' | 'offline' | 'invisible'; text: string };
}

The first role with a color determines color; role ordering therefore matters. Administrative responses can include internal account fields such as last_login.

Handshake, message, poll, status, unread, and signing timestamps are Unix seconds; fractional seconds are possible. user_timeout.duration and slow_mode_cooldown are seconds. A rate_limit.length value is milliseconds. The typing.duration field is forwarded unchanged and has no server-assigned unit.

interface Channel {
name: string;
type: 'text' | 'voice' | 'forum' | 'separator';
display_name?: string;
description?: string;
/** For separators, the UI spacing/padding rendered around the separator. */
size?: number;
/** Stored/serialized metadata; not enforced by the current server. */
max_message_age?: number;
slow_mode_cooldown?: number;
permissions: Record<'view' | 'send' | 'delete' | 'delete_own' | 'edit' | 'edit_own' | 'react' | 'pin' | 'create_thread', string[]>;
last_message?: number | null;
last_message_id?: string | null;
last_message_by_user?: number | null;
threads?: Thread[];
voice_state?: Array<{ username: string; muted: boolean }>;
}

Only fields relevant to the channel type appear. Separators are preserved in visible channel lists for layout.

For separator channels, size is explicitly a client presentation value. It represents the padding or vertical spacing around the separator; it is not a member, message, or storage limit.

interface Message {
type: 'message';
id: string;
user: string;
timestamp: number;
content: string;
thread_id?: string;
edited?: true;
edited_by?: string;
embeds?: unknown[];
attachments?: Attachment[];
reactions?: Record<string, string[]>;
pinned?: boolean;
pings?: unknown;
reply_to?: { user: string; id: string; preview: string };
ping?: boolean;
interaction?: { username: string; command: string; args?: unknown[]; author_id?: string; key_id?: string; signature?: string; timestamp?: number; nonce?: string };
author_id?: string;
key_id?: string;
signature?: string;
signed_at?: number;
}

Replies retain only a 40-character preview. A ping value on a public message reflects whether its reply pings the original author. Polls are messages with an embed whose type is poll; the server hydrates poll details for the viewer.

interface Thread {
id: string;
parent_channel: string;
name: string;
created_by: string;
created_at: number;
participants: string[];
archived: boolean;
locked: boolean;
pinned?: boolean;
last_message?: number | null;
last_message_id?: string | null;
}
interface Role {
id: string;
description: string;
color: string | null;
gradient: unknown[] | null;
permissions: string[];
hoisted: boolean;
self_assignable: boolean;
category: string | null;
position: number;
}

Roles are returned as an object keyed by role name. owner, admin, and user are protected system roles. The default owner role has owner and administrator; the permission helper treats these as broad grants.

type ErrorFrame = { cmd: 'error'; val: string; src?: string; listener?: unknown };
type RateLimitFrame = { cmd: 'rate_limit'; reason?: string; length: number };

See signing and verification for the exact canonical payload represented by author_id, key_id, signature, and signed_at.