Skip to content

Database Schema

This document describes the PostgreSQL database schema used by the Signal Bot.

For a new installation, PostgreSQL needs the base schema before the bot can run:

Terminal window
# 1. Start the postgres container
docker compose up -d postgres
# 2. Apply the initial schema (creates all tables)
docker exec -i signal-bot-postgres psql -U signal_bot signal_bot < database/000_init_schema.sql
# 3. Start the bot
docker compose up -d signal-bot

That’s it! The 000_init_schema.sql file contains everything needed for a fresh installation.

The database/ directory contains:

FileDescription
000_init_schema.sqlClean initial schema - Use this for fresh installations. Contains all tables, indexes, constraints with no data.
000_base_schema.sqlProduction schema dump (for reference/comparison)
001_*.sql - 054_*.sqlIncremental migrations - Schema changes added over time. Only needed if upgrading from an older version.

Important Notes:

  • For fresh installs, just use 000_init_schema.sql
  • Migrations (001+) are incremental and use IF NOT EXISTS / ADD COLUMN IF NOT EXISTS
  • On an existing install, apply only the migrations you haven’t run yet

TablePurpose
signal_groupsRegistered Signal groups with settings (AI enabled, meme enabled, etc.)
signal_membersUser profiles (UUID, phone, display name, profile name)
signal_member_group_membershipsWhich members are in which groups
signal_messagesMessage history for context and rollups
TablePurpose
q_and_a_questionsQuestions asked via !q or !ask
q_and_a_answersAnswers to questions via !a
q_and_a_clarificationsFollow-up clarifications
q_and_a_inquiries!inquiry prompts for elaboration
TablePurpose
verification_requestsUser verification workflow (INDOC, safety number changes)
user_removalsAudit log for user removals
TablePurpose
breakout_roomsTemporary discussion groups
breakout_room_membersParticipants in breakout rooms
breakout_room_messagesMessages within breakout sessions
breakout_annotationsTasks/notes from breakout discussions
TablePurpose
known_identity_fingerprintsStores signal-cli identity fingerprints for safety number change detection (polled every 5 min)
identity_change_logAudit trail of detected identity/fingerprint changes with action taken
TablePurpose
news_linksShared news URLs
repository_linksShared git repos
social_media_linksShared social media content
memesStored meme library
archived_filesFile archive metadata
archived_videosVideo metadata
youtube_transcriptsCached video transcripts (YouTube, Instagram, TikTok, etc.)
TablePurpose
entity_aliasesMaps alternate entity names to canonical form (e.g., “USA” → “United States”)

Note: Entity data (people, organizations, locations) is extracted from news_links and social_media_links via JSONB entities column. The controversial_claims JSONB column tracks disputed claims for cross-article analysis.

TablePurpose
rss_feedsRegistered RSS feed sources
rss_bookmarksUser-bookmarked RSS articles
rss_admin_settingsRSS reader admin configuration
TablePurpose
user_profilesExtended user profile data (backfilled from signal_members)
bot_adminsBot admin roles and permissions (DB-managed admin access)
portal_announcementsCommunity portal announcements
TablePurpose
missed_messagesProtocol exceptions and quote-detected message gaps
dm_link_trackingDM link submission tracking
youtube_transcriptsCached video transcripts (YouTube, Instagram, TikTok, etc.)
TablePurpose
scheduled_announcementsPending announcements
remindersUser reminders via !remindme
scheduled_rollupsWeekly/daily digest schedules
TablePurpose
bot_command_usageTrack command usage statistics
bot_errorsError logging for debugging
debug_logsOptional debug logging
today_i_learnedTIL entries
tasksTask tracking

The bot connects to PostgreSQL using these env vars:

Terminal window
DB_HOST=signal-bot-postgres # Use full container name to avoid DNS conflicts
DB_PORT=5432
DB_NAME=signal_bot
DB_USER=signal_bot
DB_PASSWORD=<your-password>

Important: Use DB_HOST=signal-bot-postgres (full container name) instead of just postgres to avoid DNS resolution conflicts with other postgres containers on shared networks.


  1. Create a new migration file: database/NNN_description.sql
  2. Use CREATE TABLE IF NOT EXISTS and ADD COLUMN IF NOT EXISTS for safety
  3. Add the table to ALLOWED_TABLES in src/src/db/postgres-client.ts
  4. Apply to production:
    Terminal window
    docker exec -i signal-bot-postgres psql -U signal_bot signal_bot < database/NNN_description.sql
-- Always use IF NOT EXISTS for safety (idempotent migrations)
CREATE TABLE IF NOT EXISTS my_new_table (
id SERIAL PRIMARY KEY,
name VARCHAR(255) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- Adding columns safely
ALTER TABLE existing_table ADD COLUMN IF NOT EXISTS new_column TEXT;
-- Creating indexes
CREATE INDEX IF NOT EXISTS idx_my_table_name ON my_new_table(name);

To update 000_base_schema.sql from production:

Terminal window
ssh proxmox "docker exec signal-bot-postgres pg_dump -U signal_bot -s signal_bot" > database/000_base_schema.sql

The base schema hasn’t been applied. Run:

Terminal window
docker exec -i signal-bot-postgres psql -U signal_bot signal_bot < database/000_base_schema.sql

Migration fails with “column already exists”

Section titled “Migration fails with “column already exists””

This is fine - migrations use IF NOT EXISTS so they’re idempotent. The column was already added.

  1. Check DB_HOST uses full container name (signal-bot-postgres)
  2. Verify postgres container is healthy: docker compose ps
  3. Check credentials match what’s in the postgres container env

Extract fresh schema from production and compare:

Terminal window
ssh proxmox "docker exec signal-bot-postgres pg_dump -U signal_bot -s signal_bot" > /tmp/prod_schema.sql
diff database/000_base_schema.sql /tmp/prod_schema.sql
Terminal window
# Connect to database
docker exec -it signal-bot-postgres psql -U signal_bot signal_bot
# List all tables
\dt
# Describe a specific table
\d signal_groups
# Show table with indexes
\d+ signal_messages
-- Count messages by group
SELECT g.name, COUNT(m.id) as message_count
FROM signal_groups g
LEFT JOIN signal_messages m ON g.id = m.group_id
GROUP BY g.name
ORDER BY message_count DESC;
-- Find recent questions
SELECT hash, question_text, created_at
FROM q_and_a_questions
ORDER BY created_at DESC
LIMIT 10;
-- Check pending reminders
SELECT id, user_name, message, remind_at
FROM reminders
WHERE delivered = false
ORDER BY remind_at;

Terminal window
# Full backup
ssh proxmox "docker exec signal-bot-postgres pg_dump -U signal_bot signal_bot > /home/signal-bot-selfhosted/data/backups/backup_$(date +%Y%m%d_%H%M%S).sql"
# Compressed backup
ssh proxmox "docker exec signal-bot-postgres pg_dump -U signal_bot signal_bot | gzip > /home/signal-bot-selfhosted/data/backups/backup_$(date +%Y%m%d).sql.gz"
Terminal window
# Stop the bot first
docker compose stop signal-bot
# Restore
docker exec -i signal-bot-postgres psql -U signal_bot signal_bot < backup.sql
# Start the bot
docker compose start signal-bot