name: supabase-react-auth-black-screen
description: Debug and fix "tela preta" (black screen) hang on login in Supabase + React apps — when RequireAuth gets stuck in loading state.
category: devops
Supabase React Auth — Tela Preta (Black Screen) Debug
Problem
User authenticates with Google OAuth successfully, but app shows a black/blank screen indefinitely. Browser console shows no errors (or only favicon.ico 404). The RequireAuth component is stuck in loading=true, user=null state.
Root Cause Pattern
In useAuth.ts (or similar AuthProvider), the loadProfile() function makes Supabase queries (profiles, user_roles) that are silently blocked by RLS (Row Level Security) with the anon key. Because there's no error handling or timeout, profileLoading stays true forever → RequireAuth stays in loading state → blank screen.
`typescript
// ❌ BROKEN — no error handling, no timeout
async function loadProfile(uid: string) {
setProfileLoading(true);
const [{ data: rs }, { data: profile }] = await Promise.all([
supabase.from("user_roles").select("role").eq("user_id", uid), // RLS blocks → hangs forever
supabase.from("profiles").select("full_name").eq("id", uid).maybeSingle(),
]);
// never reaches here if RLS silently blocks
setProfileLoading(false);
}
`
Also common: wrong field name — select("role") when the column is role_id.
Diagnosis Steps
1. Browser console — look for [useAuth] loadProfile falhou: messages
2. Network tab — queries to profiles or user_roles return empty [] or 403 → RLS blocking
3. Supabase dashboard — verify profiles table has the user's row; check allowed_emails if using that pattern
4. Wrong field name — check if select("role") should be select("role_id")
Fix (useAuth.ts)
Add try/catch/finally with an 8-second timeout:
`typescript
async function loadProfile(uid: string) {
setProfileLoading(true);
try {
// 8s timeout prevents infinite hang on RLS block
const timeout = new Promise
setTimeout(() => reject(new Error("timeout")), 8000)
);
const [{ data: rs, error: rsErr }, { data: profile, error: profErr }] = await Promise.all([
supabase.from("user_roles").select("role_id").eq("user_id", uid),
supabase.from("profiles").select("full_name").eq("id", uid).maybeSingle(),
timeout,
]);
if (!mounted) return;
console.warn("[useAuth] user_roles:", rsErr?.message ?? "ok", "profiles:", profErr?.message ?? "ok");
setRoles((rs?.map((r) => r.role_id as AppRole)) ?? []);
setFullName(profile?.full_name ?? "");
} catch (e) {
if (!mounted) return;
console.error("[useAuth] loadProfile falhou:", e);
setRoles([]);
setFullName("");
} finally {
if (mounted) setProfileLoading(false);
}
}
`
Rebuild & Redeploy (Documents app example)
`bash
On VPS
cd /var/www/documentos/deploy-vps/app && npm run build
systemctl restart documentos
`
Alternative Root Cause: Browser Cache with Stale JS
If console shows errors from files that don't exist on server (e.g., adm.usuarios-CfN-Fu9m.js 404), it's a browser cache problem — old JavaScript bundle is cached but server has a newer build.
Fix: Ctrl+Shift+R (hard refresh) or DevTools → Network → "Disable cache" → reload.
Key Distinction Table
| Error | Cause | Fix |
| ------- | ------- | ----- |
GET /auth/v1/admin/users 403 from file that doesn't exist on server | Browser cache | Ctrl+Shift+R |
| Black screen, no console errors, no network activity | profileLoading never resolves | Add timeout + error handling to loadProfile |
allowed_emails returns empty via REST API | RLS blocks anon key reads | Check table directly in Supabase dashboard |
RAISE EXCEPTION 'Email não autorizado' in Supabase logs | handle_new_user trigger fired but email not in allowed_emails | Add email to allowed_emails table |
Projects Using This Pattern
- - Documents (
documentos.rochasalesseguros.com.br) — Supabaselzoiuxulhnvtmadoymrb, TanStack Start, port 3010 - - ComercialRS (
comercialrs.lovable.app) — Supabasenzaxsorgsecnmqwufawn
Key Insight
The blank screen is RequireAuth rendering loading=true && !user. The loading flag is controlled by profileLoading from loadProfile(). If that function hangs (RLS block, wrong query, network timeout), the user is stuck forever. Always wrap Supabase profile queries with timeout + error handling.