Skip to main content

Overview

Supabase Storage policies control access to files in storage buckets. Portal Ciudadano Manta uses the imagenes bucket for storing photos from news articles, reports, and surveys. Location: sql/storage_policies.sql
Storage buckets require RLS-style policies. Without policies, no one can upload or access files.

Storage Bucket Configuration

Create Bucket

1

Navigate to Storage

In Supabase dashboard, go to Storage
2

Create Bucket

Click “New bucket” with these settings:
  • Name: imagenes
  • Public bucket: OFF (controlled by policies)
  • File size limit: 5MB (5242880 bytes)
  • Allowed MIME types: image/jpeg, image/jpg, image/png, image/webp
3

Create Folders

Create subfolders for organization:
  • noticias/ - News article images
  • reportes/ - Report photos
  • encuestas/ - Survey images

Storage Policy Structure

Storage policies are applied to the storage.objects table:
CREATE POLICY "policy_name"
ON storage.objects
FOR SELECT | INSERT | UPDATE | DELETE
TO authenticated | public
USING (...)      -- For SELECT/UPDATE/DELETE
WITH CHECK (...); -- For INSERT/UPDATE

Public Read Policy

Allow anyone (authenticated or not) to view images:
CREATE POLICY "Lectura pública de imágenes"
ON storage.objects
FOR SELECT
TO public
USING (
  bucket_id = 'imagenes'
);
This enables displaying images to all visitors without authentication.

Noticias Policies

Admin Upload

CREATE POLICY "Administradores pueden subir imágenes de noticias"
ON storage.objects
FOR INSERT
TO authenticated
WITH CHECK (
  bucket_id = 'imagenes' 
  AND (storage.foldername(name))[1] = 'noticias'
  AND EXISTS (
    SELECT 1 FROM public.administradores
    WHERE id = auth.uid()
  )
);
Requirements:
  • User must be authenticated
  • File must be in imagenes/noticias/ folder
  • User must exist in administradores table

Admin Update

CREATE POLICY "Administradores pueden actualizar imágenes de noticias"
ON storage.objects
FOR UPDATE
TO authenticated
USING (
  bucket_id = 'imagenes' 
  AND (storage.foldername(name))[1] = 'noticias'
  AND EXISTS (
    SELECT 1 FROM public.administradores
    WHERE id = auth.uid()
  )
);
Allows admins to replace news images.

Admin Delete

CREATE POLICY "Administradores pueden eliminar imágenes de noticias"
ON storage.objects
FOR DELETE
TO authenticated
USING (
  bucket_id = 'imagenes' 
  AND (storage.foldername(name))[1] = 'noticias'
  AND EXISTS (
    SELECT 1 FROM public.administradores
    WHERE id = auth.uid()
  )
);
Allows admins to delete news images.

Reportes Policies

User Upload

CREATE POLICY "Usuarios pueden subir imágenes de reportes"
ON storage.objects
FOR INSERT
TO authenticated
WITH CHECK (
  bucket_id = 'imagenes' 
  AND (storage.foldername(name))[1] = 'reportes'
  AND EXISTS (
    SELECT 1 FROM public.usuarios
    WHERE id = auth.uid()
  )
);
Requirements:
  • User must be authenticated
  • File must be in imagenes/reportes/ folder
  • User must exist in usuarios table

User Delete Own Files

CREATE POLICY "Usuarios pueden eliminar sus imágenes de reportes"
ON storage.objects
FOR DELETE
TO authenticated
USING (
  bucket_id = 'imagenes' 
  AND (storage.foldername(name))[1] = 'reportes'
  AND owner = auth.uid()
);
Users can only delete images they uploaded (matched by owner field).

Encuestas Policies

Admin Upload

CREATE POLICY "Administradores pueden subir imágenes de encuestas"
ON storage.objects
FOR INSERT
TO authenticated
WITH CHECK (
  bucket_id = 'imagenes' 
  AND (storage.foldername(name))[1] = 'encuestas'
  AND EXISTS (
    SELECT 1 FROM public.administradores
    WHERE id = auth.uid()
  )
);

Admin Delete

CREATE POLICY "Administradores pueden eliminar imágenes de encuestas"
ON storage.objects
FOR DELETE
TO authenticated
USING (
  bucket_id = 'imagenes' 
  AND (storage.foldername(name))[1] = 'encuestas'
  AND EXISTS (
    SELECT 1 FROM public.administradores
    WHERE id = auth.uid()
  )
);

Helper Functions

storage.foldername()

Extracts folder path from file name:
storage.foldername('noticias/2024/photo.jpg')
-- Returns: ['noticias', '2024']

(storage.foldername(name))[1]
-- Returns: 'noticias' (first folder)

auth.uid()

Returns current user’s UUID:
owner = auth.uid()  -- Check if user owns the file

File Upload Example

