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:
# 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
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:
File Description 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
Table Purpose 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
Table Purpose 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
Table Purpose verification_requestsUser verification workflow (INDOC, safety number changes) user_removalsAudit log for user removals
Table Purpose breakout_roomsTemporary discussion groups breakout_room_membersParticipants in breakout rooms breakout_room_messagesMessages within breakout sessions breakout_annotationsTasks/notes from breakout discussions
Table Purpose 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
Table Purpose 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.)
Table Purpose 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.
Table Purpose rss_feedsRegistered RSS feed sources rss_bookmarksUser-bookmarked RSS articles rss_admin_settingsRSS reader admin configuration
Table Purpose user_profilesExtended user profile data (backfilled from signal_members) bot_adminsBot admin roles and permissions (DB-managed admin access) portal_announcementsCommunity portal announcements
Table Purpose missed_messagesProtocol exceptions and quote-detected message gaps dm_link_trackingDM link submission tracking youtube_transcriptsCached video transcripts (YouTube, Instagram, TikTok, etc.)
Table Purpose scheduled_announcementsPending announcements remindersUser reminders via !remindme scheduled_rollupsWeekly/daily digest schedules
Table Purpose 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:
DB_HOST = signal-bot-postgres # Use full container name to avoid DNS conflicts
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.
Create a new migration file: database/NNN_description.sql
Use CREATE TABLE IF NOT EXISTS and ADD COLUMN IF NOT EXISTS for safety
Add the table to ALLOWED_TABLES in src/src/db/postgres-client.ts
Apply to production:
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 (
name VARCHAR ( 255 ) NOT NULL ,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
ALTER TABLE existing_table ADD COLUMN IF NOT EXISTS new_column TEXT ;
CREATE INDEX IF NOT EXISTS idx_my_table_name ON my_new_table( name );
To update 000_base_schema.sql from production:
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:
docker exec -i signal-bot-postgres psql -U signal_bot signal_bot < database/000_base_schema.sql
This is fine - migrations use IF NOT EXISTS so they’re idempotent. The column was already added.
Check DB_HOST uses full container name (signal-bot-postgres)
Verify postgres container is healthy: docker compose ps
Check credentials match what’s in the postgres container env
Extract fresh schema from production and compare:
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
docker exec -it signal-bot-postgres psql -U signal_bot signal_bot
# Describe a specific table
# Show table with indexes
-- Count messages by group
SELECT g . name , COUNT ( m . id ) as message_count
LEFT JOIN signal_messages m ON g . id = m . group_id
ORDER BY message_count DESC ;
SELECT hash , question_text, created_at
-- Check pending reminders
SELECT id, user_name, message , remind_at
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 "
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 "
docker compose stop signal-bot
docker exec -i signal-bot-postgres psql -U signal_bot signal_bot < backup.sql
docker compose start signal-bot