CREATE TABLE public.your_table ( -- Primary key id uuid PRIMARY KEY DEFAULT gen_random_uuid(), -- Your columns here name text NOT NULL, description text, -- Audit fields created_at timestamptz DEFAULT now(), updated_at timestamptz DEFAULT now(), -- Soft delete (optional) deleted_at timestamptz);-- Enable RLSALTER TABLE public.your_table ENABLE ROW LEVEL SECURITY;-- Add updated_at triggerCREATE TRIGGER update_your_table_updated_at BEFORE UPDATE ON public.your_table FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();-- Add indexes for foreign keys and common queriesCREATE INDEX idx_your_table_created_at ON public.your_table(created_at DESC);
-- Anyone can read (careful with this!)CREATE POLICY "Public read access" ON public.schools FOR SELECT USING (true);-- Only non-deleted recordsCREATE POLICY "Public read non-deleted" ON public.schools FOR SELECT USING (deleted_at IS NULL);
Always test your policies in the Supabase dashboard:
1
Navigate to Auth Policies
Go to Database → Tables → Select table → Policies
2
Use Policy Editor
Click “New policy” and use the template editor
3
Test with Different Users
Use “Test policy” with different user contexts
4
Check SQL Directly
Copy
-- Test as anonymousSET request.jwt.claim.sub TO NULL;SELECT * FROM public.your_table;-- Test as specific userSET request.jwt.claim.sub TO 'user-uuid-here';SELECT * FROM public.your_table;
-- Update timestamp trigger functionCREATE OR REPLACE FUNCTION update_updated_at_column()RETURNS TRIGGER AS $$BEGIN NEW.updated_at = now(); RETURN NEW;END;$$ LANGUAGE plpgsql;-- Soft delete functionCREATE OR REPLACE FUNCTION soft_delete()RETURNS TRIGGER AS $$BEGIN NEW.deleted_at = now(); RETURN NEW;END;$$ LANGUAGE plpgsql;-- Generate slug from nameCREATE OR REPLACE FUNCTION generate_slug(name text)RETURNS text AS $$BEGIN RETURN lower( regexp_replace( regexp_replace(name, '[^a-zA-Z0-9\s-]', '', 'g'), '\s+', '-', 'g' ) );END;$$ LANGUAGE plpgsql;
-- Calculate match percentageCREATE OR REPLACE FUNCTION calculate_match_score( p_student_id uuid, p_school_id uuid) RETURNS integer AS $$DECLARE v_score integer := 0; v_student record; v_school record;BEGIN -- Get student and school data SELECT * INTO v_student FROM public.students WHERE id = p_student_id; SELECT * INTO v_school FROM public.schools WHERE id = p_school_id; -- GPA match (25 points) IF v_student.gpa >= v_school.avg_gpa - 0.5 THEN v_score := v_score + 25; END IF; -- Size preference (25 points) IF v_student.preferred_size = CASE WHEN v_school.size < 5000 THEN 'Small' WHEN v_school.size < 15000 THEN 'Medium' ELSE 'Large' END THEN v_score := v_score + 25; END IF; -- Add more matching logic... RETURN v_score;END;$$ LANGUAGE plpgsql;-- Get conversation participantsCREATE OR REPLACE FUNCTION get_conversation_participants( p_conversation_id uuid) RETURNS TABLE ( participant_id uuid, participant_type text, participant_name text) AS $$BEGIN RETURN QUERY SELECT COALESCE(cp.student_id, cp.partner_user_id) as participant_id, cp.role as participant_type, CASE WHEN cp.student_id IS NOT NULL THEN s.name WHEN cp.partner_user_id IS NOT NULL THEN pu.full_name END as participant_name FROM public.conversation_participants cp LEFT JOIN public.students s ON cp.student_id = s.id LEFT JOIN public.partner_users pu ON cp.partner_user_id = pu.id WHERE cp.conversation_id = p_conversation_id;END;$$ LANGUAGE plpgsql;
-- Automatic updated_atCREATE TRIGGER update_students_updated_at BEFORE UPDATE ON public.students FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();-- Cascade soft deletesCREATE TRIGGER cascade_soft_delete_messages AFTER UPDATE OF deleted_at ON public.conversations FOR EACH ROW WHEN (NEW.deleted_at IS NOT NULL AND OLD.deleted_at IS NULL) EXECUTE FUNCTION soft_delete_related_messages();-- Maintain denormalized countsCREATE TRIGGER update_conversation_message_count AFTER INSERT OR DELETE ON public.messages FOR EACH ROW EXECUTE FUNCTION update_message_count();-- Audit trailCREATE TRIGGER audit_student_changes AFTER INSERT OR UPDATE OR DELETE ON public.students FOR EACH ROW EXECUTE FUNCTION create_audit_entry();
-- Soft delete related recordsCREATE OR REPLACE FUNCTION soft_delete_related_messages()RETURNS TRIGGER AS $$BEGIN UPDATE public.messages SET deleted_at = NEW.deleted_at WHERE conversation_id = NEW.id AND deleted_at IS NULL; RETURN NEW;END;$$ LANGUAGE plpgsql;-- Update denormalized countCREATE OR REPLACE FUNCTION update_message_count()RETURNS TRIGGER AS $$BEGIN IF TG_OP = 'INSERT' THEN UPDATE public.conversations SET message_count = message_count + 1, last_message_at = NEW.created_at WHERE id = NEW.conversation_id; ELSIF TG_OP = 'DELETE' THEN UPDATE public.conversations SET message_count = message_count - 1 WHERE id = OLD.conversation_id; END IF; RETURN NULL;END;$$ LANGUAGE plpgsql;
-- Student dashboard viewCREATE OR REPLACE VIEW public.student_dashboard ASSELECT s.id, s.name, s.email, COUNT(DISTINCT ssi.school_id) as schools_viewed, COUNT(DISTINCT CASE WHEN ssi.liked = true THEN ssi.school_id END) as schools_liked, COUNT(DISTINCT c.id) as active_conversations, COUNT(DISTINCT sch.id) as scholarships_savedFROM public.students sLEFT JOIN public.student_school_interactions ssi ON s.id = ssi.student_idLEFT JOIN public.conversations c ON s.id = c.initiator_id AND c.initiator_type = 'student'LEFT JOIN public.student_scholarship_interactions sch ON s.id = sch.student_id AND sch.saved = trueGROUP BY s.id, s.name, s.email;-- Grant appropriate accessGRANT SELECT ON public.student_dashboard TO authenticated;
-- School statistics that update periodicallyCREATE MATERIALIZED VIEW public.school_stats ASSELECT sch.id, sch.name, COUNT(DISTINCT ssi.student_id) as total_views, COUNT(DISTINCT CASE WHEN ssi.liked = true THEN ssi.student_id END) as total_likes, AVG(CASE WHEN ssi.match_score IS NOT NULL THEN ssi.match_score::numeric END) as avg_match_score, COUNT(DISTINCT m.id) as total_messagesFROM public.schools schLEFT JOIN public.student_school_interactions ssi ON sch.id = ssi.school_idLEFT JOIN public.partner_entities pe ON sch.id = pe.school_idLEFT JOIN public.conversations c ON pe.id = c.partner_entity_idLEFT JOIN public.messages m ON c.id = m.conversation_idGROUP BY sch.id, sch.name;-- Create index for fast lookupsCREATE INDEX idx_school_stats_id ON public.school_stats(id);-- Refresh periodically (in a scheduled function or cron job)REFRESH MATERIALIZED VIEW CONCURRENTLY public.school_stats;
-- Use EXPLAIN ANALYZE to understand query plansEXPLAIN ANALYZESELECT * FROM public.studentsWHERE graduation_year = 2024 AND gpa > 3.5;-- Create covering indexes for common queriesCREATE INDEX idx_students_searchON public.students(graduation_year, gpa) INCLUDE (name, email);-- Use proper JOIN order (smallest dataset first)SELECT * FROM public.messages mJOIN public.conversations c ON m.conversation_id = c.id -- messages first (larger)JOIN public.students s ON c.initiator_id = s.id -- then students (smaller)WHERE s.id = 'specific-id';
CREATE TABLE IF NOT EXISTS public.table_name (...);
Cannot drop table due to dependencies
Use CASCADE carefully:
Copy
-- See what depends on the tableSELECT * FROM pg_depend WHERE refobjid = 'public.table_name'::regclass;-- Drop with cascade (careful!)DROP TABLE public.table_name CASCADE;
RLS blocking all access
Check if RLS is enabled but no policies exist:
Copy
-- Check RLS statusSELECT tablename, rowsecurity FROM pg_tables WHERE schemaname = 'public';-- Temporarily disable for debuggingALTER TABLE public.table_name DISABLE ROW LEVEL SECURITY;
Function not working as expected
Debug with RAISE NOTICE:
Copy
CREATE OR REPLACE FUNCTION debug_function()RETURNS void AS $$DECLARE v_test text;BEGIN v_test := 'Hello'; RAISE NOTICE 'Value of v_test: %', v_test;END;$$ LANGUAGE plpgsql;