-- ============================================================ -- 023_chat_media.sql -- -- Adds the `chat-media` Supabase Storage bucket used when an agent -- sends a photo / video / document / voice note from the inbox -- composer (issue #213). Today media can only be RECEIVED from -- customers or sent via the Flows `send_media` node — never typed -- and sent live in a 1:1 thread. -- -- Mirrors the `flow-media` bucket (migration 016) and its -- account-scoped storage RLS (migration 020), with two differences: -- -- 1. A separate bucket so chat attachments and flow-builder media -- stay conceptually distinct (and so a future per-bucket size / -- retention policy can diverge without touching flows). -- -- 2. The allowed MIME list adds the audio types Meta accepts for -- outbound voice notes — audio/ogg (Opus), audio/mpeg, audio/aac, -- audio/mp4, audio/amr. Browser recordings (WebM/Opus) are -- transcoded to audio/ogg BEFORE upload, so WebM never lands -- here and isn't allow-listed. -- -- Path convention (same as flow-media post-020): -- chat-media/account-/-. -- The bucket is public so Meta can fetch the URL without auth; writes -- are scoped to account members via the path's first segment. -- -- Size limit 16 MB — Meta's tightest universal cap (video). Documents -- can technically be 100 MB on Meta, but we hold the universal cap to -- match flow-media and keep one limit to reason about. -- -- Idempotent — safe to re-run. -- ============================================================ -- ============================================================ -- 1. chat-media storage bucket -- ============================================================ INSERT INTO storage.buckets (id, name, public, file_size_limit, allowed_mime_types) VALUES ( 'chat-media', 'chat-media', TRUE, 16777216, -- 16 MB (Meta video cap; documents/images/audio fit under this) ARRAY[ -- Images 'image/png', 'image/jpeg', 'image/webp', -- Videos 'video/mp4', 'video/3gpp', -- Documents 'application/pdf', 'application/vnd.ms-powerpoint', 'application/msword', 'application/vnd.ms-excel', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', 'application/vnd.openxmlformats-officedocument.presentationml.presentation', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', 'text/plain', -- Audio (voice notes) — only Meta-accepted outbound types. Browser -- WebM/Opus is transcoded to audio/ogg before upload. 'audio/ogg', 'audio/mpeg', 'audio/aac', 'audio/mp4', 'audio/amr' ] ) ON CONFLICT (id) DO UPDATE SET public = EXCLUDED.public, file_size_limit = EXCLUDED.file_size_limit, allowed_mime_types = EXCLUDED.allowed_mime_types; -- ============================================================ -- 2. Storage RLS — account-scoped writes, public reads -- -- Same predicate shape as migration 020's flow-media policies: -- writes are allowed when the path's first segment is -- `account-` for an account the caller belongs to. -- Reads are public (the bucket is public so Meta can fetch links). -- -- Drop-then-create (Postgres has no CREATE POLICY IF NOT EXISTS). -- ============================================================ DROP POLICY IF EXISTS "Chat media is publicly readable" ON storage.objects; CREATE POLICY "Chat media is publicly readable" ON storage.objects FOR SELECT USING (bucket_id = 'chat-media'); DROP POLICY IF EXISTS "Members can upload chat media" ON storage.objects; CREATE POLICY "Members can upload chat media" ON storage.objects FOR INSERT WITH CHECK ( bucket_id = 'chat-media' AND EXISTS ( SELECT 1 FROM public.profiles p WHERE p.user_id = auth.uid() AND ('account-' || p.account_id::text) = (storage.foldername(name))[1] ) ); DROP POLICY IF EXISTS "Members can update chat media" ON storage.objects; CREATE POLICY "Members can update chat media" ON storage.objects FOR UPDATE USING ( bucket_id = 'chat-media' AND EXISTS ( SELECT 1 FROM public.profiles p WHERE p.user_id = auth.uid() AND ('account-' || p.account_id::text) = (storage.foldername(name))[1] ) ); DROP POLICY IF EXISTS "Members can delete chat media" ON storage.objects; CREATE POLICY "Members can delete chat media" ON storage.objects FOR DELETE USING ( bucket_id = 'chat-media' AND EXISTS ( SELECT 1 FROM public.profiles p WHERE p.user_id = auth.uid() AND ('account-' || p.account_id::text) = (storage.foldername(name))[1] ) );