📄 SKILL.md

← Vault

name: supabase-storage-400-debug

description: Debug Supabase Storage 400 errors when supabase.storage.from().download() fails for all files


Supabase Storage 400 Debug Checklist

Symptoms

supabase.storage.from("bucket").download() returns 400 for all files. Console shows:

`

https://xxx.supabase.co/storage/v1/object/bucket-name/path/to/file 400 (Bad Request)

`

Diagnostic Steps

1. Check if bucket exists — Query storage REST API directly:

`bash

curl -s 'https://PROJECT.supabase.co/storage/v1/bucket' \

-H 'Authorization: Bearer ANON_KEY'

`

- [] = bucket does NOT exist

- Returns objects = bucket exists (check RLS/permissions)

2. Check RLS on storage.objects — Even if bucket exists, RLS may block access:

`sql

SELECT * FROM storage.objects LIMIT 1;

`

(If table doesn't exist or query fails, storage may not be configured)

3. Check actual file storage path — The app may store files locally on VPS instead of Supabase Storage. Look at the upload code:

- If upload writes to /var/www/... → files are on VPS filesystem, not Supabase Storage

- Database storage_path column tells WHERE the file is

4. Check nginx alias — Verify nginx config serves the right directory:

`

location /uploads/ {

alias /var/www/documentos/uploads/; # must match actual storage path

expires 30d;

}

`

5. Test public URL directlycurl -I https://domain/uploads/... returns 200? If yes, switch to direct URL fetch instead of Supabase Storage API.

Fix Pattern: Switch to Direct URL Fetch

When Supabase Storage bucket doesn't exist but files are on VPS:

`typescript

const PUBLIC_URL = "https://your-domain.com";

function storageUrl(storagePath: string) {

return ${PUBLIC_URL}${storagePath};

}

async function openPreview(doc: Doc) {

try {

const res = await fetch(storageUrl(doc.storage_path));

if (!res.ok) throw new Error(HTTP ${res.status});

const blob = await res.blob();

const url = URL.createObjectURL(blob);

setPreview({ doc, url });

} catch {

toast.error("Falha ao abrir documento");

}

}

async function downloadZip(docs: Doc[]) {

const zip = new JSZip();

for (const d of docs) {

try {

const res = await fetch(storageUrl(d.storage_path));

if (res.ok) zip.file(d.stored_name, await res.blob());

} catch { / skip failed files / }

}

triggerBlob(await zip.generateAsync({ type: "blob" }), "arquivos.zip");

}

`

Security note: Direct URL files are public. If files need authentication, create a proxy endpoint that validates the session before serving.

Root Cause Pattern

Lovable-generated apps sometimes store uploads on VPS filesystem (/var/www/...) while the Supabase Storage bucket was never created. The storage_path in the DB is a filesystem path like /uploads/userId/clientId/storedName, not a Supabase Storage path.