+
+
+ Eligibility, goals, meeting notes and documents are restricted to the case manager,
+ administrators, and the student's parents. You can see the accommodations and services
+ you are responsible for implementing.
+
+
+ );
+}
diff --git a/src/routes/_authenticated/route.tsx b/src/routes/_authenticated/route.tsx
index 6e425a9..34f9e5a 100644
--- a/src/routes/_authenticated/route.tsx
+++ b/src/routes/_authenticated/route.tsx
@@ -4,7 +4,7 @@ import { supabase } from "@/integrations/supabase/client";
import { Button } from "@/components/ui/button";
import {
GraduationCap, LayoutDashboard, Users, ClipboardCheck, Receipt,
- MessageSquare, FileText, CalendarDays, Settings, LogOut, Loader2, BookOpen
+ MessageSquare, FileText, CalendarDays, Settings, LogOut, Loader2, BookOpen, ClipboardList
} from "lucide-react";
import { useEffect } from "react";
@@ -34,6 +34,7 @@ function ProtectedLayout() {
{ to: "/students", label: "Students", icon: Users, show: true },
{ to: "/classes", label: "Classes", icon: BookOpen, show: isAdmin || roles.includes("teacher") },
{ to: "/attendance", label: "Attendance", icon: ClipboardCheck, show: isAdmin || roles.includes("teacher") },
+ { to: "/plans", label: "504 / IEP", icon: ClipboardList, show: isAdmin || roles.includes("teacher") },
{ to: "/ledger", label: "Tuition", icon: Receipt, show: true },
{ to: "/messages", label: "Messages", icon: MessageSquare, show: true },
{ to: "/forms", label: "Forms", icon: FileText, show: true },
diff --git a/src/routes/_authenticated/students.$id.tsx b/src/routes/_authenticated/students.$id.tsx
index a421322..3d56d4f 100644
--- a/src/routes/_authenticated/students.$id.tsx
+++ b/src/routes/_authenticated/students.$id.tsx
@@ -17,6 +17,7 @@ import { createUserFn } from "@/lib/user-admin.functions";
import { createIntakeToken } from "@/lib/intake.functions";
import { genTempPassword } from "@/lib/temp-password";
import { StudentGradeReport } from "@/components/gradebook";
+import { StudentPlansTab } from "@/components/plans";
export const Route = createFileRoute("/_authenticated/students/$id")({
head: () => ({ meta: [{ title: "Student — School Portal" }] }),
@@ -90,6 +91,7 @@ function StudentDetail() {
ProfileFamily & pickupAcademics
+ 504 / IEPGradesAttendanceTuition
@@ -98,6 +100,7 @@ function StudentDetail() {
+
diff --git a/supabase/migrations/20260725120000_student_plans_504_iep.sql b/supabase/migrations/20260725120000_student_plans_504_iep.sql
new file mode 100644
index 0000000..f35967a
--- /dev/null
+++ b/supabase/migrations/20260725120000_student_plans_504_iep.sql
@@ -0,0 +1,365 @@
+-- 504 / IEP plans.
+--
+-- Confidentiality is tiered, because special-education records are the most
+-- sensitive data in the portal and FERPA access is need-to-know:
+--
+-- FULL tier — admin, the plan's case manager, and the student's parents.
+-- Sees everything: eligibility/disability category, evaluations,
+-- goals and progress data, meeting notes, signed documents.
+--
+-- IMPL tier — the above, PLUS any teacher of the student's class.
+-- Sees only what they must implement: that a plan exists, its
+-- type/status/dates, the accommodations, and the service
+-- schedule. Never the disability category or meeting notes.
+--
+-- The split is physical, not cosmetic: RLS is row-level, and every role here is
+-- the same Postgres role (`authenticated`), so column grants cannot separate
+-- these tiers. Confidential fields therefore live in their own tables
+-- (plan_details, plan_goals, plan_meetings, plan_documents) rather than as
+-- columns on student_plans.
+
+-- ── Enums ───────────────────────────────────────────────────────────────────
+DO $$ BEGIN CREATE TYPE public.plan_type AS ENUM ('iep', 'section_504');
+EXCEPTION WHEN duplicate_object THEN NULL; END $$;
+
+DO $$ BEGIN CREATE TYPE public.plan_status AS ENUM ('draft', 'active', 'archived');
+EXCEPTION WHEN duplicate_object THEN NULL; END $$;
+
+DO $$ BEGIN CREATE TYPE public.plan_goal_status AS ENUM ('not_started', 'in_progress', 'met', 'not_met', 'discontinued');
+EXCEPTION WHEN duplicate_object THEN NULL; END $$;
+
+DO $$ BEGIN CREATE TYPE public.plan_meeting_type AS ENUM ('initial', 'annual_review', 'reevaluation', 'eligibility', 'amendment', 'other');
+EXCEPTION WHEN duplicate_object THEN NULL; END $$;
+
+-- ── Core plan (IMPL tier) ───────────────────────────────────────────────────
+CREATE TABLE IF NOT EXISTS public.student_plans (
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
+ student_id UUID NOT NULL REFERENCES public.students(id) ON DELETE CASCADE,
+ plan_type plan_type NOT NULL,
+ status plan_status NOT NULL DEFAULT 'draft',
+ case_manager_id UUID REFERENCES auth.users(id) ON DELETE SET NULL,
+ effective_date DATE,
+ end_date DATE,
+ -- Compliance clocks. Annual review = yearly; reevaluation = triennial under
+ -- IDEA. Stored explicitly rather than derived, because districts amend them.
+ next_annual_review_date DATE,
+ next_reevaluation_date DATE,
+ created_by UUID REFERENCES auth.users(id),
+ created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
+ updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
+);
+CREATE INDEX IF NOT EXISTS idx_student_plans_student ON public.student_plans(student_id);
+CREATE INDEX IF NOT EXISTS idx_student_plans_case_manager ON public.student_plans(case_manager_id);
+CREATE INDEX IF NOT EXISTS idx_student_plans_review ON public.student_plans(next_annual_review_date);
+CREATE INDEX IF NOT EXISTS idx_student_plans_reeval ON public.student_plans(next_reevaluation_date);
+-- A student may hold an IEP and a 504 historically, but only one active of each.
+CREATE UNIQUE INDEX IF NOT EXISTS idx_student_plans_one_active
+ ON public.student_plans(student_id, plan_type) WHERE status = 'active';
+GRANT SELECT, INSERT, UPDATE, DELETE ON public.student_plans TO authenticated;
+GRANT ALL ON public.student_plans TO service_role;
+ALTER TABLE public.student_plans ENABLE ROW LEVEL SECURITY;
+
+-- ── Access predicates ───────────────────────────────────────────────────────
+-- SECURITY DEFINER so they can read student_plans without re-entering its RLS.
+CREATE OR REPLACE FUNCTION public.can_view_plan_full(_plan UUID)
+RETURNS BOOLEAN LANGUAGE SQL STABLE SECURITY DEFINER SET search_path = public AS $$
+ SELECT EXISTS (
+ SELECT 1 FROM public.student_plans p
+ WHERE p.id = _plan AND (
+ public.current_user_has_role('admin')
+ OR p.case_manager_id = auth.uid()
+ OR public.is_parent_of(p.student_id)
+ )
+ )
+$$;
+
+CREATE OR REPLACE FUNCTION public.can_view_plan_impl(_plan UUID)
+RETURNS BOOLEAN LANGUAGE SQL STABLE SECURITY DEFINER SET search_path = public AS $$
+ SELECT EXISTS (
+ SELECT 1 FROM public.student_plans p
+ WHERE p.id = _plan AND (
+ public.current_user_has_role('admin')
+ OR p.case_manager_id = auth.uid()
+ OR public.is_parent_of(p.student_id)
+ OR public.teaches_student(p.student_id)
+ )
+ )
+$$;
+
+-- Authoring a plan is admin or case manager only. Parents read, never write.
+CREATE OR REPLACE FUNCTION public.can_edit_plan(_plan UUID)
+RETURNS BOOLEAN LANGUAGE SQL STABLE SECURITY DEFINER SET search_path = public AS $$
+ SELECT EXISTS (
+ SELECT 1 FROM public.student_plans p
+ WHERE p.id = _plan AND (
+ public.current_user_has_role('admin') OR p.case_manager_id = auth.uid()
+ )
+ )
+$$;
+
+-- Predicates for goals and meetings are defined after those tables exist —
+-- LANGUAGE SQL bodies are parsed at CREATE time, so they cannot forward-reference.
+
+-- ── Confidential detail (FULL tier), 1:1 with the plan ──────────────────────
+CREATE TABLE IF NOT EXISTS public.plan_details (
+ plan_id UUID PRIMARY KEY REFERENCES public.student_plans(id) ON DELETE CASCADE,
+ eligibility_category TEXT, -- IDEA disability category, or the 504 impairment
+ eligibility_notes TEXT,
+ last_evaluation_date DATE,
+ consent_date DATE,
+ evaluation_summary TEXT,
+ student_strengths TEXT,
+ parent_concerns TEXT,
+ confidential_notes TEXT,
+ updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
+);
+GRANT SELECT, INSERT, UPDATE, DELETE ON public.plan_details TO authenticated;
+GRANT ALL ON public.plan_details TO service_role;
+ALTER TABLE public.plan_details ENABLE ROW LEVEL SECURITY;
+
+-- ── Accommodations (IMPL tier) ──────────────────────────────────────────────
+CREATE TABLE IF NOT EXISTS public.plan_accommodations (
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
+ plan_id UUID NOT NULL REFERENCES public.student_plans(id) ON DELETE CASCADE,
+ category TEXT NOT NULL DEFAULT 'instructional', -- instructional | environmental | assessment | behavioral | other
+ description TEXT NOT NULL,
+ setting TEXT,
+ sort_order INT NOT NULL DEFAULT 0,
+ created_at TIMESTAMPTZ NOT NULL DEFAULT now()
+);
+CREATE INDEX IF NOT EXISTS idx_plan_accommodations_plan ON public.plan_accommodations(plan_id);
+GRANT SELECT, INSERT, UPDATE, DELETE ON public.plan_accommodations TO authenticated;
+GRANT ALL ON public.plan_accommodations TO service_role;
+ALTER TABLE public.plan_accommodations ENABLE ROW LEVEL SECURITY;
+
+-- ── Related services (IMPL tier — teachers need the pull-out schedule) ──────
+CREATE TABLE IF NOT EXISTS public.plan_services (
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
+ plan_id UUID NOT NULL REFERENCES public.student_plans(id) ON DELETE CASCADE,
+ service_type TEXT NOT NULL, -- speech, OT, PT, counseling, …
+ provider_name TEXT,
+ provider_id UUID REFERENCES auth.users(id) ON DELETE SET NULL,
+ minutes_per_session INT,
+ sessions_per_period INT,
+ period TEXT NOT NULL DEFAULT 'week', -- week | month
+ location TEXT,
+ delivery TEXT, -- push_in | pull_out
+ start_date DATE,
+ end_date DATE,
+ created_at TIMESTAMPTZ NOT NULL DEFAULT now()
+);
+CREATE INDEX IF NOT EXISTS idx_plan_services_plan ON public.plan_services(plan_id);
+GRANT SELECT, INSERT, UPDATE, DELETE ON public.plan_services TO authenticated;
+GRANT ALL ON public.plan_services TO service_role;
+ALTER TABLE public.plan_services ENABLE ROW LEVEL SECURITY;
+
+-- ── Goals + progress monitoring (FULL tier) ─────────────────────────────────
+CREATE TABLE IF NOT EXISTS public.plan_goals (
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
+ plan_id UUID NOT NULL REFERENCES public.student_plans(id) ON DELETE CASCADE,
+ area TEXT, -- reading, math, behavior, speech, …
+ statement TEXT NOT NULL,
+ baseline TEXT,
+ target_criteria TEXT,
+ target_value NUMERIC, -- optional numeric target for charting
+ unit TEXT, -- %, wpm, trials, …
+ target_date DATE,
+ status plan_goal_status NOT NULL DEFAULT 'not_started',
+ sort_order INT NOT NULL DEFAULT 0,
+ created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
+ updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
+);
+CREATE INDEX IF NOT EXISTS idx_plan_goals_plan ON public.plan_goals(plan_id);
+GRANT SELECT, INSERT, UPDATE, DELETE ON public.plan_goals TO authenticated;
+GRANT ALL ON public.plan_goals TO service_role;
+ALTER TABLE public.plan_goals ENABLE ROW LEVEL SECURITY;
+
+CREATE TABLE IF NOT EXISTS public.plan_goal_progress (
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
+ goal_id UUID NOT NULL REFERENCES public.plan_goals(id) ON DELETE CASCADE,
+ date DATE NOT NULL DEFAULT CURRENT_DATE,
+ value NUMERIC,
+ narrative TEXT,
+ recorded_by UUID REFERENCES auth.users(id),
+ created_at TIMESTAMPTZ NOT NULL DEFAULT now()
+);
+CREATE INDEX IF NOT EXISTS idx_plan_goal_progress_goal ON public.plan_goal_progress(goal_id, date);
+GRANT SELECT, INSERT, UPDATE, DELETE ON public.plan_goal_progress TO authenticated;
+GRANT ALL ON public.plan_goal_progress TO service_role;
+ALTER TABLE public.plan_goal_progress ENABLE ROW LEVEL SECURITY;
+
+-- ── Meetings + team (FULL tier) ─────────────────────────────────────────────
+CREATE TABLE IF NOT EXISTS public.plan_meetings (
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
+ plan_id UUID NOT NULL REFERENCES public.student_plans(id) ON DELETE CASCADE,
+ meeting_type plan_meeting_type NOT NULL DEFAULT 'annual_review',
+ meeting_date DATE NOT NULL,
+ location TEXT,
+ notes TEXT,
+ outcome TEXT,
+ created_by UUID REFERENCES auth.users(id),
+ created_at TIMESTAMPTZ NOT NULL DEFAULT now()
+);
+CREATE INDEX IF NOT EXISTS idx_plan_meetings_plan ON public.plan_meetings(plan_id, meeting_date);
+GRANT SELECT, INSERT, UPDATE, DELETE ON public.plan_meetings TO authenticated;
+GRANT ALL ON public.plan_meetings TO service_role;
+ALTER TABLE public.plan_meetings ENABLE ROW LEVEL SECURITY;
+
+CREATE TABLE IF NOT EXISTS public.plan_meeting_attendees (
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
+ meeting_id UUID NOT NULL REFERENCES public.plan_meetings(id) ON DELETE CASCADE,
+ user_id UUID REFERENCES auth.users(id) ON DELETE SET NULL,
+ name TEXT NOT NULL,
+ team_role TEXT, -- parent, gen-ed teacher, sped teacher, LEA rep, …
+ attended BOOLEAN NOT NULL DEFAULT TRUE,
+ created_at TIMESTAMPTZ NOT NULL DEFAULT now()
+);
+CREATE INDEX IF NOT EXISTS idx_plan_meeting_attendees_meeting ON public.plan_meeting_attendees(meeting_id);
+GRANT SELECT, INSERT, UPDATE, DELETE ON public.plan_meeting_attendees TO authenticated;
+GRANT ALL ON public.plan_meeting_attendees TO service_role;
+ALTER TABLE public.plan_meeting_attendees ENABLE ROW LEVEL SECURITY;
+
+-- ── Signed documents (FULL tier) ────────────────────────────────────────────
+CREATE TABLE IF NOT EXISTS public.plan_documents (
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
+ plan_id UUID NOT NULL REFERENCES public.student_plans(id) ON DELETE CASCADE,
+ file_path TEXT NOT NULL,
+ title TEXT,
+ uploaded_by UUID REFERENCES auth.users(id),
+ created_at TIMESTAMPTZ NOT NULL DEFAULT now()
+);
+CREATE INDEX IF NOT EXISTS idx_plan_documents_plan ON public.plan_documents(plan_id);
+GRANT SELECT, INSERT, UPDATE, DELETE ON public.plan_documents TO authenticated;
+GRANT ALL ON public.plan_documents TO service_role;
+ALTER TABLE public.plan_documents ENABLE ROW LEVEL SECURITY;
+
+-- ── Goal / meeting predicates (tables now exist) ────────────────────────────
+CREATE OR REPLACE FUNCTION public.can_edit_goal(_goal UUID)
+RETURNS BOOLEAN LANGUAGE SQL STABLE SECURITY DEFINER SET search_path = public AS $$
+ SELECT EXISTS (SELECT 1 FROM public.plan_goals g WHERE g.id = _goal AND public.can_edit_plan(g.plan_id))
+$$;
+
+CREATE OR REPLACE FUNCTION public.can_view_goal(_goal UUID)
+RETURNS BOOLEAN LANGUAGE SQL STABLE SECURITY DEFINER SET search_path = public AS $$
+ SELECT EXISTS (SELECT 1 FROM public.plan_goals g WHERE g.id = _goal AND public.can_view_plan_full(g.plan_id))
+$$;
+
+CREATE OR REPLACE FUNCTION public.can_edit_meeting(_meeting UUID)
+RETURNS BOOLEAN LANGUAGE SQL STABLE SECURITY DEFINER SET search_path = public AS $$
+ SELECT EXISTS (SELECT 1 FROM public.plan_meetings m WHERE m.id = _meeting AND public.can_edit_plan(m.plan_id))
+$$;
+
+CREATE OR REPLACE FUNCTION public.can_view_meeting(_meeting UUID)
+RETURNS BOOLEAN LANGUAGE SQL STABLE SECURITY DEFINER SET search_path = public AS $$
+ SELECT EXISTS (SELECT 1 FROM public.plan_meetings m WHERE m.id = _meeting AND public.can_view_plan_full(m.plan_id))
+$$;
+
+-- ── Policies ────────────────────────────────────────────────────────────────
+
+-- student_plans: IMPL tier reads. Admin manages; case manager updates own.
+CREATE POLICY "plans read" ON public.student_plans FOR SELECT TO authenticated
+ USING (
+ public.current_user_has_role('admin')
+ OR case_manager_id = auth.uid()
+ OR public.is_parent_of(student_id)
+ OR public.teaches_student(student_id)
+ );
+CREATE POLICY "plans admin manage" ON public.student_plans FOR ALL TO authenticated
+ USING (public.current_user_has_role('admin')) WITH CHECK (public.current_user_has_role('admin'));
+CREATE POLICY "plans case manager update" ON public.student_plans FOR UPDATE TO authenticated
+ USING (case_manager_id = auth.uid()) WITH CHECK (case_manager_id = auth.uid());
+
+-- plan_details: FULL tier only.
+CREATE POLICY "plan_details read" ON public.plan_details FOR SELECT TO authenticated
+ USING (public.can_view_plan_full(plan_id));
+CREATE POLICY "plan_details write" ON public.plan_details FOR ALL TO authenticated
+ USING (public.can_edit_plan(plan_id)) WITH CHECK (public.can_edit_plan(plan_id));
+
+-- plan_accommodations: IMPL tier read, author write.
+CREATE POLICY "plan_accommodations read" ON public.plan_accommodations FOR SELECT TO authenticated
+ USING (public.can_view_plan_impl(plan_id));
+CREATE POLICY "plan_accommodations write" ON public.plan_accommodations FOR ALL TO authenticated
+ USING (public.can_edit_plan(plan_id)) WITH CHECK (public.can_edit_plan(plan_id));
+
+-- plan_services: IMPL tier read, author write.
+CREATE POLICY "plan_services read" ON public.plan_services FOR SELECT TO authenticated
+ USING (public.can_view_plan_impl(plan_id));
+CREATE POLICY "plan_services write" ON public.plan_services FOR ALL TO authenticated
+ USING (public.can_edit_plan(plan_id)) WITH CHECK (public.can_edit_plan(plan_id));
+
+-- plan_goals + progress: FULL tier only.
+CREATE POLICY "plan_goals read" ON public.plan_goals FOR SELECT TO authenticated
+ USING (public.can_view_plan_full(plan_id));
+CREATE POLICY "plan_goals write" ON public.plan_goals FOR ALL TO authenticated
+ USING (public.can_edit_plan(plan_id)) WITH CHECK (public.can_edit_plan(plan_id));
+
+CREATE POLICY "plan_goal_progress read" ON public.plan_goal_progress FOR SELECT TO authenticated
+ USING (public.can_view_goal(goal_id));
+CREATE POLICY "plan_goal_progress write" ON public.plan_goal_progress FOR ALL TO authenticated
+ USING (public.can_edit_goal(goal_id)) WITH CHECK (public.can_edit_goal(goal_id));
+
+-- plan_meetings + attendees: FULL tier only.
+CREATE POLICY "plan_meetings read" ON public.plan_meetings FOR SELECT TO authenticated
+ USING (public.can_view_plan_full(plan_id));
+CREATE POLICY "plan_meetings write" ON public.plan_meetings FOR ALL TO authenticated
+ USING (public.can_edit_plan(plan_id)) WITH CHECK (public.can_edit_plan(plan_id));
+
+CREATE POLICY "plan_meeting_attendees read" ON public.plan_meeting_attendees FOR SELECT TO authenticated
+ USING (public.can_view_meeting(meeting_id));
+CREATE POLICY "plan_meeting_attendees write" ON public.plan_meeting_attendees FOR ALL TO authenticated
+ USING (public.can_edit_meeting(meeting_id)) WITH CHECK (public.can_edit_meeting(meeting_id));
+
+-- plan_documents: FULL tier only.
+CREATE POLICY "plan_documents read" ON public.plan_documents FOR SELECT TO authenticated
+ USING (public.can_view_plan_full(plan_id));
+CREATE POLICY "plan_documents write" ON public.plan_documents FOR ALL TO authenticated
+ USING (public.can_edit_plan(plan_id)) WITH CHECK (public.can_edit_plan(plan_id));
+
+-- ── Storage: signed plan documents, FULL tier ───────────────────────────────
+-- Path convention "/", so folder[1] is the plan id.
+INSERT INTO storage.buckets (id, name, public)
+VALUES ('plan-documents', 'plan-documents', false)
+ON CONFLICT (id) DO NOTHING;
+
+CREATE POLICY "plan docs read" ON storage.objects FOR SELECT TO authenticated
+ USING (bucket_id = 'plan-documents' AND public.can_view_plan_full(((storage.foldername(name))[1])::uuid));
+CREATE POLICY "plan docs write" ON storage.objects FOR INSERT TO authenticated
+ WITH CHECK (bucket_id = 'plan-documents' AND public.can_edit_plan(((storage.foldername(name))[1])::uuid));
+CREATE POLICY "plan docs update" ON storage.objects FOR UPDATE TO authenticated
+ USING (bucket_id = 'plan-documents' AND public.can_edit_plan(((storage.foldername(name))[1])::uuid));
+CREATE POLICY "plan docs delete" ON storage.objects FOR DELETE TO authenticated
+ USING (bucket_id = 'plan-documents' AND public.can_edit_plan(((storage.foldername(name))[1])::uuid));
+
+-- ── updated_at triggers ─────────────────────────────────────────────────────
+DROP TRIGGER IF EXISTS trg_student_plans_upd ON public.student_plans;
+CREATE TRIGGER trg_student_plans_upd BEFORE UPDATE ON public.student_plans
+ FOR EACH ROW EXECUTE FUNCTION public.set_updated_at();
+
+DROP TRIGGER IF EXISTS trg_plan_details_upd ON public.plan_details;
+CREATE TRIGGER trg_plan_details_upd BEFORE UPDATE ON public.plan_details
+ FOR EACH ROW EXECUTE FUNCTION public.set_updated_at();
+
+DROP TRIGGER IF EXISTS trg_plan_goals_upd ON public.plan_goals;
+CREATE TRIGGER trg_plan_goals_upd BEFORE UPDATE ON public.plan_goals
+ FOR EACH ROW EXECUTE FUNCTION public.set_updated_at();
+
+-- ── Lock the predicates to signed-in users ──────────────────────────────────
+-- Postgres grants EXECUTE to PUBLIC by default, which would expose these
+-- SECURITY DEFINER helpers as callable REST RPC endpoints to the anon role.
+-- They exist only to back RLS policies.
+DO $$
+DECLARE fn TEXT;
+BEGIN
+ FOREACH fn IN ARRAY ARRAY[
+ 'public.can_view_plan_full(uuid)',
+ 'public.can_view_plan_impl(uuid)',
+ 'public.can_edit_plan(uuid)',
+ 'public.can_view_goal(uuid)',
+ 'public.can_edit_goal(uuid)',
+ 'public.can_view_meeting(uuid)',
+ 'public.can_edit_meeting(uuid)'
+ ] LOOP
+ EXECUTE format('REVOKE EXECUTE ON FUNCTION %s FROM PUBLIC, anon', fn);
+ EXECUTE format('GRANT EXECUTE ON FUNCTION %s TO authenticated, service_role', fn);
+ END LOOP;
+END $$;