Files
OwnCord/SCHEMA.md
T
jevb c1c25ed26c feat: implement full client UI from mockup — 10 phases, 331 tests
Client UI:
- Design system: Colors, Typography, Controls resource dictionaries
- Message actions: reply compose bar, hover edit/delete/reply buttons
- Rich content: code blocks, attachments, system messages, content parser
- Server strip: 72px sidebar with server icons, home button, add server
- Status picker: popup for changing online/idle/dnd/invisible status
- ConnectPage: server health check dots with auto-refresh
- User popup: profile card with banner, avatar, roles, member since
- Emoji picker: 6 categories, search, grid of Unicode emojis
- Settings overlay: full-screen with sidebar navigation
- Friends/DM view: sidebar + friends list with tabs (online/all/pending)
- Toast notifications: auto-dismiss after 3s with fade animation

Models & services:
- Attachment model added to Message, ApiMessage, ChatMessagePayload
- EditMessageAsync, DeleteMessageAsync, SendStatusChangeAsync APIs
- MessageContentParser (code blocks, inline code, bold, italic)
- EmojiData, ToastService, HealthStatusToBrushConverter

Server (from prior session):
- Voice room management, SFU, speaker detection
- ACME/TLS support, config improvements
- Protocol and schema updates

Tests: 331 passing (61 converter + 24 voice service + 34 voice VM +
41 parser + 9 edit/delete + existing)
2026-03-15 11:42:25 +01:00

10 KiB

Database Schema (SQLite)

Single file: data/chatserver.db. WAL mode enabled. Migrations run automatically on startup.


Users

CREATE TABLE users (
    id          INTEGER PRIMARY KEY AUTOINCREMENT,
    username    TEXT    NOT NULL UNIQUE COLLATE NOCASE,
    password    TEXT    NOT NULL,  -- bcrypt hash
    avatar      TEXT,              -- filename in uploads/ or NULL
    role_id     INTEGER NOT NULL DEFAULT 4 REFERENCES roles(id),
    totp_secret TEXT,              -- encrypted TOTP secret or NULL if 2FA disabled
    status      TEXT    NOT NULL DEFAULT 'offline',  -- online, idle, dnd, offline
    created_at  TEXT    NOT NULL DEFAULT (datetime('now')),
    last_seen   TEXT,
    banned      INTEGER NOT NULL DEFAULT 0,
    ban_reason  TEXT,
    ban_expires TEXT     -- NULL = permanent, datetime = temp ban
);

Sessions

CREATE TABLE sessions (
    id         INTEGER PRIMARY KEY AUTOINCREMENT,
    user_id    INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
    token      TEXT    NOT NULL UNIQUE,  -- 256-bit random, hex encoded
    device     TEXT,                     -- user-agent or client identifier
    ip_address TEXT,
    created_at TEXT    NOT NULL DEFAULT (datetime('now')),
    last_used  TEXT    NOT NULL DEFAULT (datetime('now')),
    expires_at TEXT    NOT NULL           -- 30 days from creation
);

CREATE INDEX idx_sessions_token ON sessions(token);
CREATE INDEX idx_sessions_user ON sessions(user_id);

Roles

CREATE TABLE roles (
    id          INTEGER PRIMARY KEY AUTOINCREMENT,
    name        TEXT    NOT NULL UNIQUE,
    color       TEXT,              -- hex color e.g. #E74C3C, NULL for default
    permissions INTEGER NOT NULL DEFAULT 0,  -- bitfield
    position    INTEGER NOT NULL DEFAULT 0,  -- hierarchy: higher = more power
    is_default  INTEGER NOT NULL DEFAULT 0   -- 1 = assigned to new users
);

-- Default roles (inserted on first run)
-- Owner:     permissions = 0x7FFFFFFF (all bits set)
-- Admin:     permissions = 0x3FFFFFFF
-- Moderator: permissions = 0x000FFFFF
-- Member:    permissions = 0x00000663

Permission Bitfield

Bit 0:  SEND_MESSAGES        (0x1)
Bit 1:  READ_MESSAGES         (0x2)
Bit 5:  ATTACH_FILES          (0x20)
Bit 6:  ADD_REACTIONS          (0x40)
Bit 8:  USE_SOUNDBOARD         (0x100)
Bit 9:  CONNECT_VOICE          (0x200)
Bit 10: SPEAK_VOICE            (0x400)
Bit 11: USE_VIDEO              (0x800)
Bit 12: SHARE_SCREEN           (0x1000)
Bit 16: MANAGE_MESSAGES        (0x10000)   -- delete others' messages, pin
Bit 17: MANAGE_CHANNELS        (0x20000)
Bit 18: KICK_MEMBERS           (0x40000)
Bit 19: BAN_MEMBERS            (0x80000)
Bit 20: MUTE_MEMBERS           (0x100000)  -- server mute/deafen
Bit 24: MANAGE_ROLES           (0x1000000)
Bit 25: MANAGE_SERVER          (0x2000000)
Bit 26: MANAGE_INVITES         (0x4000000)
Bit 27: VIEW_AUDIT_LOG         (0x8000000)
Bit 30: ADMINISTRATOR          (0x40000000) -- bypasses all checks

