ATlast — you'll never need to find your favorites on another platform again. Find your favs in the ATmosphere.
atproto
at master 151 lines 5.5 kB view raw
1-- ATlast Database Schema 2-- Migration Plan v2.0 - Phase 1 3-- Self-hosted PostgreSQL schema with fuzzy matching support 4 5-- Enable extensions 6CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; 7CREATE EXTENSION IF NOT EXISTS "pg_trgm"; -- For fuzzy matching 8 9-- OAuth state storage (transient) 10CREATE TABLE oauth_states ( 11 state TEXT PRIMARY KEY, 12 data JSONB NOT NULL, 13 created_at TIMESTAMP DEFAULT NOW() 14); 15CREATE INDEX idx_oauth_states_created ON oauth_states(created_at); 16 17-- OAuth sessions (transient) 18CREATE TABLE oauth_sessions ( 19 did TEXT PRIMARY KEY, 20 session_data JSONB NOT NULL, 21 updated_at TIMESTAMP DEFAULT NOW() 22); 23 24-- User sessions (transient) 25CREATE TABLE user_sessions ( 26 session_id TEXT PRIMARY KEY, 27 did TEXT NOT NULL, 28 fingerprint TEXT NOT NULL, 29 created_at TIMESTAMP DEFAULT NOW(), 30 expires_at TIMESTAMP NOT NULL 31); 32CREATE INDEX idx_user_sessions_did ON user_sessions(did); 33CREATE INDEX idx_user_sessions_expires ON user_sessions(expires_at); 34 35-- User uploads (persistent) 36CREATE TABLE user_uploads ( 37 upload_id TEXT PRIMARY KEY, 38 user_did TEXT NOT NULL, 39 source_platform TEXT NOT NULL, 40 created_at TIMESTAMP DEFAULT NOW(), 41 total_users INTEGER DEFAULT 0, 42 matched_users INTEGER DEFAULT 0, 43 unmatched_users INTEGER DEFAULT 0 44); 45CREATE INDEX idx_user_uploads_user_did ON user_uploads(user_did); 46-- Note: check_frequency and last_checked removed - no periodic checking in Phase 1 47 48-- Source accounts (persistent) 49CREATE TABLE source_accounts ( 50 id SERIAL PRIMARY KEY, 51 source_platform TEXT NOT NULL, 52 original_username TEXT NOT NULL, 53 normalized_username TEXT NOT NULL, 54 date_on_source TIMESTAMP, 55 created_at TIMESTAMP DEFAULT NOW(), 56 UNIQUE(source_platform, normalized_username) 57); 58CREATE INDEX idx_source_accounts_normalized ON source_accounts 59 USING gin(normalized_username gin_trgm_ops); -- Fuzzy matching! 60CREATE INDEX idx_source_accounts_platform ON source_accounts(source_platform); 61 62-- User-source follows (join table) 63CREATE TABLE user_source_follows ( 64 user_did TEXT NOT NULL, 65 upload_id TEXT NOT NULL REFERENCES user_uploads(upload_id) ON DELETE CASCADE, 66 source_account_id INTEGER NOT NULL REFERENCES source_accounts(id), 67 found_at TIMESTAMP DEFAULT NOW(), 68 PRIMARY KEY (upload_id, source_account_id) 69); 70CREATE INDEX idx_user_source_follows_user ON user_source_follows(user_did); 71CREATE INDEX idx_user_source_follows_source ON user_source_follows(source_account_id); 72 73-- AT Protocol matches (persistent) 74CREATE TABLE atproto_matches ( 75 id SERIAL PRIMARY KEY, 76 source_account_id INTEGER NOT NULL REFERENCES source_accounts(id), 77 atproto_did TEXT NOT NULL, 78 atproto_handle TEXT NOT NULL, 79 display_name TEXT, 80 match_score INTEGER NOT NULL, 81 post_count INTEGER, 82 follower_count INTEGER, 83 follow_status JSONB DEFAULT '{}', 84 found_at TIMESTAMP DEFAULT NOW(), 85 UNIQUE(source_account_id, atproto_did) 86); 87CREATE INDEX idx_atproto_matches_source ON atproto_matches(source_account_id); 88CREATE INDEX idx_atproto_matches_did ON atproto_matches(atproto_did); 89CREATE INDEX idx_atproto_matches_score ON atproto_matches(match_score DESC); 90 91-- User match status (persistent) 92CREATE TABLE user_match_status ( 93 user_did TEXT NOT NULL, 94 match_id INTEGER NOT NULL REFERENCES atproto_matches(id), 95 viewed BOOLEAN DEFAULT FALSE, 96 dismissed BOOLEAN DEFAULT FALSE, 97 followed BOOLEAN DEFAULT FALSE, 98 notified BOOLEAN DEFAULT FALSE, 99 updated_at TIMESTAMP DEFAULT NOW(), 100 PRIMARY KEY (user_did, match_id) 101); 102CREATE INDEX idx_user_match_status_user ON user_match_status(user_did); 103CREATE INDEX idx_user_match_status_notified ON user_match_status(user_did, notified) 104 WHERE notified = FALSE; 105 106-- Notification queue (transient - for Phase 2) 107CREATE TABLE notification_queue ( 108 id SERIAL PRIMARY KEY, 109 user_did TEXT NOT NULL, 110 match_id INTEGER NOT NULL REFERENCES atproto_matches(id), 111 notification_type TEXT NOT NULL, -- 'in_app', 'bluesky_dm', 'partner_api' 112 status TEXT DEFAULT 'pending', -- 'pending', 'sent', 'failed' 113 attempts INTEGER DEFAULT 0, 114 last_attempt TIMESTAMP, 115 error_message TEXT, -- Store error details for debugging 116 created_at TIMESTAMP DEFAULT NOW() 117); 118CREATE INDEX idx_notification_queue_status ON notification_queue(status) 119 WHERE status = 'pending'; 120CREATE INDEX idx_notification_queue_user ON notification_queue(user_did); 121 122-- Partner API keys (for Phase 2) 123CREATE TABLE partner_api_keys ( 124 id SERIAL PRIMARY KEY, 125 partner_name TEXT NOT NULL, -- 'skylight', 'spark', etc. 126 api_key_hash TEXT NOT NULL UNIQUE, -- SHA-256 hashed API key 127 created_at TIMESTAMP DEFAULT NOW(), 128 last_used TIMESTAMP, 129 is_active BOOLEAN DEFAULT TRUE 130); 131CREATE INDEX idx_partner_api_keys_hash ON partner_api_keys(api_key_hash) 132 WHERE is_active = TRUE; 133 134-- Cleanup function for old transient data 135CREATE OR REPLACE FUNCTION cleanup_transient_data() RETURNS void AS $$ 136BEGIN 137 -- Clean expired OAuth states (1 hour) 138 DELETE FROM oauth_states WHERE created_at < NOW() - INTERVAL '1 hour'; 139 140 -- Clean expired sessions 141 DELETE FROM user_sessions WHERE expires_at < NOW(); 142 143 -- Clean old sent notifications (7 days) 144 DELETE FROM notification_queue 145 WHERE status = 'sent' AND created_at < NOW() - INTERVAL '7 days'; 146 147 -- Clean old failed notifications (30 days) 148 DELETE FROM notification_queue 149 WHERE status = 'failed' AND created_at < NOW() - INTERVAL '30 days'; 150END; 151$$ LANGUAGE plpgsql;