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>
386 lines
15 KiB
PL/PgSQL
386 lines
15 KiB
PL/PgSQL
-- 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;
|