Channels

CREATE TABLE channels (
    id               INTEGER PRIMARY KEY AUTOINCREMENT,
    name             TEXT    NOT NULL,
    type             TEXT    NOT NULL DEFAULT 'text',  -- text, voice, announcement
    category         TEXT,                              -- category name for grouping
    topic            TEXT,                              -- channel description
    position         INTEGER NOT NULL DEFAULT 0,
    slow_mode        INTEGER NOT NULL DEFAULT 0,  -- seconds, 0=off
    archived         INTEGER NOT NULL DEFAULT 0,
    created_at       TEXT NOT NULL DEFAULT (datetime('now')),
    voice_max_users  INTEGER NOT NULL DEFAULT 0,        -- 0 = unlimited
    voice_quality    TEXT,                    -- low|medium|high; NULL=default
    mixing_threshold INTEGER,                           -- NULL = server default
    voice_max_video  INTEGER NOT NULL DEFAULT 10  -- max video streams
);

Channel Permission Overrides

CREATE TABLE channel_overrides (
    id         INTEGER PRIMARY KEY AUTOINCREMENT,
    channel_id INTEGER NOT NULL REFERENCES channels(id) ON DELETE CASCADE,
    role_id    INTEGER NOT NULL REFERENCES roles(id) ON DELETE CASCADE,
    allow      INTEGER NOT NULL DEFAULT 0,  -- permission bits to grant
    deny       INTEGER NOT NULL DEFAULT 0,  -- permission bits to revoke
    UNIQUE(channel_id, role_id)
);

Messages

CREATE TABLE messages (
    id         INTEGER PRIMARY KEY AUTOINCREMENT,
    channel_id INTEGER NOT NULL REFERENCES channels(id) ON DELETE CASCADE,
    user_id    INTEGER NOT NULL REFERENCES users(id),
    content    TEXT    NOT NULL,
    reply_to   INTEGER REFERENCES messages(id) ON DELETE SET NULL,
    edited_at  TEXT,
    deleted    INTEGER NOT NULL DEFAULT 0,  -- 1 = soft deleted
    pinned     INTEGER NOT NULL DEFAULT 0,
    timestamp  TEXT    NOT NULL DEFAULT (datetime('now'))
);

CREATE INDEX idx_messages_channel ON messages(channel_id, id DESC);
CREATE INDEX idx_messages_user ON messages(user_id);
CREATE VIRTUAL TABLE messages_fts USING fts5(
    content,
    content='messages',
    content_rowid='id'
);

-- Triggers to keep FTS in sync
CREATE TRIGGER messages_ai AFTER INSERT ON messages BEGIN
    INSERT INTO messages_fts(rowid, content) VALUES (new.id, new.content);
END;

CREATE TRIGGER messages_ad AFTER DELETE ON messages BEGIN
    INSERT INTO messages_fts(messages_fts, rowid, content)
        VALUES('delete', old.id, old.content);
END;

CREATE TRIGGER messages_au AFTER UPDATE ON messages BEGIN
    INSERT INTO messages_fts(messages_fts, rowid, content)
        VALUES('delete', old.id, old.content);
    INSERT INTO messages_fts(rowid, content) VALUES (new.id, new.content);
END;

Attachments

CREATE TABLE attachments (
    id         TEXT    PRIMARY KEY,  -- UUID
    message_id INTEGER REFERENCES messages(id) ON DELETE CASCADE,
    filename   TEXT    NOT NULL,     -- original filename
    stored_as  TEXT    NOT NULL,     -- UUID filename on disk
    mime_type  TEXT    NOT NULL,
    size       INTEGER NOT NULL,     -- bytes
    uploaded_at TEXT   NOT NULL DEFAULT (datetime('now'))
);

Reactions

CREATE TABLE reactions (
    id         INTEGER PRIMARY KEY AUTOINCREMENT,
    message_id INTEGER NOT NULL REFERENCES messages(id) ON DELETE CASCADE,
    user_id    INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
    emoji      TEXT    NOT NULL,
    UNIQUE(message_id, user_id, emoji)
);

