Database Schema
Status: Complete
Chatalot uses PostgreSQL as its primary data store. The schema is managed through 39 sequential SQL migration files in the migrations/ directory. Migrations run automatically on server startup.
Note: Chatalot uses sqlx with runtime-checked queries (not compile-time macros). Set
SQLX_OFFLINE=truefor builds without a live database.
Entity Relationship Overview
Instance
├── users (authentication, profiles)
│ ├── identity_keys (E2E encryption)
│ ├── signed_prekeys / one_time_prekeys
│ ├── refresh_tokens (sessions)
│ ├── user_preferences (JSONB settings)
│ └── user_blocks
├── communities
│ ├── community_members (with roles)
│ ├── community_invites
│ ├── community_bans
│ ├── custom_emojis
│ └── groups
│ ├── group_members (with roles)
│ └── channels
│ ├── channel_members (with roles)
│ ├── channel_bans
│ ├── messages
│ │ ├── message_edits (edit history)
│ │ ├── reactions
│ │ ├── pinned_messages
│ │ └── files (attachments)
│ ├── webhooks
│ ├── polls → poll_votes
│ ├── voice_sessions → voice_session_participants
│ ├── read_cursors
│ ├── sender_key_distributions (E2E)
│ ├── scheduled_messages
│ ├── user_timeouts
│ └── warnings
├── dm_pairs (DM channel mapping)
├── reports (content/user reports)
├── audit_log (admin activity)
├── announcements → announcement_dismissals
├── bookmarks (personal saved messages)
└── blocked_hashes (file security)
Core Tables
users
The primary user table. Stores authentication credentials, profile information, and admin flags.
| Column | Type | Notes |
|---|---|---|
id |
UUID PK |
|
username |
VARCHAR(32) UNIQUE |
3-32 chars, letters/numbers/underscores/hyphens/dots |
display_name |
VARCHAR(64) |
|
email |
VARCHAR(255) UNIQUE |
|
password_hash |
TEXT |
Argon2id |
avatar_url |
TEXT |
|
banner_url |
TEXT |
|
status |
VARCHAR(16) |
online/idle/dnd/invisible/offline |
custom_status |
VARCHAR(128) |
|
bio |
TEXT |
|
pronouns |
VARCHAR(50) |
|
totp_secret |
BYTEA |
Encrypted if TOTP_ENCRYPTION_KEY set |
totp_enabled |
BOOLEAN |
|
totp_backup_codes |
TEXT[] |
SHA-256 hashed |
is_admin |
BOOLEAN |
Instance admin |
is_owner |
BOOLEAN |
Instance owner (god role) |
suspended_at |
TIMESTAMPTZ |
|
suspended_reason |
TEXT |
|
recovery_code_hash |
TEXT |
|
upload_bytes_used |
BIGINT |
File quota tracking |
created_at |
TIMESTAMPTZ |
|
updated_at |
TIMESTAMPTZ |
identity_keys
Ed25519 identity keys for E2E encryption. One per user.
| Column | Type | Notes |
|---|---|---|
user_id |
UUID PK FK→users |
CASCADE delete |
identity_key |
BYTEA |
32 bytes Ed25519 public key |
fingerprint |
VARCHAR(64) |
SHA-256 hex |
created_at |
TIMESTAMPTZ |
|
rotated_at |
TIMESTAMPTZ |
signed_prekeys
X3DH signed prekeys (X25519 public key + Ed25519 signature).
| Column | Type | Notes |
|---|---|---|
id |
UUID PK |
|
user_id |
UUID FK→users |
|
key_id |
INTEGER |
UNIQUE with user_id |
public_key |
BYTEA |
32 bytes X25519 |
signature |
BYTEA |
64 bytes Ed25519 |
created_at |
TIMESTAMPTZ |
one_time_prekeys
X3DH one-time prekeys (consumed on use).
| Column | Type | Notes |
|---|---|---|
id |
UUID PK |
|
user_id |
UUID FK→users |
|
key_id |
INTEGER |
UNIQUE with user_id |
public_key |
BYTEA |
32 bytes X25519 |
used |
BOOLEAN |
DEFAULT FALSE |
created_at |
TIMESTAMPTZ |
Index: (user_id, used) WHERE NOT used
refresh_tokens
JWT refresh tokens. SHA-256 hashed for storage, rotated on use.
| Column | Type | Notes |
|---|---|---|
id |
UUID PK |
|
user_id |
UUID FK→users |
|
token_hash |
BYTEA UNIQUE |
SHA-256 of token |
device_name |
VARCHAR(128) |
Parsed from User-Agent |
ip_address |
TEXT |
|
expires_at |
TIMESTAMPTZ |
30 days |
created_at |
TIMESTAMPTZ |
|
revoked_at |
TIMESTAMPTZ |
Soft revoke for rotation |
Community Tables
communities
Top-level organizational containers.
| Column | Type | Notes |
|---|---|---|
id |
UUID PK |
|
name |
VARCHAR(64) |
|
description |
TEXT |
|
icon_url |
TEXT |
|
banner_url |
TEXT |
|
accent_color |
VARCHAR(7) |
Hex color |
owner_id |
UUID FK→users |
|
discoverable |
BOOLEAN |
|
community_theme |
JSONB |
|
welcome_message |
TEXT |
|
who_can_create_groups |
VARCHAR(16) |
DEFAULT 'admin' |
who_can_create_invites |
VARCHAR(16) |
DEFAULT 'admin' |
warn_escalation |
JSONB |
|
created_at |
TIMESTAMPTZ |
|
updated_at |
TIMESTAMPTZ |
community_members
Community membership with roles.
| Column | Type | Notes |
|---|---|---|
community_id |
UUID FK→communities |
Composite PK |
user_id |
UUID FK→users |
Composite PK |
role |
VARCHAR(16) |
DEFAULT 'member' |
nickname |
VARCHAR(64) |
Community-specific nickname |
joined_at |
TIMESTAMPTZ |
community_invites
Invite codes for joining a community.
| Column | Type | Notes |
|---|---|---|
id |
UUID PK |
|
community_id |
UUID FK→communities |
|
code |
VARCHAR(16) UNIQUE |
|
created_by |
UUID FK→users |
|
max_uses |
INTEGER |
NULL = unlimited |
used_count |
INTEGER |
|
expires_at |
TIMESTAMPTZ |
|
created_at |
TIMESTAMPTZ |
community_bans
Banned users per community.
| Column | Type | Notes |
|---|---|---|
community_id |
UUID FK→communities |
Composite PK |
user_id |
UUID FK→users |
Composite PK |
banned_by |
UUID FK→users |
|
reason |
TEXT |
|
created_at |
TIMESTAMPTZ |
Group Tables
groups
Groups within a community, containing channels.
| Column | Type | Notes |
|---|---|---|
id |
UUID PK |
|
name |
VARCHAR(64) |
|
description |
TEXT |
|
community_id |
UUID FK→communities |
NOT NULL |
owner_id |
UUID FK→users |
|
created_at |
TIMESTAMPTZ |
|
updated_at |
TIMESTAMPTZ |
group_members
Group membership with roles.
| Column | Type | Notes |
|---|---|---|
group_id |
UUID FK→groups |
Composite PK |
user_id |
UUID FK→users |
Composite PK |
role |
VARCHAR(16) |
DEFAULT 'member' |
joined_at |
TIMESTAMPTZ |
Channel Tables
channels
Text, voice, and DM channels.
| Column | Type | Notes |
|---|---|---|
id |
UUID PK |
|
name |
VARCHAR(64) |
|
channel_type |
ENUM | text, voice, dm |
group_id |
UUID FK→groups |
Nullable |
topic |
TEXT |
|
read_only |
BOOLEAN |
|
slow_mode_seconds |
INTEGER |
0 = off |
archived |
BOOLEAN |
|
message_ttl_seconds |
INTEGER |
Auto-delete after N seconds |
voice_background |
TEXT |
Voice channel background URL |
created_by |
UUID FK→users |
|
created_at |
TIMESTAMPTZ |
|
updated_at |
TIMESTAMPTZ |
channel_members
Channel membership with roles.
| Column | Type | Notes |
|---|---|---|
channel_id |
UUID FK→channels |
Composite PK |
user_id |
UUID FK→users |
Composite PK |
role |
VARCHAR(16) |
DEFAULT 'member' |
joined_at |
TIMESTAMPTZ |
channel_bans
Banned users per channel.
| Column | Type | Notes |
|---|---|---|
channel_id |
UUID FK→channels |
Composite PK |
user_id |
UUID FK→users |
Composite PK |
banned_by |
UUID FK→users |
|
reason |
TEXT |
|
banned_at |
TIMESTAMPTZ |
channel_slowmode_tracker
Tracks when users last sent a message in slow-mode channels.
| Column | Type | Notes |
|---|---|---|
channel_id |
UUID FK→channels |
Composite PK |
user_id |
UUID FK→users |
Composite PK |
last_sent |
TIMESTAMPTZ |
Messaging Tables
messages
Encrypted messages. Supports text, file, system, and webhook message types.
| Column | Type | Notes |
|---|---|---|
id |
UUID PK |
|
channel_id |
UUID FK→channels |
|
sender_id |
UUID FK→users |
|
ciphertext |
BYTEA |
64 KiB max |
nonce |
BYTEA |
|
message_type |
VARCHAR(16) |
text/file/system/webhook |
sender_key_id |
UUID FK→sender_key_distributions |
Nullable |
reply_to_id |
UUID FK→messages |
Nullable |
thread_id |
UUID FK→messages |
Nullable |
plaintext |
TEXT |
|
expires_at |
TIMESTAMPTZ |
Auto-delete (message TTL) |
edited_at |
TIMESTAMPTZ |
|
deleted_at |
TIMESTAMPTZ |
Soft delete |
quarantined_at |
TIMESTAMPTZ |
|
quarantined_by |
UUID FK→users |
|
created_at |
TIMESTAMPTZ |
Indexes: (channel_id, created_at DESC), (sender_id, created_at DESC)
message_edits
Tracks message edit history. Stores the previous ciphertext before each edit.
| Column | Type | Notes |
|---|---|---|
id |
UUID PK |
|
message_id |
UUID FK→messages |
|
old_ciphertext |
BYTEA |
|
old_nonce |
BYTEA |
|
edited_at |
TIMESTAMPTZ |
reactions
Emoji reactions on messages.
| Column | Type | Notes |
|---|---|---|
id |
UUID PK |
|
message_id |
UUID FK→messages |
|
user_id |
UUID FK→users |
|
emoji |
VARCHAR(32) |
|
created_at |
TIMESTAMPTZ |
UNIQUE: (message_id, user_id, emoji)
pinned_messages
Pinned messages within a channel.
| Column | Type | Notes |
|---|---|---|
message_id |
UUID PK FK→messages |
|
channel_id |
UUID FK→channels |
|
pinned_by |
UUID FK→users |
|
pinned_at |
TIMESTAMPTZ |
Index: (channel_id, pinned_at DESC)
read_cursors
Per-user unread message tracking.
| Column | Type | Notes |
|---|---|---|
user_id |
UUID FK→users |
Composite PK |
channel_id |
UUID FK→channels |
Composite PK |
last_read_message_id |
UUID FK→messages |
|
last_read_at |
TIMESTAMPTZ |
File Tables
files
Uploaded file records.
| Column | Type | Notes |
|---|---|---|
id |
UUID PK |
|
uploader_id |
UUID FK→users |
|
encrypted_name |
VARCHAR(512) |
|
size_bytes |
BIGINT |
|
content_type |
VARCHAR(128) |
|
storage_path |
TEXT |
|
checksum |
VARCHAR(128) |
|
channel_id |
UUID FK→channels |
Nullable |
quarantined_at |
TIMESTAMPTZ |
|
quarantined_by |
UUID FK→users |
|
created_at |
TIMESTAMPTZ |
blocked_hashes
SHA-256 hashes of blocked file content. Prevents re-upload of banned files.
| Column | Type | Notes |
|---|---|---|
id |
UUID PK |
|
hash |
VARCHAR(128) UNIQUE |
|
reason |
TEXT |
|
blocked_by |
UUID FK→users |
|
created_at |
TIMESTAMPTZ |
DM Tables
dm_pairs
Maps direct message conversations to their channel. Pair is stored with user_a < user_b to ensure uniqueness.
| Column | Type | Notes |
|---|---|---|
user_a |
UUID FK→users |
Composite PK (lower UUID) |
user_b |
UUID FK→users |
Composite PK (higher UUID) |
channel_id |
UUID FK→channels |
UNIQUE |
created_at |
TIMESTAMPTZ |
Voice Tables
voice_sessions
Tracks voice/video sessions.
| Column | Type | Notes |
|---|---|---|
id |
UUID PK |
|
channel_id |
UUID FK→channels |
|
started_by |
UUID FK→users |
|
started_at |
TIMESTAMPTZ |
|
ended_at |
TIMESTAMPTZ |
NULL while active |
voice_session_participants
Tracks participants within a voice session.
| Column | Type | Notes |
|---|---|---|
session_id |
UUID FK→voice_sessions |
Composite PK |
user_id |
UUID FK→users |
Composite PK |
joined_at |
TIMESTAMPTZ |
|
left_at |
TIMESTAMPTZ |
NULL while connected |
Encryption Tables
sender_key_distributions
Sender keys for group E2E encryption (Signal Sender Keys protocol).
| Column | Type | Notes |
|---|---|---|
id |
UUID PK |
|
channel_id |
UUID FK→channels |
|
user_id |
UUID FK→users |
|
chain_id |
INTEGER |
|
distribution |
JSONB |
Serialized SenderKeyDistribution |
created_at |
TIMESTAMPTZ |
UNIQUE: (channel_id, user_id)
Webhook Tables
webhooks
Channel webhooks for external integrations.
| Column | Type | Notes |
|---|---|---|
id |
UUID PK |
|
channel_id |
UUID FK→channels |
|
name |
VARCHAR(64) |
|
token |
VARCHAR(128) UNIQUE |
|
created_by |
UUID FK→users |
|
avatar_url |
TEXT |
|
active |
BOOLEAN |
|
created_at |
TIMESTAMPTZ |
Moderation Tables
user_timeouts
Temporary mutes within a channel.
| Column | Type | Notes |
|---|---|---|
id |
UUID PK |
|
user_id |
UUID FK→users |
|
channel_id |
UUID FK→channels |
|
issued_by |
UUID FK→users |
|
reason |
TEXT |
|
expires_at |
TIMESTAMPTZ |
|
created_at |
TIMESTAMPTZ |
warnings
Formal warnings issued to users.
| Column | Type | Notes |
|---|---|---|
id |
UUID PK |
|
user_id |
UUID FK→users |
|
channel_id |
UUID FK→channels |
|
issued_by |
UUID FK→users |
|
reason |
TEXT |
|
created_at |
TIMESTAMPTZ |
reports
Content and user reports.
| Column | Type | Notes |
|---|---|---|
id |
UUID PK |
|
reporter_id |
UUID FK→users |
|
report_type |
VARCHAR(32) |
|
target_id |
UUID |
Generic target reference |
reason |
TEXT |
|
status |
VARCHAR(32) |
pending/reviewed/resolved/dismissed |
reviewed_by |
UUID FK→users |
Nullable |
reviewed_at |
TIMESTAMPTZ |
|
admin_notes |
TEXT |
|
created_at |
TIMESTAMPTZ |
user_blocks
Per-user block list.
| Column | Type | Notes |
|---|---|---|
blocker_id |
UUID FK→users |
Composite PK |
blocked_id |
UUID FK→users |
Composite PK |
created_at |
TIMESTAMPTZ |
Poll Tables
polls
In-channel polls.
| Column | Type | Notes |
|---|---|---|
id |
UUID PK |
|
channel_id |
UUID FK→channels |
|
created_by |
UUID FK→users |
|
question |
TEXT |
|
options |
JSONB |
|
multi_select |
BOOLEAN |
|
anonymous |
BOOLEAN |
|
closed |
BOOLEAN |
|
expires_at |
TIMESTAMPTZ |
|
created_at |
TIMESTAMPTZ |
poll_votes
Individual votes on a poll.
| Column | Type | Notes |
|---|---|---|
id |
UUID PK |
|
poll_id |
UUID FK→polls |
|
user_id |
UUID FK→users |
|
option_index |
INTEGER |
|
created_at |
TIMESTAMPTZ |
UNIQUE: (poll_id, user_id, option_index)
Scheduled Messages
scheduled_messages
Messages scheduled for future delivery.
| Column | Type | Notes |
|---|---|---|
id |
UUID PK |
|
channel_id |
UUID FK→channels |
|
user_id |
UUID FK→users |
|
ciphertext |
TEXT |
|
nonce |
TEXT |
|
scheduled_for |
TIMESTAMPTZ |
|
created_at |
TIMESTAMPTZ |
Bookmark Tables
bookmarks
Per-user saved messages.
| Column | Type | Notes |
|---|---|---|
id |
UUID PK |
|
user_id |
UUID FK→users |
|
message_id |
UUID FK→messages |
|
note |
TEXT |
|
created_at |
TIMESTAMPTZ |
UNIQUE: (user_id, message_id)
Custom Emoji Tables
custom_emojis
Community custom emojis.
| Column | Type | Notes |
|---|---|---|
id |
UUID PK |
|
community_id |
UUID FK→communities |
|
uploaded_by |
UUID FK→users |
|
shortcode |
VARCHAR(32) |
|
file_path |
TEXT |
|
content_type |
VARCHAR(64) |
|
created_at |
TIMESTAMPTZ |
UNIQUE: (community_id, shortcode)
Announcement Tables
announcements
Instance-wide announcements from admins.
| Column | Type | Notes |
|---|---|---|
id |
UUID PK |
|
title |
VARCHAR(200) |
|
body |
TEXT |
|
created_by |
UUID FK→users |
|
created_at |
TIMESTAMPTZ |
announcement_dismissals
Tracks which users have dismissed an announcement.
| Column | Type | Notes |
|---|---|---|
user_id |
UUID FK→users |
Composite PK |
announcement_id |
UUID FK→announcements |
Composite PK |
dismissed_at |
TIMESTAMPTZ |
Preference Tables
user_preferences
Per-user preference storage.
| Column | Type | Notes |
|---|---|---|
user_id |
UUID PK FK→users |
|
preferences |
JSONB |
DEFAULT '{}' |
updated_at |
TIMESTAMPTZ |
Administrative Tables
audit_log
Records administrative and security-relevant events.
| Column | Type | Notes |
|---|---|---|
id |
UUID PK |
|
user_id |
UUID FK→users |
Nullable |
action |
VARCHAR(64) |
|
ip_address |
TEXT |
|
user_agent |
TEXT |
|
metadata |
JSONB |
|
created_at |
TIMESTAMPTZ |
Migrations
Migrations are in /migrations/ numbered 001 through 039. They run automatically on server startup via sqlx. Key migrations:
| Migration | Description |
|---|---|
| 001 | users table |
| 002 | identity_keys (E2E) |
| 003 | refresh_tokens |
| 004 | audit_log |
| 005 | Prekeys (signed + one-time) |
| 006 | channels + channel_members |
| 007 | messages |
| 008 | files |
| 009 | dm_pairs |
| 010 | voice_sessions |
| 011 | reactions |
| 012 | Unread tracking (read_cursors) |
| 013 | channel_bans |
| 014 | groups + group_members |
| 019 | communities + community_members / community_invites / community_bans |
| 020 | user_preferences |
| 021 | pinned_messages |
| 022 | sender_key_distributions |
| 024 | Permissions (slow mode, voice_background) |
| 026 | Security suite (blocked_hashes) |
| 028 | Blocking and reports |
| 029 | New features (webhooks, timeouts, warnings, scheduled_messages, polls, bookmarks, custom_emojis, announcements) |
| 034 | Community customization |
| 038 | message_edits |