Three Supabase-advisor findings from the 2026-07-09 production log triage:
1. exchange_rates (rls_policy_always_true): the exchange_rates_insert
policy was WITH CHECK (true) for authenticated, letting any signed-in
user poison the shared FX cache that feeds money math (amount_sek on
ingested transactions, invoice SEK conversion). Migration
20260710100000 drops the policy and revokes INSERT from
anon/authenticated; only the service role writes the cache now (the
05:00 enable-banking sync cron and the v1 API-key paths both use the
service client). writeCachedRate() in lib/currency/riksbanken.ts was
already fail-soft and never inspects the upsert result, so
user-client paths (bank file import, refresh-exchange-rate) keep
returning the fetched rate unchanged when the cache write is
rejected; documented and covered by a new unit test.
2. journal_entry_lines (duplicate_index): idx_journal_entry_lines_entry
and idx_journal_entry_lines_entry_id are byte-identical btree indexes
on (journal_entry_id), verified via pg_indexes on prod. Migration
20260710101000 drops idx_journal_entry_lines_entry (created outside
the migration history); the repo-defined _entry_id stays.
3. receipts bucket (public_bucket_allows_listing): receipts_public_read
gave anon SELECT over every object in the bucket, enabling anonymous
listing. The bucket is unused: no code references it, public.receipts
has 0 rows in prod, 2 orphan objects from 2026-02-26. Migration
20260710102000 drops the anon policy; authenticated own-folder
policies stay untouched.
New tests/pg/db-advisor-lockdowns.pg.test.ts covers all three
(authenticated INSERT rejected, SELECT still works, privilege revoked,
duplicate index gone, anon cannot list receipts).
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>