The User Management system provides administrators with tools to monitor user accounts, track activity, manage credits, and view detailed usage analytics across the Studley AI platform.
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);
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));
Studley AI uses a hybrid authentication system with both a custom users table and Supabase auth.users. The user_profiles table links to Supabase auth.
CREATE TABLE IF NOT EXISTS generations ( id TEXT PRIMARY KEY DEFAULT gen_random_uuid()::text, "userId" TEXT NOT NULL, type TEXT NOT NULL, -- 'quiz', 'flashcard', 'study_guide', etc. topic TEXT NOT NULL, content TEXT NOT NULL, "createdAt" TIMESTAMP DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY ("userId") REFERENCES users(id) ON DELETE CASCADE);
Query user generations:
-- Get all generations by a specific userSELECT type, topic, "createdAt"FROM generations WHERE "userId" = 'user-id-here'ORDER BY "createdAt" DESC;-- Count generations by typeSELECT type, COUNT(*) as count FROM generations WHERE "userId" = 'user-id-here'GROUP BY type;
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());
Query user library statistics:
-- Get user's library summarySELECT item_type, COUNT(*) as total_items, MAX(created_at) as last_createdFROM user_library_itemsWHERE user_id = 'user-id-here'GROUP BY item_type;
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, -- 'deduction', 'purchase', 'bonus', 'refund' description TEXT, "createdAt" TIMESTAMP DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY ("userId") REFERENCES users(id) ON DELETE CASCADE);
-- Get user's current credit balanceSELECT credits FROM users WHERE id = 'user-id-here';-- Get credit usage historySELECT amount, type, description, "createdAt"FROM credit_usageWHERE "userId" = 'user-id-here'ORDER BY "createdAt" DESCLIMIT 50;
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);
View active sessions:
-- Get all active sessionsSELECT s."userId", u.email, u.name, s.expires, s."createdAt"FROM sessions sJOIN users u ON s."userId" = u.idWHERE s.expires > CURRENT_TIMESTAMPORDER BY s."createdAt" DESC;
Revoke user session:
-- Force logout by deleting sessionDELETE FROM sessions WHERE "userId" = 'user-id-here';
-- Top 10 users by generation countSELECT u.id, u.email, u.name, COUNT(g.id) as total_generationsFROM users uLEFT JOIN generations g ON u.id = g."userId"GROUP BY u.id, u.email, u.nameORDER BY total_generations DESCLIMIT 10;
-- Users who signed up in the last 7 daysSELECT id, email, name, credits, "createdAt"FROM usersWHERE "createdAt" > CURRENT_TIMESTAMP - INTERVAL '7 days'ORDER BY "createdAt" DESC;
-- Users who haven't generated content in 30 daysSELECT u.id, u.email, u.name, MAX(g."createdAt") as last_generationFROM users uLEFT JOIN generations g ON u.id = g."userId"GROUP BY u.id, u.email, u.nameHAVING MAX(g."createdAt") < CURRENT_TIMESTAMP - INTERVAL '30 days' OR MAX(g."createdAt") IS NULL;
-- Credit balance distributionSELECT CASE WHEN credits = 0 THEN 'Empty' WHEN credits < 1000 THEN 'Low (1-999)' WHEN credits < 5000 THEN 'Medium (1K-5K)' WHEN credits < 10000 THEN 'High (5K-10K)' ELSE 'Very High (10K+)' END as credit_range, COUNT(*) as user_countFROM usersGROUP BY credit_rangeORDER BY MIN(credits);
-- Get complete user informationSELECT u.id, u.email, u.name, u.credits, u."emailVerified", u."createdAt", COUNT(DISTINCT g.id) as total_generations, COUNT(DISTINCT l.id) as library_items, COUNT(DISTINCT f.id) as foldersFROM users uLEFT JOIN generations g ON u.id = g."userId"LEFT JOIN user_library_items l ON u.id::uuid = l.user_idLEFT JOIN user_folders f ON u.id::uuid = f.user_idWHERE u.email = '[email protected]'GROUP BY u.id, u.email, u.name, u.credits, u."emailVerified", u."createdAt";
CREATE TABLE clever_accounts ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), user_id UUID NOT NULL, clever_id TEXT UNIQUE NOT NULL, district_id TEXT, school_id TEXT, role TEXT NOT NULL, -- 'teacher', 'student' raw_data JSONB, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (user_id) REFERENCES users(id));
Query Clever users:
-- Get all Clever-authenticated usersSELECT u.email, u.name, c.role, c.district_id, c.school_idFROM clever_accounts cJOIN users u ON c.user_id = u.idORDER BY c.created_at DESC;
-- Delete user and all associated data-- Cascades automatically to:-- - sessions-- - credit_usage -- - generations-- - user_library_items-- - user_folders-- - accountsDELETE FROM users WHERE id = 'user-id-here';
Consider implementing a soft delete by adding an active column:
-- Add active column (one-time migration)ALTER TABLE users ADD COLUMN active BOOLEAN DEFAULT true;-- Disable account instead of deletingUPDATE users SET active = false WHERE id = 'user-id-here';-- Modify queries to exclude inactive usersSELECT * FROM users WHERE active = true;
-- Check which users have verified emailsSELECT email, name, "emailVerified" IS NOT NULL as is_verified, "emailVerified" as verified_atFROM usersORDER BY "createdAt" DESC;