-- FocusFlow Initial Schema -- Migration 001: Core tables for ADHD Task Manager MVP BEGIN; -- ============================================================ -- EXTENSIONS -- ============================================================ CREATE EXTENSION IF NOT EXISTS "pgcrypto"; -- ============================================================ -- USERS & AUTH -- ============================================================ CREATE TABLE users ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), email VARCHAR(255) UNIQUE NOT NULL, password_hash VARCHAR(255) NOT NULL, display_name VARCHAR(100) NOT NULL, avatar_url VARCHAR(500), -- ADHD-specific preferences default_energy_level SMALLINT DEFAULT 3 CHECK (default_energy_level BETWEEN 1 AND 5), focus_duration_minutes INTEGER DEFAULT 25, reward_style VARCHAR(20) DEFAULT 'playful', -- playful | minimal | data forgiveness_enabled BOOLEAN DEFAULT TRUE, daily_task_limit INTEGER DEFAULT 10, -- Subscription subscription_tier VARCHAR(20) DEFAULT 'free', -- free | premium | lifetime subscription_expires_at TIMESTAMPTZ, -- Timestamps created_at TIMESTAMPTZ DEFAULT NOW(), updated_at TIMESTAMPTZ DEFAULT NOW(), deleted_at TIMESTAMPTZ ); CREATE INDEX idx_users_email ON users(email) WHERE deleted_at IS NULL; CREATE TABLE refresh_tokens ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, token VARCHAR(255) NOT NULL, expires_at TIMESTAMPTZ NOT NULL, revoked BOOLEAN DEFAULT FALSE, created_at TIMESTAMPTZ DEFAULT NOW() ); CREATE INDEX idx_refresh_tokens_user ON refresh_tokens(user_id) WHERE revoked = FALSE; CREATE INDEX idx_refresh_tokens_token ON refresh_tokens(token) WHERE revoked = FALSE; -- ============================================================ -- TASKS -- ============================================================ CREATE TABLE tasks ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, title VARCHAR(300) NOT NULL, description TEXT, status VARCHAR(20) DEFAULT 'pending', -- pending | in_progress | completed | skipped | archived priority SMALLINT DEFAULT 3 CHECK (priority BETWEEN 1 AND 5), energy_level SMALLINT DEFAULT 3 CHECK (energy_level BETWEEN 1 AND 5), -- Time estimation estimated_minutes INTEGER, actual_minutes INTEGER, -- Dopamine scoring inputs dopamine_score NUMERIC(5,4) DEFAULT 0, novelty_factor NUMERIC(3,2) DEFAULT 0.5, times_postponed INTEGER DEFAULT 0, last_interacted_at TIMESTAMPTZ, -- Organization category VARCHAR(100), tags TEXT[] DEFAULT '{}', due_date TIMESTAMPTZ, -- Recurrence is_recurring BOOLEAN DEFAULT FALSE, recurrence_rule VARCHAR(200), -- RRULE format parent_task_id UUID REFERENCES tasks(id), -- Completion completed_at TIMESTAMPTZ, -- Sync version INTEGER DEFAULT 1, -- Timestamps created_at TIMESTAMPTZ DEFAULT NOW(), updated_at TIMESTAMPTZ DEFAULT NOW(), deleted_at TIMESTAMPTZ ); CREATE INDEX idx_tasks_user_status ON tasks(user_id, status) WHERE deleted_at IS NULL; CREATE INDEX idx_tasks_user_due ON tasks(user_id, due_date) WHERE deleted_at IS NULL AND status = 'pending'; CREATE INDEX idx_tasks_dopamine ON tasks(user_id, dopamine_score DESC) WHERE deleted_at IS NULL AND status = 'pending'; CREATE INDEX idx_tasks_tags ON tasks USING GIN(tags); CREATE INDEX idx_tasks_parent ON tasks(parent_task_id) WHERE parent_task_id IS NOT NULL; -- ============================================================ -- STREAKS -- ============================================================ CREATE TABLE streaks ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), user_id UUID UNIQUE NOT NULL REFERENCES users(id) ON DELETE CASCADE, current_count INTEGER DEFAULT 0, longest_count INTEGER DEFAULT 0, -- Forgiveness mechanics grace_days SMALLINT DEFAULT 2, grace_used SMALLINT DEFAULT 0, frozen_until DATE, -- Decay instead of reset (premium) decay_enabled BOOLEAN DEFAULT FALSE, -- Tracking last_completed_date DATE, started_at TIMESTAMPTZ DEFAULT NOW(), updated_at TIMESTAMPTZ DEFAULT NOW() ); CREATE TABLE streak_entries ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), streak_id UUID NOT NULL REFERENCES streaks(id) ON DELETE CASCADE, entry_date DATE NOT NULL, tasks_done INTEGER DEFAULT 0, was_forgiven BOOLEAN DEFAULT FALSE, was_frozen BOOLEAN DEFAULT FALSE, -- Points earned on this day points_earned INTEGER DEFAULT 0, created_at TIMESTAMPTZ DEFAULT NOW(), UNIQUE(streak_id, entry_date) ); CREATE INDEX idx_streak_entries_date ON streak_entries(streak_id, entry_date DESC); -- ============================================================ -- REWARDS -- ============================================================ CREATE TABLE rewards ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, task_id UUID REFERENCES tasks(id), -- Reward details reward_type VARCHAR(20) NOT NULL, -- points | badge | animation | message | unlock | surprise magnitude NUMERIC(8,2) NOT NULL DEFAULT 1.0, is_surprise BOOLEAN DEFAULT FALSE, -- Content title VARCHAR(200), description TEXT, animation_key VARCHAR(100), -- Lottie animation identifier -- Metadata created_at TIMESTAMPTZ DEFAULT NOW() ); CREATE INDEX idx_rewards_user ON rewards(user_id, created_at DESC); CREATE INDEX idx_rewards_type ON rewards(user_id, reward_type); -- User point balance (materialized for fast reads) CREATE TABLE user_points ( user_id UUID PRIMARY KEY REFERENCES users(id) ON DELETE CASCADE, total INTEGER DEFAULT 0, level INTEGER DEFAULT 1, updated_at TIMESTAMPTZ DEFAULT NOW() ); -- ============================================================ -- TIME PERCEPTION TRACKING -- ============================================================ CREATE TABLE time_estimates ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, task_id UUID REFERENCES tasks(id), -- Estimate vs actual estimated_minutes INTEGER NOT NULL, actual_minutes INTEGER, accuracy_ratio NUMERIC(5,2), -- actual / estimated -- Context category VARCHAR(100), energy_level SMALLINT, -- Timestamps started_at TIMESTAMPTZ, completed_at TIMESTAMPTZ, created_at TIMESTAMPTZ DEFAULT NOW() ); CREATE INDEX idx_time_estimates_user ON time_estimates(user_id, created_at DESC); CREATE INDEX idx_time_estimates_category ON time_estimates(user_id, category); -- ============================================================ -- BODY DOUBLING / CO-WORKING ROOMS (Phase 2) -- ============================================================ CREATE TABLE coworking_rooms ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), name VARCHAR(100) NOT NULL, description TEXT, host_id UUID NOT NULL REFERENCES users(id), is_public BOOLEAN DEFAULT TRUE, max_participants INTEGER DEFAULT 10, ambient_sound VARCHAR(50) DEFAULT 'none', -- none | cafe | rain | lofi | forest status VARCHAR(20) DEFAULT 'active', -- active | ended started_at TIMESTAMPTZ DEFAULT NOW(), ended_at TIMESTAMPTZ ); CREATE TABLE room_participants ( room_id UUID NOT NULL REFERENCES coworking_rooms(id) ON DELETE CASCADE, user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, current_task VARCHAR(200), joined_at TIMESTAMPTZ DEFAULT NOW(), left_at TIMESTAMPTZ, PRIMARY KEY (room_id, user_id) ); CREATE INDEX idx_room_participants_active ON room_participants(room_id) WHERE left_at IS NULL; -- ============================================================ -- ACCOUNTABILITY PARTNERS (Phase 2) -- ============================================================ CREATE TABLE accountability_partners ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, partner_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, status VARCHAR(20) DEFAULT 'pending', -- pending | active | blocked created_at TIMESTAMPTZ DEFAULT NOW(), UNIQUE(user_id, partner_id) ); CREATE TABLE nudges ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), from_id UUID NOT NULL REFERENCES users(id), to_id UUID NOT NULL REFERENCES users(id), message TEXT, nudge_type VARCHAR(20) DEFAULT 'gentle', -- gentle | encouraging | celebratory read_at TIMESTAMPTZ, created_at TIMESTAMPTZ DEFAULT NOW() ); CREATE INDEX idx_nudges_to ON nudges(to_id, created_at DESC) WHERE read_at IS NULL; -- ============================================================ -- COACHING MARKETPLACE (Phase 3) -- ============================================================ CREATE TABLE coaches ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), user_id UUID UNIQUE NOT NULL REFERENCES users(id), bio TEXT, specializations TEXT[] DEFAULT '{}', hourly_rate NUMERIC(8,2), currency VARCHAR(3) DEFAULT 'USD', rating_avg NUMERIC(3,2) DEFAULT 0, rating_count INTEGER DEFAULT 0, verified BOOLEAN DEFAULT FALSE, status VARCHAR(20) DEFAULT 'pending', -- pending | active | suspended created_at TIMESTAMPTZ DEFAULT NOW() ); CREATE TABLE coaching_sessions ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), coach_id UUID NOT NULL REFERENCES coaches(id), client_id UUID NOT NULL REFERENCES users(id), status VARCHAR(20) DEFAULT 'scheduled', -- scheduled | in_progress | completed | cancelled scheduled_at TIMESTAMPTZ NOT NULL, duration_min INTEGER DEFAULT 30, notes TEXT, rating SMALLINT CHECK (rating BETWEEN 1 AND 5), amount NUMERIC(8,2), commission NUMERIC(8,2), created_at TIMESTAMPTZ DEFAULT NOW() ); CREATE INDEX idx_coaching_sessions_coach ON coaching_sessions(coach_id, scheduled_at); CREATE INDEX idx_coaching_sessions_client ON coaching_sessions(client_id, scheduled_at); -- ============================================================ -- PUSH NOTIFICATIONS -- ============================================================ CREATE TABLE push_tokens ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, token VARCHAR(500) NOT NULL, platform VARCHAR(20) NOT NULL, -- ios | android | web created_at TIMESTAMPTZ DEFAULT NOW(), UNIQUE(user_id, token) ); CREATE TABLE notification_log ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), user_id UUID NOT NULL REFERENCES users(id), notification_type VARCHAR(50) NOT NULL, title VARCHAR(200), body TEXT, data_json JSONB, sent_at TIMESTAMPTZ DEFAULT NOW(), read_at TIMESTAMPTZ ); CREATE INDEX idx_notification_log_user ON notification_log(user_id, sent_at DESC); -- ============================================================ -- SYNC TRACKING -- ============================================================ CREATE TABLE sync_changelog ( id BIGSERIAL PRIMARY KEY, entity_type VARCHAR(50) NOT NULL, -- task | streak | reward | time_estimate entity_id UUID NOT NULL, action VARCHAR(20) NOT NULL, -- insert | update | delete changed_at TIMESTAMPTZ DEFAULT NOW(), changed_by UUID REFERENCES users(id) ); CREATE INDEX idx_sync_changelog_type_time ON sync_changelog(entity_type, changed_at); CREATE INDEX idx_sync_changelog_user ON sync_changelog(changed_by, changed_at); -- ============================================================ -- SUBSCRIPTIONS -- ============================================================ CREATE TABLE subscriptions ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, provider VARCHAR(20) NOT NULL, -- stripe | revenuecat | apple | google provider_subscription_id VARCHAR(200), plan VARCHAR(50) NOT NULL, -- premium_monthly | premium_yearly | lifetime status VARCHAR(20) DEFAULT 'active', -- active | cancelled | expired | past_due current_period_start TIMESTAMPTZ, current_period_end TIMESTAMPTZ, cancelled_at TIMESTAMPTZ, created_at TIMESTAMPTZ DEFAULT NOW(), updated_at TIMESTAMPTZ DEFAULT NOW() ); CREATE INDEX idx_subscriptions_user ON subscriptions(user_id); CREATE INDEX idx_subscriptions_provider ON subscriptions(provider, provider_subscription_id); -- ============================================================ -- UPDATED_AT TRIGGER FUNCTION -- ============================================================ CREATE OR REPLACE FUNCTION update_updated_at_column() RETURNS TRIGGER AS $$ BEGIN NEW.updated_at = NOW(); RETURN NEW; END; $$ language 'plpgsql'; -- Apply trigger to tables with updated_at CREATE TRIGGER update_users_updated_at BEFORE UPDATE ON users FOR EACH ROW EXECUTE FUNCTION update_updated_at_column(); CREATE TRIGGER update_tasks_updated_at BEFORE UPDATE ON tasks FOR EACH ROW EXECUTE FUNCTION update_updated_at_column(); CREATE TRIGGER update_streaks_updated_at BEFORE UPDATE ON streaks FOR EACH ROW EXECUTE FUNCTION update_updated_at_column(); CREATE TRIGGER update_subscriptions_updated_at BEFORE UPDATE ON subscriptions FOR EACH ROW EXECUTE FUNCTION update_updated_at_column(); -- ============================================================ -- SYNC TRIGGER: Auto-log changes to sync_changelog -- ============================================================ CREATE OR REPLACE FUNCTION log_sync_change() RETURNS TRIGGER AS $$ BEGIN IF TG_OP = 'DELETE' THEN INSERT INTO sync_changelog (entity_type, entity_id, action, changed_by) VALUES (TG_TABLE_NAME, OLD.id, 'delete', OLD.user_id); RETURN OLD; ELSE INSERT INTO sync_changelog (entity_type, entity_id, action, changed_by) VALUES (TG_TABLE_NAME, NEW.id, TG_OP::VARCHAR, NEW.user_id); RETURN NEW; END IF; END; $$ language 'plpgsql'; -- Track task changes for offline sync CREATE TRIGGER sync_tasks_changes AFTER INSERT OR UPDATE OR DELETE ON tasks FOR EACH ROW EXECUTE FUNCTION log_sync_change(); COMMIT;