-- File assets library (validated uploads per tenant) -- Idempotent. create table if not exists public.asset_files ( id uuid primary key default gen_random_uuid(), tenant_id uuid not null references public.tenants(id) on delete cascade, title text not null, folder text not null default '', file_name text not null, mime_type text, bytes bigint, storage_path text not null, sha256 text, status text not null default 'approved' check (status in ('approved', 'archived')), 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 asset_files_tenant_folder_idx on public.asset_files (tenant_id, folder, created_at desc); alter table public.asset_files enable row level security; drop policy if exists asset_files_tenant_select on public.asset_files; drop policy if exists asset_files_tenant_insert on public.asset_files; drop policy if exists asset_files_tenant_update on public.asset_files; drop policy if exists asset_files_tenant_delete on public.asset_files; create policy asset_files_tenant_select on public.asset_files for select using ( tenant_id::text = coalesce( auth.jwt() -> 'app_metadata' ->> 'tenant_id', auth.jwt() -> 'user_metadata' ->> 'tenant_id' ) ); create policy asset_files_tenant_insert on public.asset_files for insert with check ( tenant_id::text = coalesce( auth.jwt() -> 'app_metadata' ->> 'tenant_id', auth.jwt() -> 'user_metadata' ->> 'tenant_id' ) ); create policy asset_files_tenant_update on public.asset_files for update using ( tenant_id::text = coalesce( auth.jwt() -> 'app_metadata' ->> 'tenant_id', auth.jwt() -> 'user_metadata' ->> 'tenant_id' ) ); create policy asset_files_tenant_delete on public.asset_files for delete using ( tenant_id::text = coalesce( auth.jwt() -> 'app_metadata' ->> 'tenant_id', auth.jwt() -> 'user_metadata' ->> 'tenant_id' ) ); -- Private bucket (created via API if missing). Policies for storage.objects: -- path layout: {tenant_id}/{optional/folder/}{uuid}_{filename}