If your app uses Supabase and suddenly stopped working for Indian users, you're not alone. India has DNS-blocked *.supabase.co at the ISP level. This article explains what's happening, why it breaks only browser-side calls, and how to fix it permanently in under 30 minutes — for free — using a Cloudflare Worker as a transparent proxy.
What Is Actually Happening
This is not a Supabase outage. The service is running fine globally. What's happening is that Indian ISPs have blocked the DNS records for *.supabase.co. When a browser in India tries to connect to your Supabase project, it asks the local DNS resolver for the IP address of abcdefgh.supabase.co. The ISP intercepts that query and returns nothing — or an error — so the connection never starts.
Your server-side code is completely unaffected. When Vercel (or any hosting provider outside India) runs your Next.js server components or API routes, it makes the DNS query from a US/EU data center, where Supabase is not blocked. Only the browser, running on the user's device in India, is impacted.
Why This Breaks More Than You Think
The Supabase JS SDK runs in the browser. When you call
supabase.auth.signIn(), supabase.from('table').select(), or
subscribe to a Realtime channel, those are all browser-to-Supabase
HTTP/WebSocket calls. Every single one fails for Indian users.
Symptoms your Indian users see:
- Login page spins forever, then shows a network error
- Dashboard loads (SSR works), but data tables are empty
- Real-time updates never arrive
- File uploads silently fail
The Fix: A Cloudflare Worker as a Transparent Proxy
The solution is to put a proxy in the middle that lives on a domain
that is NOT blocked in India. Cloudflare Workers run on
*.workers.dev — Cloudflare's own domain, fully accessible in India.
The Worker receives every browser request meant for Supabase, strips
nothing, adds nothing meaningful, and forwards it verbatim to the real
Supabase project URL. From Supabase's perspective, it looks like a
normal request. From the browser's perspective, it's talking to a
workers.dev domain that it can actually reach.
Why Cloudflare Workers specifically?
| Option | WebSocket Support | Free Tier | No Domain Needed |
|---|---|---|---|
| Cloudflare Worker | ✅ Yes | 100k req/day | ✅ *.workers.dev
|
| Vercel Rewrites | ❌ No | Unlimited | ✅ |
| Nginx reverse proxy | ✅ Yes | Self-hosted cost | ❌ Needs a domain |
| AWS Lambda | ✅ With ALB | Limited | ❌ Needs a domain |
Vercel Rewrites seem like the obvious choice since you're already on Vercel — but they do not support WebSocket protocol upgrades. Supabase Realtime uses WebSockets. If you route through Vercel, Realtime silently breaks. Cloudflare Workers handle WebSocket proxying natively.
Architecture After the Fix
Note that server-side clients (server.ts, service.ts) continue to communicate directly with Supabase. Only client.ts — the browser SDK — needs the proxy URL, and it picks that up automatically from NEXT_PUBLIC_SUPABASE_URL.
Step-by-Step Implementation
Step 1 — Create the Cloudflare Worker
- Go to dash.cloudflare.com
- In the left sidebar click Compute → Workers & Pages
- Click Create → Create Worker
- Name it
supabase-proxy - Click Deploy (this deploys a hello-world placeholder)
- Click Edit code
- Delete all existing code and paste the following:
const SUPABASE_ORIGIN = 'https://YOUR_PROJECT_REF.supabase.co';
export default {
async fetch(request) {
const url = new URL(request.url);
const targetUrl = SUPABASE_ORIGIN + url.pathname + url.search;
// Clone and rewrite the Host header so Supabase accepts the request
const headers = new Headers(request.headers);
headers.set('host', new URL(SUPABASE_ORIGIN).host);
// WebSocket upgrade (Supabase Realtime) — pass through unchanged
if (request.headers.get('Upgrade') === 'websocket') {
return fetch(targetUrl, { headers, method: request.method });
}
return fetch(targetUrl, {
method: request.method,
headers,
body: ['GET', 'HEAD'].includes(request.method) ? undefined : request.body,
redirect: 'follow',
});
},
};
Replace
YOUR_PROJECT_REFwith your actual Supabase project reference ID. Find it at: Supabase Dashboard → Project Settings → General → Reference ID It looks likeabcdefghijklmnop.Click Deploy
Copy your Worker URL from the deployment screen:
https://supabase-proxy.<your-account>.workers.dev
Step 2 — Update Your Environment Variable
This is the only application-level change required.
Before:
NEXT_PUBLIC_SUPABASE_URL=https://abcdefghijklmnop.supabase.co
After:
NEXT_PUBLIC_SUPABASE_URL=https://supabase-proxy.<your-account>.workers.dev
All other environment variables stay identical:
NEXT_PUBLIC_SUPABASE_ANON_KEY=eyJ... # unchanged
SUPABASE_SERVICE_ROLE_KEY=eyJ... # unchanged
If you're on Vercel:
- Dashboard → Project → Settings → Environment Variables
- Edit
NEXT_PUBLIC_SUPABASE_URL - Save → Redeploy
The Supabase JS SDK reads NEXT_PUBLIC_SUPABASE_URL on initialisation. Every browser-side call now targets your Worker URL automatically, with zero code changes in your application.
Step 3 — Fix Your Content Security Policy
If you have a Content Security Policy header with connect-src, you need to ensure it allows your Worker URL and its WebSocket equivalent (wss:// instead of https://).
The naive approach is to hardcode both URLs:
// ❌ Brittle — requires updating in two places when URL changes
const cspConnectSrc = `${supabaseUrl} wss://*.supabase.co`;
The correct approach derives the WebSocket URL dynamically from whatever NEXT_PUBLIC_SUPABASE_URL is set to:
// ✅ Works for any URL — Supabase direct or Worker proxy
const workerUrl = process.env.NEXT_PUBLIC_SUPABASE_URL ?? "";
const workerWss = workerUrl.replace(/^https:\/\//, "wss://");
const cspConnectSrc = workerUrl ? `${workerUrl} ${workerWss}` : "";
Now, when you change the env var, the CSP updates automatically on the next build. No second place to update.
How the Worker Script Works
Let's walk through the Worker script line by line.
const SUPABASE_ORIGIN = 'https://YOUR_PROJECT_REF.supabase.co';
The real destination. Only configured in one place in the Worker.
const url = new URL(request.url);
const targetUrl = SUPABASE_ORIGIN + url.pathname + url.search;
The browser calls https://supabase-proxy.account.workers.dev/auth/v1/token.
We strip the Worker's origin and reconstruct the target as
https://abcdefgh.supabase.co/auth/v1/token. Path and query string are
preserved exactly.
const headers = new Headers(request.headers);
headers.set('host', new URL(SUPABASE_ORIGIN).host);
This is the critical part. HTTP requests must include a Host header matching the destination server. Without rewriting it, Supabase would receive Host: supabase-proxy.account.workers.dev and reject the request. We rewrite it to abcdefgh.supabase.co.
All other headers — including Authorization, apikey, and Content-Type — pass through untouched.
if (request.headers.get('Upgrade') === 'websocket') {
return fetch(targetUrl, { headers, method: request.method });
}
Supabase Realtime connects via WebSocket. The browser sends an HTTP Upgrade request. We detect it and forward it directly — Cloudflare Workers handle the protocol upgrade natively.
return fetch(targetUrl, {
method: request.method,
headers,
body: ['GET', 'HEAD'].includes(request.method) ? undefined : request.body,
redirect: 'follow',
});
For all other requests: forward method, headers, and body. GET and HEAD cannot have a body per the HTTP spec — passing undefined avoids a runtime error. redirect: 'follow' handles any redirects Supabase issues transparently.
Verification
After deploying:
- Open your app from an Indian network, or use a VPN set to India
- Open DevTools → Network tab
- Look for Supabase-related requests
Before the fix:
❌ GET https://abcdefgh.supabase.co/rest/v1/transactions
Status: (failed) net::ERR_NAME_NOT_RESOLVED
After the fix:
✅ GET https://supabase-proxy.account.workers.dev/rest/v1/transactions
Status: 200 OK
Also verify in the Cloudflare dashboard:
Workers & Pages → supabase-proxy → Metrics
You should see request counts climbing as your users hit the Worker. If you don't, check the Worker logs for any errors.
This Generalises to Any Blocked Service
The Worker doesn't care that it's proxying Supabase. Change SUPABASE_ORIGIN to any blocked backend URL:
// Firebase
const SUPABASE_ORIGIN = 'https://your-project.firebaseio.com';
// PlanetScale
const SUPABASE_ORIGIN = 'https://your-db.us-east.psdb.cloud';
// Any REST API
const SUPABASE_ORIGIN = 'https://api.your-service.com';
Same Worker code, different origin constant. The pattern is universal. You can use this to proxy any blocked service to any non-blocked domain.
Limitations
This is a workaround, not a root fix. The correct fix is for Indian ISPs to unblock Supabase's DNS records, which is outside your control. This proxy adds one network hop (browser → Cloudflare edge → Supabase), which adds a small amount of latency. In practice, Cloudflare's global edge network means the hop is geographically close to both the user and Supabase, so the latency impact is minimal.
The free tier gives you 100,000 requests per day. For most applications, this is more than sufficient. If you exceed it, Cloudflare's paid plan is $5/month for 10 million requests.
The entire fix is:
- A 25-line Cloudflare Worker
- One environment variable change
- A two-line CSP update
Your application code stays untouched.




Top comments (2)
The DNS-level block only hitting browser-side calls is the sneaky part — server components just keep working and you think everything is fine. We ran into a similar ISP-level block with a different service and the Cloudflare Worker proxy approach was the cleanest fix by far.
It worked.. Thanks