Initial scaffold: FocusFlow ADHD Task Manager backend
Dart Shelf API with modules: auth (JWT + PBKDF2), tasks (CRUD + dopamine scorer), streaks (forgiveness + freeze), rewards (variable reward engine), time perception, sync (offline-first push/pull), rooms (body doubling placeholder). Includes DB migration (001_initial_schema.sql) and Docker Compose. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
385
migrations/001_initial_schema.sql
Normal file
385
migrations/001_initial_schema.sql
Normal file
@@ -0,0 +1,385 @@
|
||||
-- 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;
|
||||
Reference in New Issue
Block a user