# Database Schema Version: 1.0 Status: Specification and implementation guide --- ## Overview Vnox uses SQLite for local deployment and supports PostgreSQL for larger deployments. Database schema defines storage for: - Users and identity - Guilds, channels, categories - Roles and permissions - Messages and history - Direct messages - Presence and online status - Audit logs All tables use UUID for primary keys (no auto-increment) for federation compatibility. --- ## Core Tables ### users Global user identity across all Vnox instances. ```sql CREATE TABLE users ( id TEXT PRIMARY KEY, username TEXT NOT NULL, identity_key TEXT NOT NULL, created_at TIMESTAMP NOT NULL, updated_at TIMESTAMP NOT NULL ); CREATE INDEX idx_users_username ON users(username); ``` | Column | Type | Constraints | Description | | ------------ | --------- | -------------- | -------------------- | | id | TEXT | PRIMARY KEY | UUID | | username | TEXT | NOT NULL | Display name | | identity_key | TEXT | NOT NULL | Ed25519 public key | | created_at | TIMESTAMP | NOT NULL | Creation time | | updated_at | TIMESTAMP | NOT NULL | Last modification | --- ### sessions Active user sessions on a gateway instance. ```sql CREATE TABLE sessions ( id TEXT PRIMARY KEY, user_id TEXT NOT NULL, session_token TEXT NOT NULL UNIQUE, expires_at TIMESTAMP NOT NULL, created_at TIMESTAMP NOT NULL, FOREIGN KEY(user_id) REFERENCES users(id) ); CREATE INDEX idx_sessions_user ON sessions(user_id); CREATE INDEX idx_sessions_expires ON sessions(expires_at); ``` | Column | Type | Constraints | Description | | ------------- | --------- | -------------- | -------------------- | | id | TEXT | PRIMARY KEY | Session UUID | | user_id | TEXT | FK users | User owning session | | session_token | TEXT | UNIQUE | Auth token | | expires_at | TIMESTAMP | NOT NULL | Expiration time | | created_at | TIMESTAMP | NOT NULL | Creation time | --- ## Guild Tables ### guilds Guild entities (communities/servers). ```sql CREATE TABLE guilds ( id TEXT PRIMARY KEY, owner_id TEXT NOT NULL, name TEXT NOT NULL, description TEXT, icon TEXT, banner TEXT, created_at TIMESTAMP NOT NULL, updated_at TIMESTAMP NOT NULL, FOREIGN KEY(owner_id) REFERENCES users(id) ); CREATE INDEX idx_guilds_owner ON guilds(owner_id); ``` | Column | Type | Constraints | Description | | ----------- | --------- | -------------- | -------------------- | | id | TEXT | PRIMARY KEY | Guild UUID | | owner_id | TEXT | FK users | Guild owner | | name | TEXT | NOT NULL | Guild name | | description | TEXT | | Guild description | | icon | TEXT | | Asset ID | | banner | TEXT | | Asset ID | | created_at | TIMESTAMP | NOT NULL | Creation time | | updated_at | TIMESTAMP | NOT NULL | Last modification | --- ### guild_members Guild membership tracking. ```sql CREATE TABLE guild_members ( guild_id TEXT NOT NULL, user_id TEXT NOT NULL, nickname TEXT, joined_at TIMESTAMP NOT NULL, timeout_until TIMESTAMP, PRIMARY KEY (guild_id, user_id), FOREIGN KEY(guild_id) REFERENCES guilds(id), FOREIGN KEY(user_id) REFERENCES users(id) ); CREATE INDEX idx_guild_members_user ON guild_members(user_id); CREATE INDEX idx_guild_members_joined ON guild_members(joined_at); ``` | Column | Type | Constraints | Description | | ------------- | --------- | -------------- | -------------------- | | guild_id | TEXT | PRIMARY KEY | Guild UUID | | user_id | TEXT | PRIMARY KEY | User UUID | | nickname | TEXT | | Guild-specific nick | | joined_at | TIMESTAMP | NOT NULL | Join time | | timeout_until | TIMESTAMP | | Moderation timeout | --- ### categories Channel categories for organization. ```sql CREATE TABLE categories ( id TEXT PRIMARY KEY, guild_id TEXT NOT NULL, name TEXT NOT NULL, position INTEGER NOT NULL DEFAULT 0, created_at TIMESTAMP NOT NULL, updated_at TIMESTAMP NOT NULL, FOREIGN KEY(guild_id) REFERENCES guilds(id) ); CREATE INDEX idx_categories_guild ON categories(guild_id); CREATE INDEX idx_categories_position ON categories(guild_id, position); ``` | Column | Type | Constraints | Description | | ---------- | --------- | -------------- | -------------------- | | id | TEXT | PRIMARY KEY | Category UUID | | guild_id | TEXT | FK guilds | Parent guild | | name | TEXT | NOT NULL | Display name | | position | INTEGER | NOT NULL | Sort order | | created_at | TIMESTAMP | NOT NULL | Creation time | | updated_at | TIMESTAMP | NOT NULL | Last modification | --- ### channels Communication channels (text, voice, announcements). ```sql CREATE TABLE channels ( id TEXT PRIMARY KEY, guild_id TEXT NOT NULL, category_id TEXT, type TEXT NOT NULL, name TEXT NOT NULL, topic TEXT, position INTEGER NOT NULL DEFAULT 0, created_at TIMESTAMP NOT NULL, updated_at TIMESTAMP NOT NULL, FOREIGN KEY(guild_id) REFERENCES guilds(id), FOREIGN KEY(category_id) REFERENCES categories(id) ); CREATE INDEX idx_channels_guild ON channels(guild_id); CREATE INDEX idx_channels_category ON channels(category_id); CREATE INDEX idx_channels_position ON channels(guild_id, position); ``` | Column | Type | Constraints | Description | | ----------- | --------- | -------------- | -------------------- | | id | TEXT | PRIMARY KEY | Channel UUID | | guild_id | TEXT | FK guilds | Parent guild | | category_id | TEXT | FK categories | Parent category | | type | TEXT | NOT NULL | TEXT/VOICE/etc | | name | TEXT | NOT NULL | Display name | | topic | TEXT | | Channel description | | position | INTEGER | NOT NULL | Sort order | | created_at | TIMESTAMP | NOT NULL | Creation time | | updated_at | TIMESTAMP | NOT NULL | Last modification | --- ## Role & Permission Tables ### roles Guild roles with permission bitmasks. ```sql CREATE TABLE roles ( id TEXT PRIMARY KEY, guild_id TEXT NOT NULL, name TEXT NOT NULL, color INTEGER NOT NULL DEFAULT 0, permissions INTEGER NOT NULL DEFAULT 0, position INTEGER NOT NULL DEFAULT 0, hoist INTEGER NOT NULL DEFAULT 0, mentionable INTEGER NOT NULL DEFAULT 1, created_at TIMESTAMP NOT NULL, updated_at TIMESTAMP NOT NULL, FOREIGN KEY(guild_id) REFERENCES guilds(id) ); CREATE INDEX idx_roles_guild ON roles(guild_id); CREATE INDEX idx_roles_position ON roles(guild_id, position); ``` | Column | Type | Constraints | Description | | ------------ | --------- | -------------- | -------------------- | | id | TEXT | PRIMARY KEY | Role UUID | | guild_id | TEXT | FK guilds | Parent guild | | name | TEXT | NOT NULL | Display name | | color | INTEGER | NOT NULL | RGB color value | | permissions | INTEGER | NOT NULL | Bitmask (u128) | | position | INTEGER | NOT NULL | Hierarchy position | | hoist | INTEGER | NOT NULL | Display separately | | mentionable | INTEGER | NOT NULL | Can be @mentioned | | created_at | TIMESTAMP | NOT NULL | Creation time | | updated_at | TIMESTAMP | NOT NULL | Last modification | --- ### member_roles Many-to-many mapping of members to roles. ```sql CREATE TABLE member_roles ( member_guild_id TEXT NOT NULL, member_user_id TEXT NOT NULL, role_id TEXT NOT NULL, PRIMARY KEY (member_guild_id, member_user_id, role_id), FOREIGN KEY(member_guild_id, member_user_id) REFERENCES guild_members(guild_id, user_id), FOREIGN KEY(role_id) REFERENCES roles(id) ); CREATE INDEX idx_member_roles_role ON member_roles(role_id); ``` | Column | Type | Constraints | Description | | ---------------- | ---- | -------------- | -------------------- | | member_guild_id | TEXT | PRIMARY KEY | Guild UUID | | member_user_id | TEXT | PRIMARY KEY | User UUID | | role_id | TEXT | PRIMARY KEY | Role UUID | --- ### channel_overrides Role and user permission overrides per channel. ```sql CREATE TABLE channel_overrides ( id TEXT PRIMARY KEY, channel_id TEXT NOT NULL, target_id TEXT NOT NULL, target_type TEXT NOT NULL, allow_mask INTEGER NOT NULL DEFAULT 0, deny_mask INTEGER NOT NULL DEFAULT 0, created_at TIMESTAMP NOT NULL, updated_at TIMESTAMP NOT NULL, FOREIGN KEY(channel_id) REFERENCES channels(id) ); CREATE INDEX idx_channel_overrides_channel ON channel_overrides(channel_id); CREATE INDEX idx_channel_overrides_target ON channel_overrides(target_id, target_type); ``` | Column | Type | Constraints | Description | | ----------- | --------- | -------------- | ------------------------ | | id | TEXT | PRIMARY KEY | Override UUID | | channel_id | TEXT | FK channels | Channel | | target_id | TEXT | NOT NULL | Role or User UUID | | target_type | TEXT | NOT NULL | "ROLE" or "USER" | | allow_mask | INTEGER | NOT NULL | Allowed permissions | | deny_mask | INTEGER | NOT NULL | Denied permissions | | created_at | TIMESTAMP | NOT NULL | Creation time | | updated_at | TIMESTAMP | NOT NULL | Last modification | --- ## Message Tables ### messages Text messages in channels. ```sql CREATE TABLE messages ( id TEXT PRIMARY KEY, channel_id TEXT NOT NULL, sender_id TEXT NOT NULL, content TEXT NOT NULL, created_at TIMESTAMP NOT NULL, updated_at TIMESTAMP, deleted_at TIMESTAMP, FOREIGN KEY(channel_id) REFERENCES channels(id), FOREIGN KEY(sender_id) REFERENCES users(id) ); CREATE INDEX idx_messages_channel ON messages(channel_id, created_at DESC); CREATE INDEX idx_messages_sender ON messages(sender_id); ``` | Column | Type | Constraints | Description | | ---------- | --------- | -------------- | -------------------- | | id | TEXT | PRIMARY KEY | Message UUID | | channel_id | TEXT | FK channels | Parent channel | | sender_id | TEXT | FK users | Message author | | content | TEXT | NOT NULL | Encrypted content | | created_at | TIMESTAMP | NOT NULL | Creation time | | updated_at | TIMESTAMP | | Edit time | | deleted_at | TIMESTAMP | | Deletion time (soft) | --- ### message_reactions Emoji reactions on messages. ```sql CREATE TABLE message_reactions ( message_id TEXT NOT NULL, user_id TEXT NOT NULL, emoji TEXT NOT NULL, created_at TIMESTAMP NOT NULL, PRIMARY KEY (message_id, user_id, emoji), FOREIGN KEY(message_id) REFERENCES messages(id), FOREIGN KEY(user_id) REFERENCES users(id) ); CREATE INDEX idx_reactions_message ON message_reactions(message_id); ``` | Column | Type | Constraints | Description | | ---------- | --------- | -------------- | -------------------- | | message_id | TEXT | PRIMARY KEY | Message UUID | | user_id | TEXT | PRIMARY KEY | User UUID | | emoji | TEXT | PRIMARY KEY | Emoji character | | created_at | TIMESTAMP | NOT NULL | Reaction time | --- ## Direct Message Tables ### direct_messages DM channel tracking (not per-guild). ```sql CREATE TABLE direct_messages ( dm_id TEXT PRIMARY KEY, user_id_1 TEXT NOT NULL, user_id_2 TEXT NOT NULL, created_at TIMESTAMP NOT NULL, updated_at TIMESTAMP NOT NULL, FOREIGN KEY(user_id_1) REFERENCES users(id), FOREIGN KEY(user_id_2) REFERENCES users(id) ); CREATE UNIQUE INDEX idx_dm_users ON direct_messages( MIN(user_id_1, user_id_2), MAX(user_id_1, user_id_2) ); ``` | Column | Type | Constraints | Description | | ---------- | --------- | -------------- | -------------------- | | dm_id | TEXT | PRIMARY KEY | DM UUID | | user_id_1 | TEXT | FK users | First user (lexicog) | | user_id_2 | TEXT | FK users | Second user | | created_at | TIMESTAMP | NOT NULL | Creation time | | updated_at | TIMESTAMP | NOT NULL | Last message time | --- ### dm_messages Direct messages (archived). ```sql CREATE TABLE dm_messages ( id TEXT PRIMARY KEY, dm_id TEXT NOT NULL, sender_id TEXT NOT NULL, content TEXT NOT NULL, created_at TIMESTAMP NOT NULL, FOREIGN KEY(dm_id) REFERENCES direct_messages(dm_id), FOREIGN KEY(sender_id) REFERENCES users(id) ); CREATE INDEX idx_dm_messages_dm ON dm_messages(dm_id, created_at DESC); CREATE INDEX idx_dm_messages_sender ON dm_messages(sender_id); ``` | Column | Type | Constraints | Description | | ---------- | --------- | -------------- | -------------------- | | id | TEXT | PRIMARY KEY | Message UUID | | dm_id | TEXT | FK dm tables | Parent DM | | sender_id | TEXT | FK users | Message author | | content | TEXT | NOT NULL | Encrypted content | | created_at | TIMESTAMP | NOT NULL | Creation time | --- ## Invitation Tables ### invites Guild invitations. ```sql CREATE TABLE invites ( id TEXT PRIMARY KEY, guild_id TEXT NOT NULL, creator_id TEXT NOT NULL, code TEXT NOT NULL UNIQUE, expires_at TIMESTAMP, max_uses INTEGER, uses INTEGER NOT NULL DEFAULT 0, created_at TIMESTAMP NOT NULL, FOREIGN KEY(guild_id) REFERENCES guilds(id), FOREIGN KEY(creator_id) REFERENCES users(id) ); CREATE INDEX idx_invites_guild ON invites(guild_id); CREATE INDEX idx_invites_code ON invites(code); CREATE INDEX idx_invites_expires ON invites(expires_at); ``` | Column | Type | Constraints | Description | | ---------- | --------- | -------------- | -------------------- | | id | TEXT | PRIMARY KEY | Invite UUID | | guild_id | TEXT | FK guilds | Target guild | | creator_id | TEXT | FK users | Creator | | code | TEXT | UNIQUE | Invite slug | | expires_at | TIMESTAMP | | Expiration time | | max_uses | INTEGER | | Use limit | | uses | INTEGER | NOT NULL | Current uses | | created_at | TIMESTAMP | NOT NULL | Creation time | --- ## Presence Tables ### presence User online status and activity. ```sql CREATE TABLE presence ( user_id TEXT PRIMARY KEY, status TEXT NOT NULL, activity_type TEXT, activity_text TEXT, last_seen TIMESTAMP NOT NULL, FOREIGN KEY(user_id) REFERENCES users(id) ); CREATE INDEX idx_presence_status ON presence(status); CREATE INDEX idx_presence_last_seen ON presence(last_seen); ``` | Column | Type | Constraints | Description | | ------------- | --------- | -------------- | -------------------- | | user_id | TEXT | PRIMARY KEY | User UUID | | status | TEXT | NOT NULL | ONLINE/IDLE/etc | | activity_type | TEXT | | PLAYING/LISTENING | | activity_text | TEXT | | Activity description | | last_seen | TIMESTAMP | NOT NULL | Last activity time | --- ## Friendship Tables ### friends User friendship relationships. ```sql CREATE TABLE friends ( requester_id TEXT NOT NULL, target_id TEXT NOT NULL, status TEXT NOT NULL, created_at TIMESTAMP NOT NULL, PRIMARY KEY (requester_id, target_id), FOREIGN KEY(requester_id) REFERENCES users(id), FOREIGN KEY(target_id) REFERENCES users(id) ); CREATE INDEX idx_friends_target ON friends(target_id, status); ``` | Column | Type | Constraints | Description | | ------------ | --------- | -------------- | ------------------------ | | requester_id | TEXT | PRIMARY KEY | Request originator | | target_id | TEXT | PRIMARY KEY | Request recipient | | status | TEXT | NOT NULL | pending/accepted/blocked | | created_at | TIMESTAMP | NOT NULL | Request time | --- ## Audit Log Table ### audit_logs Moderation and administrative actions. ```sql CREATE TABLE audit_logs ( id TEXT PRIMARY KEY, guild_id TEXT NOT NULL, actor_id TEXT NOT NULL, action TEXT NOT NULL, target_id TEXT, target_type TEXT, reason TEXT, changes TEXT, created_at TIMESTAMP NOT NULL, FOREIGN KEY(guild_id) REFERENCES guilds(id), FOREIGN KEY(actor_id) REFERENCES users(id) ); CREATE INDEX idx_audit_logs_guild ON audit_logs(guild_id, created_at DESC); CREATE INDEX idx_audit_logs_actor ON audit_logs(actor_id); CREATE INDEX idx_audit_logs_target ON audit_logs(target_id, target_type); ``` | Column | Type | Constraints | Description | | ----------- | --------- | -------------- | -------------------- | | id | TEXT | PRIMARY KEY | Log UUID | | guild_id | TEXT | FK guilds | Guild context | | actor_id | TEXT | FK users | Admin/moderator | | action | TEXT | NOT NULL | ACTION_TYPE | | target_id | TEXT | | Affected entity | | target_type | TEXT | | Entity type | | reason | TEXT | | Reason for action | | changes | TEXT | | JSON of old/new | | created_at | TIMESTAMP | NOT NULL | Action time | --- ## Migration Strategy ### Phase 1.1 (Current) ```sql -- Already exists CREATE TABLE users CREATE TABLE sessions CREATE TABLE channels CREATE TABLE messages CREATE TABLE direct_messages CREATE TABLE dm_messages ``` ### Phase 1.2 ```sql -- Add guild system CREATE TABLE guilds CREATE TABLE guild_members CREATE TABLE categories CREATE TABLE roles CREATE TABLE member_roles CREATE TABLE channel_overrides CREATE TABLE invites CREATE TABLE presence CREATE TABLE friends CREATE TABLE audit_logs ``` ### Backward Compatibility During transition to guilds: - Create a default guild per deployment - Migrate existing channels to default guild - Add all existing users as guild members --- ## Performance Considerations ### Indexes Key composite indexes: - `messages(channel_id, created_at DESC)` — range queries - `dm_messages(dm_id, created_at DESC)` — pagination - `presence(status, last_seen)` — online user queries - `guild_members(guild_id, joined_at)` — membership lists ### Query Patterns Optimize for: - Message history fetch (paginated, DESC) - Channel member list - Permission resolution (cached) - Online presence broadcast - Recent DM threads ### Caching Recommended client-side caches: - Guild data (invalidated on GUILD_UPDATE) - Channel list (invalidated on CHANNEL_CREATE/DELETE) - User presence (invalidated on PRESENCE_UPDATE) - Role permissions (cached by guild) --- ## PostgreSQL vs SQLite ### SQLite (Development, Single-Server) ```toml [database] type = "sqlite" path = "data/vnox.db" ``` ### PostgreSQL (Production, Distributed) ```toml [database] type = "postgres" url = "postgresql://user:pass@localhost/vnox" pool_size = 32 ``` Both use identical schema; driver handles translation of: - `INTEGER` ↔ `BIGINT` - `TEXT` ↔ `VARCHAR` - UUID handling