name: supabase-management-api-cors-proxy
description: Proxy Supabase Management API calls through TanStack Start server route to avoid browser CORS blocks
Problem
The Supabase Management API (api.supabase.com/v1/projects/...) blocks browser-based fetch calls with CORS, even when the OPTIONS preflight returns 204. POST requests with JSON body fail because the Access-Control-Allow-Origin header is not included in the response.
This happens because:
- -
curl -I -X OPTIONS(preflight) returns 204 with CORS headers - - Simple GET requests work
- - But POST with
Content-Type: application/jsonandAuthorization: Bearergets blocked - -
Authorization: Bearer— management key - -
apikey:— also needed for RLS bypass - -
Prefer: return=representation— returns the inserted row
Solution
Create a TanStack Start API route (server-side) that proxies Management API calls. The browser calls the internal route (same origin, no CORS), and the server makes the actual Management API request.
Step 1: Create the proxy route
Create src/routes/api/public/proxy-route.ts:
`typescript
import { createFileRoute } from "@tanstack/react-router";
export const Route = createFileRoute("/api/public/your-action")({
POST: async ({ request }) => {
try {
const { someField } = await request.json();
const SUPABASE_URL = "https://dauftiqcvgaydddoxqhh.supabase.co";
const SUPABASE_PAT = "sbp_REDACTED";
const res = await fetch(${SUPABASE_URL}/rest/v1/your_table, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: Bearer ${SUPABASE_PAT},
apikey: SUPABASE_PAT,
Prefer: "return=representation",
},
body: JSON.stringify({ ... }),
});
const data = await res.json();
if (!res.ok) {
return Response.json({ error: "Erro ao fazer algo", details: data }, { status: 500 });
}
return Response.json({ success: true, data });
} catch (err: any) {
return Response.json({ error: err.message || "Erro desconhecido" }, { status: 500 });
}
},
});
`
Key headers for Supabase REST API (bypass RLS):
Step 2: Call from the frontend
`typescript
async function doSomething() {
const res = await fetch("/api/public/your-action", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ someField }),
});
const data = await res.json();
if (!res.ok || data.error) {
return toast.error(data.error || "Erro");
}
return data;
}
`
Why not use Management API directly from browser?
Management API CORS preflight responds with 204 but the actual POST request (with JSON body and Bearer token) gets blocked by the browser's CORS policy. The proxy route avoids this entirely.
Critical Limitation — No Parameterized Queries
The POST /v1/projects/{ref}/database/query endpoint does NOT support $1, $2 etc. parameter placeholders in the params array. All queries are rejected with 42P02: there is no parameter $1.
Always use direct string interpolation with SQL escaping:
`typescript
const safeValue = value.trim().replace(/'/g, "''");
body: JSON.stringify({
query: SELECT ... WHERE token = '${safeToken}' AND name = '${safeName}',
})
`
Tokens (SHA256 hex, 64 chars) are inherently safe from injection. For user text fields, always apply replace(/'/g, "''").
WRONG (silently fails with 400/404):
`typescript
body: JSON.stringify({
query: SELECT ... WHERE token = $1,
params: [token],
})
`
Alternative: Use Supabase REST API with anon key
For tables where RLS allows anonymous inserts, use supabase.from('table').insert(...) directly from the browser without CORS issues. Only use the proxy for Management API calls.