Invites

CREATE TABLE invites (
    id          INTEGER PRIMARY KEY AUTOINCREMENT,
    code        TEXT    NOT NULL UNIQUE,  -- random token
    created_by  INTEGER NOT NULL REFERENCES users(id),
    redeemed_by INTEGER REFERENCES users(id),
    max_uses    INTEGER,                  -- NULL = unlimited
    use_count   INTEGER NOT NULL DEFAULT 0,
    expires_at  TEXT,                      -- NULL = never
    created_at  TEXT    NOT NULL DEFAULT (datetime('now')),
    revoked     INTEGER NOT NULL DEFAULT 0
);

CREATE INDEX idx_invites_code ON invites(code);

Read State (unread tracking)

CREATE TABLE read_states (
    user_id         INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
    channel_id      INTEGER NOT NULL REFERENCES channels(id) ON DELETE CASCADE,
    last_message_id INTEGER NOT NULL DEFAULT 0,
    mention_count   INTEGER NOT NULL DEFAULT 0,
    PRIMARY KEY (user_id, channel_id)
);

Audit Log

CREATE TABLE audit_log (
    id         INTEGER PRIMARY KEY AUTOINCREMENT,
    user_id    INTEGER REFERENCES users(id),
    action     TEXT    NOT NULL,  -- e.g. user_ban, channel_create
    target_type TEXT,             -- user, channel, message, role, invite
    target_id   INTEGER,
    details    TEXT,              -- JSON with extra context
    timestamp  TEXT    NOT NULL DEFAULT (datetime('now'))
);

CREATE INDEX idx_audit_timestamp ON audit_log(timestamp DESC);

Login Attempts (rate limiting)

CREATE TABLE login_attempts (
    id         INTEGER PRIMARY KEY AUTOINCREMENT,
    ip_address TEXT    NOT NULL,
    username   TEXT,
    success    INTEGER NOT NULL DEFAULT 0,
    timestamp  TEXT    NOT NULL DEFAULT (datetime('now'))
);

CREATE INDEX idx_login_ip ON login_attempts(ip_address, timestamp);

Server Settings (key-value)

CREATE TABLE settings (
    key   TEXT PRIMARY KEY,
    value TEXT NOT NULL
);

-- Default settings inserted on first run:
-- server_name, server_icon, motd, max_upload_bytes, voice_quality,
-- require_2fa, registration_open (always 0), backup_schedule, backup_retention

Custom Emoji

CREATE TABLE emoji (
    id         INTEGER PRIMARY KEY AUTOINCREMENT,
    shortcode  TEXT    NOT NULL UNIQUE,  -- e.g. :pepe:
    filename   TEXT    NOT NULL,          -- stored in uploads/emoji/
    uploaded_by INTEGER NOT NULL REFERENCES users(id),
    created_at TEXT    NOT NULL DEFAULT (datetime('now'))
);

Soundboard

CREATE TABLE sounds (
    id          INTEGER PRIMARY KEY AUTOINCREMENT,
    name        TEXT    NOT NULL,
    filename    TEXT    NOT NULL,  -- stored in uploads/sounds/
    duration_ms INTEGER NOT NULL,
    uploaded_by INTEGER NOT NULL REFERENCES users(id),
    created_at  TEXT    NOT NULL DEFAULT (datetime('now'))
);

Voice States

CREATE TABLE voice_states (
    user_id     INTEGER PRIMARY KEY REFERENCES users(id) ON DELETE CASCADE,
    channel_id  INTEGER NOT NULL REFERENCES channels(id) ON DELETE CASCADE,
    muted       INTEGER NOT NULL DEFAULT 0,
    deafened    INTEGER NOT NULL DEFAULT 0,
    speaking    INTEGER NOT NULL DEFAULT 0,
    camera      INTEGER NOT NULL DEFAULT 0,
    screenshare INTEGER NOT NULL DEFAULT 0,
    joined_at   TEXT    NOT NULL DEFAULT (datetime('now'))
);

CREATE INDEX idx_voice_states_channel ON voice_states(channel_id);

On startup: DELETE FROM voice_states; clears stale state from previous run.


Notes

  • All datetimes stored as ISO 8601 UTC strings.
  • Enable WAL mode on connection: PRAGMA journal_mode=WAL;
  • Enable foreign keys: PRAGMA foreign_keys=ON;
  • Use modernc.org/sqlite (pure Go, no CGO needed).
  • Migrations: schema version in settings, apply incremental SQL on startup.