Row Level Security (RLS) is PostgreSQL’s built-in mechanism for restricting database access at the row level. Studley AI uses RLS extensively to ensure users can only access their own data while allowing admins to manage the platform.
RLS policies are the foundation of Studley AI’s security model. They ensure data isolation between users even if the application layer is compromised.
-- Enable RLSALTER TABLE public.user_profiles ENABLE ROW LEVEL SECURITY;-- Users can view their own profileCREATE POLICY "Users can view their own profile"ON public.user_profilesFOR SELECTUSING (auth.uid() = user_id);-- Users can update their own profileCREATE POLICY "Users can update their own profile"ON public.user_profilesFOR UPDATEUSING (auth.uid() = user_id);-- Users can insert their own profileCREATE POLICY "Users can insert their own profile"ON public.user_profilesFOR INSERTWITH CHECK (auth.uid() = user_id);
How it works:
auth.uid() returns the current authenticated user’s ID
Policy compares it to the user_id column in each row
Only rows where user_id matches auth.uid() are accessible
Security Note: The INSERT policy uses WITH CHECK instead of USING. This prevents users from creating profiles for other users.
ALTER TABLE public.user_folders ENABLE ROW LEVEL SECURITY;-- Users can view their own foldersCREATE POLICY "Users can view their own folders"ON public.user_foldersFOR SELECTUSING (auth.uid() = user_id);-- Users can insert their own foldersCREATE POLICY "Users can insert their own folders"ON public.user_foldersFOR INSERTWITH CHECK (auth.uid() = user_id);-- Users can update their own foldersCREATE POLICY "Users can update their own folders"ON public.user_foldersFOR UPDATEUSING (auth.uid() = user_id);-- Users can delete their own foldersCREATE POLICY "Users can delete their own folders"ON public.user_foldersFOR DELETEUSING (auth.uid() = user_id);
ALTER TABLE public.user_library_items ENABLE ROW LEVEL SECURITY;-- Users can view their own library itemsCREATE POLICY "Users can view their own library items"ON public.user_library_itemsFOR SELECTUSING (auth.uid() = user_id);-- Users can insert their own library itemsCREATE POLICY "Users can insert their own library items"ON public.user_library_itemsFOR INSERTWITH CHECK (auth.uid() = user_id);-- Users can update their own library itemsCREATE POLICY "Users can update their own library items"ON public.user_library_itemsFOR UPDATEUSING (auth.uid() = user_id);-- Users can delete their own library itemsCREATE POLICY "Users can delete their own library items"ON public.user_library_itemsFOR DELETEUSING (auth.uid() = user_id);
ALTER TABLE public.admin_notifications ENABLE ROW LEVEL SECURITY;-- Anyone can view enabled notificationsCREATE POLICY "Anyone can view enabled notifications"ON public.admin_notificationsFOR SELECTUSING (enabled = true);-- Admins can manage notificationsCREATE POLICY "Admins can manage notifications"ON public.admin_notificationsFOR ALL USING (auth.uid() IN (SELECT id FROM public.admin_users));
How admin check works:
Assumes an admin_users table exists with admin user IDs
Alternatively, check for an is_admin flag:
CREATE POLICY "Admins can manage notifications"ON public.admin_notificationsFOR ALL USING ( EXISTS ( SELECT 1 FROM user_profiles WHERE user_id = auth.uid() AND is_admin = true ));
ALTER TABLE public.admin_config ENABLE ROW LEVEL SECURITY;-- Anyone can view admin configCREATE POLICY "Anyone can view admin config"ON public.admin_configFOR SELECTUSING (true);-- Admins can manage admin configCREATE POLICY "Admins can manage admin config"ON public.admin_configFOR ALL USING (true) WITH CHECK (true);
Note: These policies use USING (true) which allows all authenticated users. In production, implement proper admin role checking.
ALTER TABLE user_feedback ENABLE ROW LEVEL SECURITY;-- Anyone can submit feedbackCREATE POLICY "Anyone can submit feedback"ON user_feedback FOR INSERTTO anon, authenticatedWITH CHECK (true);-- Admins can view all feedbackCREATE POLICY "Admins can view all feedback"ON user_feedback FOR SELECTTO authenticatedUSING (true);-- Admins can update feedbackCREATE POLICY "Admins can update feedback"ON user_feedback FOR UPDATETO authenticatedUSING (true);
ALTER TABLE shared_materials ENABLE ROW LEVEL SECURITY;-- Owner can manage their shared materialsCREATE POLICY shared_materials_owner_policy ON shared_materialsFOR ALLUSING (true)WITH CHECK (true);-- Shared user can view their shared materialsCREATE POLICY shared_materials_shared_user_policy ON shared_materialsFOR SELECTUSING (true);-- Public access (no user restriction)CREATE POLICY shared_materials_public_policy ON shared_materialsFOR SELECTUSING (public_access_enabled = true AND public_token IS NOT NULL);
The shared materials policies use USING (true) currently. In production, these should be tightened to check ownership and sharing relationships.
Recommended improvement:
-- Better owner policyCREATE POLICY shared_materials_owner_policy ON shared_materialsFOR ALLUSING (auth.uid() = owner_id)WITH CHECK (auth.uid() = owner_id);-- Better shared user policyCREATE POLICY shared_materials_shared_user_policy ON shared_materialsFOR SELECTUSING ( auth.uid() = shared_with_user_id OR auth.uid() = owner_id);
ALTER TABLE public.ai_chat_sessions ENABLE ROW LEVEL SECURITY;-- Allow all for now (should be restricted in production)CREATE POLICY "allow_all_chat_sessions"ON public.ai_chat_sessionsFOR ALLUSING (true);
Production-ready policy:
-- Users can only access their own chat sessionsCREATE POLICY "Users can manage their own chat sessions"ON public.ai_chat_sessionsFOR ALLUSING (auth.uid() = user_id)WITH CHECK (auth.uid() = user_id);
ALTER TABLE public.ai_chat_messages ENABLE ROW LEVEL SECURITY;-- Production policy: Users can only access messages from their sessionsCREATE POLICY "Users can view messages from their sessions"ON public.ai_chat_messagesFOR SELECTUSING ( EXISTS ( SELECT 1 FROM ai_chat_sessions WHERE id = session_id AND user_id = auth.uid() ));CREATE POLICY "Users can insert messages to their sessions"ON public.ai_chat_messagesFOR INSERTWITH CHECK ( EXISTS ( SELECT 1 FROM ai_chat_sessions WHERE id = session_id AND user_id = auth.uid() ));
ALTER TABLE notifications ENABLE ROW LEVEL SECURITY;-- Users can view their own notificationsCREATE POLICY notifications_user_policy ON notificationsFOR SELECTUSING (true); -- Should be: auth.uid() = user_id-- Users can update read status of their own notificationsCREATE POLICY notifications_update_policy ON notificationsFOR UPDATEUSING (true) -- Should be: auth.uid() = user_idWITH CHECK (true); -- Should be: auth.uid() = user_id-- System/API can insert notificationsCREATE POLICY notifications_insert_policy ON notificationsFOR INSERTWITH CHECK (true);-- Users can delete their own notificationsCREATE POLICY notifications_delete_policy ON notificationsFOR DELETEUSING (true); -- Should be: auth.uid() = user_id
Security Issue: The notifications policies currently use USING (true) which allows any user to access any notification. This should be fixed in production.
ALTER TABLE public.classrooms ENABLE ROW LEVEL SECURITY;-- Teachers can view and manage their own classroomsCREATE POLICY "Teachers can manage their classrooms"ON public.classroomsFOR ALLUSING (auth.uid() = teacher_id)WITH CHECK (auth.uid() = teacher_id);
ALTER TABLE public.students ENABLE ROW LEVEL SECURITY;-- Teachers can view students in their classroomsCREATE POLICY "Teachers can view their students"ON public.studentsFOR SELECTUSING ( EXISTS ( SELECT 1 FROM classrooms WHERE id = classroom_id AND teacher_id = auth.uid() ));-- Teachers can manage students in their classroomsCREATE POLICY "Teachers can manage their students"ON public.studentsFOR ALLUSING ( EXISTS ( SELECT 1 FROM classrooms WHERE id = classroom_id AND teacher_id = auth.uid() ))WITH CHECK ( EXISTS ( SELECT 1 FROM classrooms WHERE id = classroom_id AND teacher_id = auth.uid() ));
-- Drop a policyDROP POLICY IF EXISTS "policy_name" ON table_name;-- Recreate with new rulesCREATE POLICY "policy_name" ON table_name FOR SELECT USING (new_condition);
-- Disable RLS on a table (use with caution!)ALTER TABLE table_name DISABLE ROW LEVEL SECURITY;-- Force RLS even for table ownerALTER TABLE table_name FORCE ROW LEVEL SECURITY;
Never disable RLS in production without understanding the security implications. It exposes all data to all users.
-- Set session to specific userSET LOCAL ROLE authenticated;SET LOCAL request.jwt.claims = '{"sub": "user-uuid-here"}';-- Run test querySELECT * FROM user_library_items;-- ResetRESET ROLE;
-- Create test usersINSERT INTO auth.users (id, email) VALUES ('user1-uuid', '[email protected]'), ('user2-uuid', '[email protected]');-- Insert test dataINSERT INTO user_library_items (user_id, title, item_type, content) VALUES ('user1-uuid', 'Test Item 1', 'quiz', '{}'), ('user2-uuid', 'Test Item 2', 'quiz', '{}');-- Test as user1 (should only see their item)SET LOCAL request.jwt.claims = '{"sub": "user1-uuid"}';SELECT * FROM user_library_items; -- Should return only 1 rowRESET ROLE;