Data Types Reference¶
Partly out of date
Rendered from ProjectInfo/data-types.md on docs/encryption-modes-spec. Its protocol content is still useful, but parts of it describe an older client or an earlier API. Where it disagrees with the code, the code wins — see the spec errata.
Uses the design names for encryption modes. The wire values are persistent and secure.
⚠️ Where this document and Encryption Modes disagree, that document wins. It reconciles the mode names, key distribution and media model across the specs and the running code, which have diverged.
EncryptionModehere uses the spec names; the database enum currently usespersistent/secure.
All types returned by the API as JSON. UUIDs are lowercase hyphenated strings. Timestamps are ISO 8601.
User¶
interface User {
id: string;
username: string;
display_name: string | null;
avatar_url: string | null; // relative attachment path e.g. /media/attachments/{id}
banner_url: string | null;
bio: string | null;
email: string | null; // only on GET /api/users/@me
verified: boolean;
bot: boolean;
status: 1 | 2 | 3 | 4 | 5; // ONLINE=1, IDLE=2, DND=3, OFFLINE=4, INVISIBLE=5
status_text: string | null;
created_at: string;
}
Server¶
interface Server {
id: string;
name: string;
description: string | null;
icon_url: string | null;
banner_url: string | null;
owner_id: string;
member_count: number;
verification_level: 0 | 1 | 2 | 3 | 4;
explicit_filter: 0 | 1 | 2;
system_channel_id: string | null;
rules_channel_id: string | null;
afk_channel_id: string | null;
afk_timeout_seconds: number;
created_at: string;
}
Channel¶
type ChannelType = "text" | "voice" | "announcement" | "stage" | "category" | "dm" | "group_dm";
// Three encryption modes — set at channel creation, cannot be changed later.
type EncryptionMode =
| "server_encrypted" // Vault KEK; server can read; full history on join
| "rolling_key" // client epoch keys; server cannot read; no history before join
| "peer_provided"; // no server key storage; key delivered P2P; server cannot read
interface Channel {
id: string;
server_id: string | null; // null for DMs
parent_id: string | null; // category parent
type: ChannelType;
name: string;
topic: string | null;
position: number;
nsfw: boolean;
slowmode_seconds: number;
bitrate: number; // voice channels (default 64000)
user_limit: number; // 0 = unlimited
last_message_id: string | null;
encryption_mode: EncryptionMode;
ck_version: number; // for rolling_key: current epoch number; 0 = no epoch yet
created_at: string;
updated_at: string;
}
Encryption mode summary:
| Mode | Who holds key | Server can read | History on join |
|---|---|---|---|
server_encrypted |
Server (Vault KEK) | Yes — disclosed to users | Full |
rolling_key |
Clients only (epoch keys) | No | None |
peer_provided |
Clients only (P2P delivery) | No | None |
ChannelEpoch¶
Returned by POST /api/channels/{id}/epoch. Represents one key rotation record for a rolling_key channel.
interface ChannelEpoch {
id: string; // UUID identifying this epoch
channel_id: string;
epoch_num: number; // 1-based counter; increments monotonically
created_by: string; // user_id of the client that issued the rotation
created_at: string;
member_hash: string; // SHA-256 of sorted server member user_id list at rotation time
}
EpochKeyResponse¶
Returned by GET /api/channels/{id}/epoch. Contains the caller's sealed epoch key for the current epoch.
interface EpochKeyResponse {
epoch_id: string; // UUID of the current epoch
epoch_num: number;
encrypted_key: string; // base64 — 32-byte epoch key sealed with caller's device IK X25519
}
EpochGrant¶
Used in POST /api/channels/{id}/epoch and POST /api/channels/{id}/epoch/{epochID}/grants request bodies.
interface EpochGrant {
user_id: string;
device_id: string;
encrypted_key_b64: string; // base64 — 32-byte epoch key sealed with device IK X25519
}
PermissionOverwrite¶
interface PermissionOverwrite {
id: string;
channel_id: string;
target_id: string; // role or member user ID
target_type: "role" | "member";
allow: number; // int64 bitfield — permissions explicitly granted
deny: number; // int64 bitfield — permissions explicitly denied
}
Resolution order: @everyone → role permissions → channel overwrite (role deny → role allow → member deny → member allow). Administrator short-circuits all checks.
ReadState¶
interface ReadState {
channel_id: string;
user_id: string;
last_read_id: string | null; // ID of the last acknowledged message
mention_count: number; // unread @mentions since last ack
updated_at: string;
}
Role¶
interface Role {
id: string;
server_id: string;
name: string;
color: number; // RGB packed integer, e.g. 0xFF5733
hoist: boolean;
mentionable: boolean;
position: number;
permissions: number; // int64 bitfield
managed: boolean;
created_at: string;
}
ServerMember¶
interface ServerMember {
server_id: string;
user_id: string;
username: string;
avatar_url: string | null;
nickname: string | null;
server_avatar: string | null;
deaf: boolean;
mute: boolean;
timeout_until: string | null;
joined_at: string;
roles: string[]; // role IDs
}
Note: The server returns a flat struct — NOT
{ user: User, ... }. Usemember.user_id,member.usernamedirectly.
MessageResponse¶
The content field and decryption behavior depend on encryption_mode:
server_encrypted:contentis the decrypted plaintext. Server decrypts transparently.rolling_key:contentisbase64(nonce || ciphertext). Client decrypts using the epoch key forepoch_id.peer_provided:contentis an opaque encrypted blob. Client decrypts using a peer-provided key.
interface MessageResponse {
id: string;
channel_id: string;
author_id: string;
author_name: string;
author_avatar: string | null;
content: string;
encryption_mode: EncryptionMode;
epoch_id: string | null; // set for rolling_key messages; null otherwise
type: number; // 0 = DEFAULT
reply_to_id: string | null;
pinned: boolean;
edited: boolean;
edited_at: string | null;
created_at: string;
attachments: Attachment[];
}
DMChannel¶
interface DMMember {
user_id: string;
username: string;
avatar: string | null;
joined_at: string;
}
interface DMChannel {
id: string;
type: "dm" | "group_dm";
last_message_id: string | null;
created_at: string;
members: DMMember[];
}
DMMessage¶
interface DeviceCiphertext {
device_id: string;
ciphertext: string; // base64
}
interface DMMessage {
id: string;
channel_id: string;
sender_id: string;
sender_device: string;
ciphertext: string; // base64 — Double Ratchet ciphertext
ratchet_header: string; // base64
x3dh_header: string | null; // base64 — only on first message in a session
device_ciphertexts: DeviceCiphertext[];
reply_to_id: string | null;
edited: boolean;
edited_at: string | null;
created_at: string;
}
Attachment¶
All files are encrypted by the client before upload. url is a path to the raw ciphertext; the client must fetch metadata from /media/attachments/{id} to determine how to decrypt.
interface Attachment {
id: string;
uploader_id: string;
filename: string;
url: string; // path to raw ciphertext — NOT directly displayable
proxy_url: string | null;
content_type: string;
size: number; // bytes (of ciphertext)
width: number | null;
height: number | null;
ephemeral: boolean;
}
AttachmentMeta¶
Returned by GET /media/attachments/{id}. Contains everything the client needs to decrypt.
type AttachmentKeyType = "public" | "dm" | "server_encrypted";
interface AttachmentMeta {
id: string;
filename: string;
content_type: string;
size: number;
key_type: AttachmentKeyType;
raw_url: string; // URL to fetch raw ciphertext bytes
// For key_type = "public":
dek_b64: string | null; // plaintext 32-byte DEK in base64
// For key_type = "dm":
encrypted_dek_b64: string | null; // sealed DEK for caller's device (base64)
// For key_type = "server_encrypted":
// raw_url points to a legacy endpoint that decrypts server-side
}
VoiceState¶
interface VoiceState {
user_id: string;
session_id: string;
self_mute: boolean;
self_deaf: boolean;
self_video: boolean;
self_stream: boolean;
server_mute: boolean;
server_deaf: boolean;
suppress: boolean;
joined_at: string;
}
Invite¶
interface Invite {
code: string;
server_id: string;
server_name: string;
server_icon: string | null;
member_count: number;
channel_id: string | null;
channel_name: string | null;
creator_id: string;
uses: number;
max_uses: number | null; // null = unlimited
expires_at: string | null;
created_at: string;
}
Ban¶
interface Ban {
server_id: string;
user_id: string;
username: string;
reason: string | null;
banned_by: string;
created_at: string;
}
Friend / FriendRequest¶
interface Friend {
user: User;
status: "accepted";
}
interface PendingFriend {
user_id: string;
username: string;
created_at: string;
}
PreKeyBundle (keyserver)¶
interface PreKeyBundle {
identity_key: string; // base64, Ed25519 public key
signed_prekey: string; // base64, X25519 public key
signed_prekey_signature: string; // base64, Ed25519 signature
signed_prekey_id: number;
one_time_prekey: {
id: number;
key: string; // base64, X25519 public key
} | null; // null if all OTPs consumed
}
Errors¶
| Status | Meaning |
|---|---|
| 400 | Bad request — missing or invalid fields |
| 401 | Unauthorized — missing/expired/invalid token |
| 403 | Forbidden — insufficient permissions |
| 404 | Not found |
| 409 | Conflict — stale epoch number or duplicate resource |
| 422 | Unprocessable — grant set does not cover all server members |
| 429 | Rate limit hit |
| 500 | Internal server error |