Studley AI uses PostgreSQL as its database with Supabase for authentication and real-time features. The schema is organized into several key areas: authentication, user data, content generation, feedback, and integrations.
Studley AI uses a hybrid authentication system with both Supabase auth.users and a custom users table. New features use Supabase auth, while legacy features use the custom table.
CREATE TABLE IF NOT EXISTS users ( id TEXT PRIMARY KEY DEFAULT gen_random_uuid()::text, name TEXT, email TEXT UNIQUE NOT NULL, "emailVerified" TIMESTAMP, password TEXT NOT NULL, image TEXT, credits INTEGER DEFAULT 5000, "createdAt" TIMESTAMP DEFAULT CURRENT_TIMESTAMP, "updatedAt" TIMESTAMP DEFAULT CURRENT_TIMESTAMP);
Extended user profile information (Supabase auth):
CREATE TABLE IF NOT EXISTS public.user_profiles ( id uuid PRIMARY KEY DEFAULT gen_random_uuid(), user_id uuid NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE, full_name text, avatar_url text, created_at timestamp with time zone DEFAULT now(), updated_at timestamp with time zone DEFAULT now(), UNIQUE(user_id));
Features:
Links to Supabase auth.users
Stores profile pictures in Vercel Blob
One profile per user (enforced by UNIQUE constraint)
CREATE TABLE IF NOT EXISTS accounts ( id TEXT PRIMARY KEY DEFAULT gen_random_uuid()::text, "userId" TEXT NOT NULL, type TEXT NOT NULL, provider TEXT NOT NULL, "providerAccountId" TEXT NOT NULL, refresh_token TEXT, access_token TEXT, expires_at INTEGER, token_type TEXT, scope TEXT, id_token TEXT, session_state TEXT, FOREIGN KEY ("userId") REFERENCES users(id) ON DELETE CASCADE, UNIQUE(provider, "providerAccountId"));
CREATE TABLE IF NOT EXISTS sessions ( id TEXT PRIMARY KEY DEFAULT gen_random_uuid()::text, "sessionToken" TEXT UNIQUE NOT NULL, "userId" TEXT NOT NULL, expires TIMESTAMP NOT NULL, FOREIGN KEY ("userId") REFERENCES users(id) ON DELETE CASCADE);
CREATE TABLE IF NOT EXISTS verification_tokens ( identifier TEXT NOT NULL, token TEXT UNIQUE NOT NULL, expires TIMESTAMP NOT NULL, UNIQUE(identifier, token));
CREATE TABLE IF NOT EXISTS public.user_library_items ( id uuid PRIMARY KEY DEFAULT gen_random_uuid(), user_id uuid NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE, folder_id uuid REFERENCES public.user_folders(id) ON DELETE SET NULL, item_type text NOT NULL, -- 'quiz', 'flashcard', 'study_guide', 'writing_prompt' title text NOT NULL, topic text, content jsonb NOT NULL, created_at timestamp with time zone DEFAULT now(), updated_at timestamp with time zone DEFAULT now());
CREATE TABLE IF NOT EXISTS public.user_folders ( id uuid PRIMARY KEY DEFAULT gen_random_uuid(), user_id uuid NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE, name text NOT NULL, description text, created_at timestamp with time zone DEFAULT now(), updated_at timestamp with time zone DEFAULT now());
CREATE TABLE IF NOT EXISTS generations ( id TEXT PRIMARY KEY DEFAULT gen_random_uuid()::text, "userId" TEXT NOT NULL, type TEXT NOT NULL, topic TEXT NOT NULL, content TEXT NOT NULL, "createdAt" TIMESTAMP DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY ("userId") REFERENCES users(id) ON DELETE CASCADE);
CREATE TABLE IF NOT EXISTS credit_usage ( id TEXT PRIMARY KEY DEFAULT gen_random_uuid()::text, "userId" TEXT NOT NULL, amount INTEGER NOT NULL, type TEXT NOT NULL, description TEXT, "createdAt" TIMESTAMP DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY ("userId") REFERENCES users(id) ON DELETE CASCADE);
CREATE TABLE quiz_results ( id UUID DEFAULT gen_random_uuid() PRIMARY KEY, user_id UUID NOT NULL, quiz_item_id UUID REFERENCES study_items(id) ON DELETE SET NULL, score NUMERIC NOT NULL, total_questions INTEGER NOT NULL, correct_count INTEGER NOT NULL DEFAULT 0, partial_count INTEGER NOT NULL DEFAULT 0, incorrect_count INTEGER NOT NULL DEFAULT 0, accuracy NUMERIC NOT NULL DEFAULT 0, avg_time_per_question NUMERIC NOT NULL DEFAULT 0, best_streak INTEGER NOT NULL DEFAULT 0, answers JSONB, created_at TIMESTAMP WITHOUT TIME ZONE DEFAULT NOW(), updated_at TIMESTAMP WITHOUT TIME ZONE DEFAULT NOW());
CREATE TABLE flashcard_results ( id UUID DEFAULT gen_random_uuid() PRIMARY KEY, user_id UUID NOT NULL, flashcard_item_id UUID REFERENCES study_items(id) ON DELETE SET NULL, known_count INTEGER NOT NULL DEFAULT 0, unknown_count INTEGER NOT NULL DEFAULT 0, total_cards INTEGER NOT NULL, cards_status JSONB, created_at TIMESTAMP WITHOUT TIME ZONE DEFAULT NOW(), updated_at TIMESTAMP WITHOUT TIME ZONE DEFAULT NOW());
Indexes:
flashcard_results_user_id_idx on user_id
flashcard_results_flashcard_item_id_idx on flashcard_item_id
CREATE TABLE writing_results ( id UUID DEFAULT gen_random_uuid() PRIMARY KEY, user_id UUID NOT NULL, writing_item_id UUID REFERENCES study_items(id) ON DELETE SET NULL, overall_score NUMERIC NOT NULL DEFAULT 0, total_prompts INTEGER NOT NULL, prompt_responses JSONB, created_at TIMESTAMP WITHOUT TIME ZONE DEFAULT NOW(), updated_at TIMESTAMP WITHOUT TIME ZONE DEFAULT NOW());
Indexes:
writing_results_user_id_idx on user_id
writing_results_writing_item_id_idx on writing_item_id
CREATE TABLE IF NOT EXISTS public.ai_chat_sessions ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), user_id UUID NOT NULL, session_name TEXT NOT NULL DEFAULT 'New Study Session', created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), last_message_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), FOREIGN KEY (user_id) REFERENCES public.users(id) ON DELETE CASCADE);
CREATE TABLE IF NOT EXISTS public.ai_chat_messages ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), session_id UUID NOT NULL, role TEXT NOT NULL CHECK (role IN ('user', 'assistant', 'system')), content TEXT NOT NULL, created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), metadata JSONB DEFAULT '{}', FOREIGN KEY (session_id) REFERENCES public.ai_chat_sessions(id) ON DELETE CASCADE);
CREATE TABLE IF NOT EXISTS public.workspace_documents ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), user_id UUID NOT NULL, session_id UUID, title TEXT NOT NULL, content TEXT NOT NULL, document_type TEXT NOT NULL CHECK (document_type IN ('note', 'pdf', 'text', 'image')), created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), FOREIGN KEY (user_id) REFERENCES public.users(id) ON DELETE CASCADE, FOREIGN KEY (session_id) REFERENCES public.ai_chat_sessions(id) ON DELETE SET NULL);
CREATE TABLE IF NOT EXISTS shared_quizzes ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), quiz_id TEXT NOT NULL UNIQUE, topic TEXT NOT NULL, quiz_data JSONB NOT NULL, created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), expires_at TIMESTAMP WITH TIME ZONE, access_count INTEGER DEFAULT 0);
CREATE TABLE IF NOT EXISTS shared_sessions ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), session_id TEXT NOT NULL UNIQUE, quiz_data JSONB NOT NULL, created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), expires_at TIMESTAMP WITH TIME ZONE);
CREATE TABLE IF NOT EXISTS public.admin_config ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), config_key TEXT NOT NULL UNIQUE, config_value JSONB NOT NULL, updated_by TEXT, updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW());
CREATE TABLE IF NOT EXISTS public.admin_notifications ( id uuid PRIMARY KEY DEFAULT gen_random_uuid(), title text NOT NULL, message text NOT NULL, enabled boolean DEFAULT false, created_at timestamp with time zone DEFAULT now(), created_by text, expires_at timestamp with time zone);
CREATE TABLE IF NOT EXISTS bug_reports ( id UUID DEFAULT gen_random_uuid() PRIMARY KEY, preferred_name TEXT NOT NULL, severity TEXT NOT NULL CHECK (severity IN ('low', 'medium', 'high', 'critical')), error_code TEXT, has_error_code BOOLEAN DEFAULT true, description TEXT NOT NULL, browser_info TEXT, created_at TIMESTAMP WITH TIME ZONE DEFAULT timezone('utc'::text, now()) NOT NULL, status TEXT DEFAULT 'pending' CHECK (status IN ('pending', 'in_progress', 'resolved', 'closed')), admin_notes TEXT);
CREATE TABLE IF NOT EXISTS user_feedback ( id UUID DEFAULT gen_random_uuid() PRIMARY KEY, preferred_name TEXT NOT NULL, feedback_type TEXT NOT NULL CHECK (feedback_type IN ('general', 'feature_request', 'improvement', 'other')), message TEXT NOT NULL, rating INTEGER CHECK (rating >= 1 AND rating <= 5), created_at TIMESTAMP WITH TIME ZONE DEFAULT timezone('utc'::text, now()) NOT NULL, status TEXT DEFAULT 'pending' CHECK (status IN ('pending', 'reviewed', 'implemented', 'closed')), admin_notes TEXT);
CREATE TABLE IF NOT EXISTS notifications ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, sender_id UUID REFERENCES users(id) ON DELETE SET NULL, material_id UUID REFERENCES study_items(id) ON DELETE SET NULL, type TEXT NOT NULL CHECK (type IN ('share', 'live_invite', 'comment')), title TEXT NOT NULL, message TEXT NOT NULL, read BOOLEAN DEFAULT false, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, expires_at TIMESTAMP);
CREATE TABLE public.classrooms ( id uuid PRIMARY KEY DEFAULT gen_random_uuid(), teacher_id uuid NOT NULL, clever_section_id text NOT NULL UNIQUE, name text NOT NULL, subject text, grade text, period text, raw_clever_data jsonb, created_at timestamp with time zone DEFAULT now(), updated_at timestamp with time zone DEFAULT now(), FOREIGN KEY (teacher_id) REFERENCES public.users(id) ON DELETE CASCADE);
Indexes:
idx_classrooms_teacher_id on teacher_id
idx_classrooms_clever_section_id on clever_section_id
CREATE TABLE public.students ( id uuid PRIMARY KEY DEFAULT gen_random_uuid(), classroom_id uuid NOT NULL, clever_student_id text NOT NULL UNIQUE, first_name text, last_name text, email text, raw_clever_data jsonb, created_at timestamp with time zone DEFAULT now(), updated_at timestamp with time zone DEFAULT now(), FOREIGN KEY (classroom_id) REFERENCES public.classrooms(id) ON DELETE CASCADE);
Indexes:
idx_students_classroom_id on classroom_id
idx_students_clever_student_id on clever_student_id
CREATE TABLE IF NOT EXISTS generation_rate_limits ( id BIGSERIAL PRIMARY KEY, identifier TEXT NOT NULL, created_at TIMESTAMP WITH TIME ZONE DEFAULT now());
Indexes:
idx_rate_limit_identifier_time on (identifier, created_at DESC)