Fix Supabase linter performance findings

- add covering indexes for flagged foreign keys
- optimize RLS policies to use select auth.uid()
- consolidate posts update policies into one permissive rule
- document linter findings and decisions

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
denshooter
2026-04-24 12:00:43 +00:00
parent 8959485964
commit 0d4b69713d
3 changed files with 144 additions and 16 deletions
+64
View File
@@ -0,0 +1,64 @@
# Database performance notes (Supabase linter)
This project had two performance-related linter groups:
1. **RLS auth function evaluation**
2. **Unindexed foreign keys**
## What was changed
1. Added migration `infrastructure/migrations/002_perf_linter_fixes.sql`.
2. Updated `infrastructure/migrations/001_schema.sql` to keep fresh setups aligned with the same fixes.
### RLS changes
The affected policies now use:
- `(select auth.uid())` instead of `auth.uid()`
This avoids repeated per-row evaluation in RLS plans for these policies:
- `profiles_update`
- `posts_select`
- `posts_insert`
- `posts_update_own`
- `resonances_insert`
- `resonances_delete`
- `whispers_select`
- `whispers_insert`
- `follows_insert`
- `follows_delete`
- `reports_insert`
- `reports_select_own`
Also, duplicate permissive update policies on `public.posts` were consolidated by removing `posts_softdelete` and keeping a single `posts_update_own`.
### FK index changes
Added covering indexes:
- `idx_follows_following_id` on `public.follows(following_id)`
- `idx_reports_reporter_id` on `public.reports(reporter_id)`
- `idx_resonances_user_id` on `public.resonances(user_id)`
- `idx_whispers_from_user_id` on `public.whispers(from_user_id)`
- `idx_whispers_post_id` on `public.whispers(post_id)`
## About "unused index" findings
`unused_index` is based on `pg_stat_user_indexes.idx_scan`, which can read as zero after restarts or during low traffic.
In this environment, PostgreSQL was recently restarted, so these were not dropped automatically.
Suggested review query:
```sql
select
schemaname,
relname as table_name,
indexrelname as index_name,
idx_scan
from pg_stat_user_indexes
where schemaname = 'public'
order by idx_scan asc, indexrelname;
```
Drop an index only after observing sustained zero usage over representative production traffic.
+18 -16
View File
@@ -87,8 +87,13 @@ CREATE INDEX IF NOT EXISTS idx_posts_created_at ON public.posts(created_at DESC)
CREATE INDEX IF NOT EXISTS idx_posts_review ON public.posts(review_status) CREATE INDEX IF NOT EXISTS idx_posts_review ON public.posts(review_status)
WHERE review_status != 'ok'; WHERE review_status != 'ok';
CREATE INDEX IF NOT EXISTS idx_resonances_post ON public.resonances(post_id); CREATE INDEX IF NOT EXISTS idx_resonances_post ON public.resonances(post_id);
CREATE INDEX IF NOT EXISTS idx_resonances_user_id ON public.resonances(user_id);
CREATE INDEX IF NOT EXISTS idx_follows_follower ON public.follows(follower_id); CREATE INDEX IF NOT EXISTS idx_follows_follower ON public.follows(follower_id);
CREATE INDEX IF NOT EXISTS idx_follows_following_id ON public.follows(following_id);
CREATE INDEX IF NOT EXISTS idx_whispers_to ON public.whispers(to_user_id, read_at); CREATE INDEX IF NOT EXISTS idx_whispers_to ON public.whispers(to_user_id, read_at);
CREATE INDEX IF NOT EXISTS idx_whispers_from_user_id ON public.whispers(from_user_id);
CREATE INDEX IF NOT EXISTS idx_whispers_post_id ON public.whispers(post_id);
CREATE INDEX IF NOT EXISTS idx_reports_reporter_id ON public.reports(reporter_id);
-- ── View: Feed Posts ──────────────────────────────────────────────────────── -- ── View: Feed Posts ────────────────────────────────────────────────────────
-- Wird in der iOS-App direkt abgefragt — joins alles zusammen -- Wird in der iOS-App direkt abgefragt — joins alles zusammen
@@ -195,7 +200,7 @@ ALTER TABLE public.reports ENABLE ROW LEVEL SECURITY;
-- profiles: jeder darf lesen, nur eigenes darf geändert werden -- profiles: jeder darf lesen, nur eigenes darf geändert werden
CREATE POLICY "profiles_select" ON public.profiles FOR SELECT USING (true); CREATE POLICY "profiles_select" ON public.profiles FOR SELECT USING (true);
CREATE POLICY "profiles_update" ON public.profiles FOR UPDATE USING (auth.uid() = id); CREATE POLICY "profiles_update" ON public.profiles FOR UPDATE USING ((select auth.uid()) = id);
-- posts: Feed-Sichtbarkeit (über View geregelt), eigene Verwaltung -- posts: Feed-Sichtbarkeit (über View geregelt), eigene Verwaltung
CREATE POLICY "posts_select" ON public.posts FOR SELECT CREATE POLICY "posts_select" ON public.posts FOR SELECT
@@ -204,34 +209,31 @@ CREATE POLICY "posts_select" ON public.posts FOR SELECT
AND review_status IN ('ok', 'approved') AND review_status IN ('ok', 'approved')
AND ( AND (
NOT is_anonymous NOT is_anonymous
OR user_id = auth.uid() OR user_id = (select auth.uid())
) )
); );
CREATE POLICY "posts_insert" ON public.posts FOR INSERT CREATE POLICY "posts_insert" ON public.posts FOR INSERT
WITH CHECK (auth.uid() = user_id); WITH CHECK ((select auth.uid()) = user_id);
CREATE POLICY "posts_update_own" ON public.posts FOR UPDATE CREATE POLICY "posts_update_own" ON public.posts FOR UPDATE
USING (auth.uid() = user_id AND deleted_at IS NULL); USING ((select auth.uid()) = user_id)
-- Soft-Delete: nur eigene Posts dürfen als gelöscht markiert werden WITH CHECK ((select auth.uid()) = user_id);
CREATE POLICY "posts_softdelete" ON public.posts FOR UPDATE
USING (auth.uid() = user_id)
WITH CHECK (deleted_at IS NOT NULL);
-- resonances -- resonances
CREATE POLICY "resonances_select" ON public.resonances FOR SELECT USING (true); CREATE POLICY "resonances_select" ON public.resonances FOR SELECT USING (true);
CREATE POLICY "resonances_insert" ON public.resonances FOR INSERT WITH CHECK (auth.uid() = user_id); CREATE POLICY "resonances_insert" ON public.resonances FOR INSERT WITH CHECK ((select auth.uid()) = user_id);
CREATE POLICY "resonances_delete" ON public.resonances FOR DELETE USING (auth.uid() = user_id); CREATE POLICY "resonances_delete" ON public.resonances FOR DELETE USING ((select auth.uid()) = user_id);
-- whispers: nur Absender und Empfänger -- whispers: nur Absender und Empfänger
CREATE POLICY "whispers_select" ON public.whispers FOR SELECT CREATE POLICY "whispers_select" ON public.whispers FOR SELECT
USING (auth.uid() = from_user_id OR auth.uid() = to_user_id); USING ((select auth.uid()) = from_user_id OR (select auth.uid()) = to_user_id);
CREATE POLICY "whispers_insert" ON public.whispers FOR INSERT CREATE POLICY "whispers_insert" ON public.whispers FOR INSERT
WITH CHECK (auth.uid() = from_user_id); WITH CHECK ((select auth.uid()) = from_user_id);
-- follows -- follows
CREATE POLICY "follows_select" ON public.follows FOR SELECT USING (true); CREATE POLICY "follows_select" ON public.follows FOR SELECT USING (true);
CREATE POLICY "follows_insert" ON public.follows FOR INSERT WITH CHECK (auth.uid() = follower_id); CREATE POLICY "follows_insert" ON public.follows FOR INSERT WITH CHECK ((select auth.uid()) = follower_id);
CREATE POLICY "follows_delete" ON public.follows FOR DELETE USING (auth.uid() = follower_id); CREATE POLICY "follows_delete" ON public.follows FOR DELETE USING ((select auth.uid()) = follower_id);
-- reports: nur eigene einsehen -- reports: nur eigene einsehen
CREATE POLICY "reports_insert" ON public.reports FOR INSERT WITH CHECK (auth.uid() = reporter_id); CREATE POLICY "reports_insert" ON public.reports FOR INSERT WITH CHECK ((select auth.uid()) = reporter_id);
CREATE POLICY "reports_select_own" ON public.reports FOR SELECT USING (auth.uid() = reporter_id); CREATE POLICY "reports_select_own" ON public.reports FOR SELECT USING ((select auth.uid()) = reporter_id);
@@ -0,0 +1,62 @@
-- Performance follow-up for DB linter findings:
-- 1) Wrap auth.uid() in RLS policies with SELECT to avoid per-row re-evaluation.
-- 2) Ensure every FK in follows/reports/resonances/whispers has a covering index.
-- 3) Merge duplicate permissive UPDATE policies on posts.
BEGIN;
ALTER POLICY profiles_update ON public.profiles
USING ((select auth.uid()) = id);
ALTER POLICY posts_select ON public.posts
USING (
deleted_at IS NULL
AND review_status IN ('ok', 'approved')
AND (
NOT is_anonymous
OR user_id = (select auth.uid())
)
);
ALTER POLICY posts_insert ON public.posts
WITH CHECK ((select auth.uid()) = user_id);
DROP POLICY IF EXISTS posts_softdelete ON public.posts;
ALTER POLICY posts_update_own ON public.posts
USING ((select auth.uid()) = user_id)
WITH CHECK ((select auth.uid()) = user_id);
ALTER POLICY resonances_insert ON public.resonances
WITH CHECK ((select auth.uid()) = user_id);
ALTER POLICY resonances_delete ON public.resonances
USING ((select auth.uid()) = user_id);
ALTER POLICY whispers_select ON public.whispers
USING (
(select auth.uid()) = from_user_id
OR (select auth.uid()) = to_user_id
);
ALTER POLICY whispers_insert ON public.whispers
WITH CHECK ((select auth.uid()) = from_user_id);
ALTER POLICY follows_insert ON public.follows
WITH CHECK ((select auth.uid()) = follower_id);
ALTER POLICY follows_delete ON public.follows
USING ((select auth.uid()) = follower_id);
ALTER POLICY reports_insert ON public.reports
WITH CHECK ((select auth.uid()) = reporter_id);
ALTER POLICY reports_select_own ON public.reports
USING ((select auth.uid()) = reporter_id);
CREATE INDEX IF NOT EXISTS idx_follows_following_id ON public.follows (following_id);
CREATE INDEX IF NOT EXISTS idx_reports_reporter_id ON public.reports (reporter_id);
CREATE INDEX IF NOT EXISTS idx_resonances_user_id ON public.resonances (user_id);
CREATE INDEX IF NOT EXISTS idx_whispers_from_user_id ON public.whispers (from_user_id);
CREATE INDEX IF NOT EXISTS idx_whispers_post_id ON public.whispers (post_id);
COMMIT;