DEV Community

Ritvik Dayal
Ritvik Dayal

Posted on

Supabase is Blocked in India. Here's the Free Fix Using a Cloudflare Worker

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.

How Supabase is blocked

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.

nextjs application fails to communicate to supabase

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.

introduction of cloudflare worker

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

after fix architecture

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

  1. Go to dash.cloudflare.com
  2. In the left sidebar click ComputeWorkers & Pages
  3. Click CreateCreate Worker
  4. Name it supabase-proxy
  5. Click Deploy (this deploys a hello-world placeholder)
  6. Click Edit code
  7. 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',
    });
  },
};
Enter fullscreen mode Exit fullscreen mode
  1. Replace YOUR_PROJECT_REF with your actual Supabase project reference ID. Find it at: Supabase Dashboard → Project Settings → General → Reference ID It looks like abcdefghijklmnop.

  2. Click Deploy

  3. 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
Enter fullscreen mode Exit fullscreen mode

After:

NEXT_PUBLIC_SUPABASE_URL=https://supabase-proxy.<your-account>.workers.dev
Enter fullscreen mode Exit fullscreen mode

All other environment variables stay identical:

NEXT_PUBLIC_SUPABASE_ANON_KEY=eyJ...          # unchanged
SUPABASE_SERVICE_ROLE_KEY=eyJ...              # unchanged
Enter fullscreen mode Exit fullscreen mode

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`;
Enter fullscreen mode Exit fullscreen mode

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}` : "";
Enter fullscreen mode Exit fullscreen mode

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';
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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 });
}
Enter fullscreen mode Exit fullscreen mode

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',
});
Enter fullscreen mode Exit fullscreen mode

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:

  1. Open your app from an Indian network, or use a VPN set to India
  2. Open DevTools → Network tab
  3. Look for Supabase-related requests

Before the fix:

❌ GET https://abcdefgh.supabase.co/rest/v1/transactions
   Status: (failed) net::ERR_NAME_NOT_RESOLVED
Enter fullscreen mode Exit fullscreen mode

After the fix:

✅ GET https://supabase-proxy.account.workers.dev/rest/v1/transactions
   Status: 200 OK
Enter fullscreen mode Exit fullscreen mode

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';
Enter fullscreen mode Exit fullscreen mode

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:

  1. A 25-line Cloudflare Worker
  2. One environment variable change
  3. A two-line CSP update

Your application code stays untouched.

Top comments (2)

Collapse
 
klement_gunndu profile image
klement Gunndu

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.

Collapse
 
nithinbharathi profile image
Nithinbharathi

It worked.. Thanks