A community based topic aggregation platform built on atproto
1-- +goose Up
2-- Phone verification tables (privacy-first: only hashed phones stored)
3
4-- Stores phone verification status (hashed phone only, no plaintext)
5CREATE TABLE phone_verifications (
6 did TEXT PRIMARY KEY REFERENCES users(did) ON DELETE CASCADE,
7 phone_hash TEXT UNIQUE NOT NULL, -- HMAC-SHA256(phone, pepper) for duplicate detection
8 verified_at TIMESTAMP WITH TIME ZONE NOT NULL,
9 expires_at TIMESTAMP WITH TIME ZONE NOT NULL, -- Annual re-verification (365 days)
10 created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
11 updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
12);
13
14-- Index for checking if phone is already verified (duplicate prevention)
15CREATE INDEX idx_phone_verifications_phone_hash ON phone_verifications(phone_hash);
16CREATE INDEX idx_phone_verifications_expires_at ON phone_verifications(expires_at);
17
18-- Stores pending verification requests (short-lived, auto-cleanup)
19CREATE TABLE phone_verification_requests (
20 request_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
21 did TEXT NOT NULL REFERENCES users(did) ON DELETE CASCADE,
22 phone_hash TEXT NOT NULL, -- Same hash as phone_verifications for duplicate check
23 otp_code_hash TEXT NOT NULL, -- Bcrypt hash of OTP code
24 attempts INT DEFAULT 0, -- Track failed attempts (max 3)
25 created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
26 expires_at TIMESTAMP WITH TIME ZONE NOT NULL -- OTP expires in 10 minutes
27);
28
29CREATE INDEX idx_phone_verification_requests_did ON phone_verification_requests(did);
30CREATE INDEX idx_phone_verification_requests_expires_at ON phone_verification_requests(expires_at);
31
32-- Rate limiting table (prevent SMS abuse)
33CREATE TABLE phone_verification_rate_limits (
34 identifier TEXT PRIMARY KEY, -- Could be DID or phone_hash
35 request_count INT DEFAULT 1,
36 window_start TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
37 last_request_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
38);
39
40CREATE INDEX idx_phone_verification_rate_limits_window ON phone_verification_rate_limits(window_start);
41
42-- Audit log for security monitoring
43CREATE TABLE phone_verification_audit_log (
44 id BIGSERIAL PRIMARY KEY,
45 did TEXT REFERENCES users(did) ON DELETE SET NULL,
46 event_type TEXT NOT NULL, -- 'request_sent', 'verification_success', 'verification_failed', 'rate_limit_hit'
47 phone_hash TEXT, -- Optional, for tracking abuse patterns
48 ip_address INET, -- Track source IP for fraud detection
49 user_agent TEXT,
50 metadata JSONB, -- Additional context (error details, etc.)
51 created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
52);
53
54CREATE INDEX idx_phone_verification_audit_log_did ON phone_verification_audit_log(did);
55CREATE INDEX idx_phone_verification_audit_log_event_type ON phone_verification_audit_log(event_type);
56CREATE INDEX idx_phone_verification_audit_log_created_at ON phone_verification_audit_log(created_at);
57
58-- Cleanup function for expired requests (run via cron)
59CREATE OR REPLACE FUNCTION cleanup_expired_verification_requests()
60RETURNS void AS $$
61BEGIN
62 DELETE FROM phone_verification_requests WHERE expires_at < NOW();
63 DELETE FROM phone_verification_rate_limits WHERE window_start < NOW() - INTERVAL '1 hour';
64END;
65$$ LANGUAGE plpgsql;
66
67-- +goose Down
68DROP FUNCTION IF EXISTS cleanup_expired_verification_requests();
69DROP TABLE IF EXISTS phone_verification_audit_log;
70DROP TABLE IF EXISTS phone_verification_rate_limits;
71DROP TABLE IF EXISTS phone_verification_requests;
72DROP TABLE IF EXISTS phone_verifications;