VNOX-Server/docs/10-database.md

20 KiB

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.

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.

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).

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.

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.

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).

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.

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.

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.

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.

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.

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).

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).

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.

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.

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.

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.

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)

-- 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

-- 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)

[database]
type = "sqlite"
path = "data/vnox.db"

PostgreSQL (Production, Distributed)

[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