Upload a file from the client:
import { supabase } from '@/lib/supabase';

async function uploadImage(file: File, folder: 'noticias' | 'reportes' | 'encuestas') {
  const fileName = `${Date.now()}-${file.name}`;
  const filePath = `${folder}/${fileName}`;
  
  const { data, error } = await supabase.storage
    .from('imagenes')
    .upload(filePath, file, {
      cacheControl: '3600',
      upsert: false
    });
  
  if (error) {
    console.error('Error uploading:', error);
    return null;
  }
  
  // Get public URL
  const { data: { publicUrl } } = supabase.storage
    .from('imagenes')
    .getPublicUrl(filePath);
  
  return publicUrl;
}

File Download/Display

Get public URL for displaying images:
const imageUrl = supabase.storage
  .from('imagenes')
  .getPublicUrl('noticias/photo.jpg').data.publicUrl;

// Use in <img> tag
<img :src="imageUrl" alt="News photo" />
Public URLs work because of the “Lectura pública de imágenes” SELECT policy.

File Deletion

Delete a file:
const { error } = await supabase.storage
  .from('imagenes')
  .remove(['reportes/old-photo.jpg']);

if (error) {
  console.error('Error deleting:', error);
}

Folder Organization

Recommended folder structure:
imagenes/
├── noticias/
│   ├── 2024-01-15-alcalde-reunion.jpg
│   ├── 2024-01-20-nueva-obra.jpg
│   └── ...
├── reportes/
│   ├── <uuid>-bache-calle-10.jpg
│   ├── <uuid>-alumbrado-roto.jpg
│   └── ...
└── encuestas/
    ├── encuesta-satisfaccion.jpg
    └── ...
Best practices:
  • Use descriptive filenames
  • Include timestamps or UUIDs to avoid collisions
  • Keep folder structure flat (avoid deep nesting)

Verifying Policies

Check storage policies in SQL Editor:
SELECT 
  policyname,
  cmd,
  roles,
  qual,
  with_check
FROM pg_policies
WHERE tablename = 'objects' 
  AND schemaname = 'storage'
ORDER BY policyname;
Expected policies:
policynamecmdroles
Administradores pueden eliminar imágenes de encuestasDELETEauthenticated
Administradores pueden eliminar imágenes de noticiasDELETEauthenticated
Administradores pueden subir imágenes de encuestasINSERTauthenticated
Administradores pueden subir imágenes de noticiasINSERTauthenticated
Administradores pueden actualizar imágenes de noticiasUPDATEauthenticated
Lectura pública de imágenesSELECTpublic
Usuarios pueden eliminar sus imágenes de reportesDELETEauthenticated
Usuarios pueden subir imágenes de reportesINSERTauthenticated

File Size Limits

Bucket Configuration

Set in Supabase dashboard:
  • Max file size: 5MB (5242880 bytes)
  • Enforced server-side automatically

Client-Side Validation

Validate before upload:
function validateFile(file: File): string | null {
  const maxSize = 5 * 1024 * 1024; // 5MB
  const allowedTypes = ['image/jpeg', 'image/jpg', 'image/png', 'image/webp'];
  
  if (file.size > maxSize) {
    return 'El archivo excede 5MB';
  }
  
  if (!allowedTypes.includes(file.type)) {
    return 'Formato no permitido. Use JPG, PNG o WebP';
  }
  
  return null; // Valid
}

Troubleshooting

Upload Fails: “new row violates row-level security policy”

Cause: Policy blocking upload Solutions:
  1. Verify user is authenticated
  2. Check user exists in correct table (administradores or usuarios)
  3. Verify file path matches folder in policy (e.g., starts with noticias/)
  4. Check bucket name is exactly imagenes

Cannot View Images

Cause: Missing SELECT policy Solution: Ensure “Lectura pública de imágenes” policy exists:
CREATE POLICY "Lectura pública de imágenes"
ON storage.objects FOR SELECT TO public
USING (bucket_id = 'imagenes');

Cannot Delete Own Files

Cause: owner field not matching auth.uid() Solution:
  1. When uploading, Supabase automatically sets owner to auth.uid()
  2. Verify user is same one who uploaded file
  3. Check DELETE policy includes owner = auth.uid()

Performance Considerations

Image Optimization

Optimize images before upload:
import sharp from 'sharp'; // Server-side

// Resize and compress
const optimized = await sharp(file)
  .resize(1200, 1200, { fit: 'inside', withoutEnlargement: true })
  .jpeg({ quality: 85 })
  .toBuffer();

CDN Caching

Supabase Storage uses CDN. Set cache headers:
const { data, error } = await supabase.storage
  .from('imagenes')
  .upload(filePath, file, {
    cacheControl: '3600', // 1 hour cache
  });

Next Steps

Row Level Security

Learn about database RLS policies

Supabase Setup

Complete backend configuration

Build docs developers (and LLMs) love