← Back to blog · Tutorial 12 min read

How to Proxy Supabase Through Cloudflare Workers (Step-by-Step)

Indian ISPs are DNS-blocking *.supabase.co, breaking every app that relies on Supabase for auth, database, storage, or realtime. In this tutorial, you will build a Cloudflare Worker reverse proxy from scratch that routes all Supabase traffic through your own unblocked domain.

1. Why you need a Supabase proxy

If you are building with Supabase and your users are in India, your app is likely broken right now. Multiple major ISPs - Jio, Airtel, ACT Fibernet, and others - are DNS-blocking *.supabase.co at the network level. Every REST query, auth call, storage upload, and realtime subscription fails with ERR_CONNECTION_TIMED_OUT.

ISP DNS blocks

Jio, Airtel, and ACT resolve *.supabase.co to sinkhole IPs via DNS poisoning. Changing to 1.1.1.1 does not help if DPI is active.

Infrastructure resilience

A proxy layer decouples your app from Supabase's domain. If blocks happen again, your users never notice.

Custom domains

Serve Supabase APIs from your own domain like api.yourapp.com. Professional branding and no third-party domain exposure.

A reverse proxy sits between your frontend and Supabase. Instead of calling xyz.supabase.co, your client calls your-domain.com. The proxy Worker receives the request, forwards it to Supabase, and relays the response back. Because your-domain.com is not blocked, everything works.

2. Architecture overview

The request flow is straightforward. Your browser talks to your domain, Cloudflare's edge network runs your Worker, and the Worker talks to Supabase on the backend.

Browser
your-domain.com
CF Worker
*.supabase.co

The Worker handles three types of traffic: standard HTTP requests (REST, Auth, Storage), CORS preflight requests (OPTIONS), and WebSocket upgrade requests (Realtime). Each has slightly different handling, which we will implement step by step.

3. Prerequisites

Before you start, make sure you have the following:

  • A Cloudflare account (free tier works). Sign up at dash.cloudflare.com.
  • A Supabase project with a project URL like xyz.supabase.co.
  • Node.js 18+ installed locally.
  • The Wrangler CLI installed: npm install -g wrangler
  • A custom domain added to your Cloudflare account (so you can route traffic through it).

4. Step 1: Create the Worker project

Scaffold a new Cloudflare Worker project using Wrangler:

terminal
# Create a new Worker project
npm create cloudflare@latest -- supabase-proxy
# Select: "Hello World" Worker, TypeScript
cd supabase-proxy

Next, configure wrangler.toml with your Supabase project URL and custom domain:

wrangler.toml
name = "supabase-proxy"
main = "src/index.ts"
compatibility_date = "2024-12-01"
# Your Supabase project URL
[vars]
SUPABASE_URL = "https://xyz.supabase.co"
ALLOWED_ORIGINS = "https://yourapp.com,http://localhost:3000"
# Route traffic from your custom domain
[[routes]]
pattern = "api.yourapp.com/*"
zone_name = "yourapp.com"

Replace xyz.supabase.co with your actual Supabase project reference and yourapp.com with your own domain.

5. Step 2: Write the proxy handler

The core of the proxy is an HTTP handler that receives incoming requests, rewrites them to point at your Supabase origin, forwards them, and relays the response back with proper headers. Create src/index.ts:

src/index.ts
interface Env {
SUPABASE_URL: string;
ALLOWED_ORIGINS: string;
}
export default {
async fetch(request: Request, env: Env): Promise<Response> {
const url = new URL(request.url);
// Health check endpoint
if (url.pathname === "/__health") {
return Response.json({ status: "ok" });
}
// Handle CORS preflight
if (request.method === "OPTIONS") {
return handlePreflight(request, env);
}
// Check for WebSocket upgrade
const upgrade = request.headers.get("Upgrade");
if (upgrade?.toLowerCase() === "websocket") {
return handleWebSocket(request, env);
}
// Proxy the HTTP request
return handleHttp(request, env);
},
};

Now add the handleHttp function that does the actual proxying. This is the heart of the Worker:

src/index.ts (continued)
async function handleHttp(
request: Request,
env: Env
): Promise<Response> {
const url = new URL(request.url);
// Build the upstream Supabase URL
const upstream = new URL(env.SUPABASE_URL);
upstream.pathname = url.pathname;
upstream.search = url.search;
// Clone headers, rewrite Host
const headers = new Headers(request.headers);
headers.set("Host", upstream.hostname);
// Remove Cloudflare-specific headers
headers.delete("cf-connecting-ip");
headers.delete("cf-ray");
headers.delete("cf-visitor");
headers.delete("cf-ipcountry");
// Forward the request to Supabase
const resp = await fetch(upstream.toString(), {
method: request.method,
headers,
body: request.method !== "GET"
&& request.method !== "HEAD"
? request.body : undefined,
redirect: "manual",
});
// Clone response and add CORS
const respHeaders = new Headers(resp.headers);
addCorsHeaders(respHeaders, request, env);
respHeaders.set("X-Proxied-By", "supabase-proxy");
// Rewrite Location header on redirects
const loc = respHeaders.get("Location");
if (loc?.includes(".supabase.co")) {
respHeaders.set("Location",
loc.replace(upstream.hostname, url.hostname));
}
return new Response(resp.body, {
status: resp.status,
statusText: resp.statusText,
headers: respHeaders,
});
}

Key detail: Host header rewriting

You must set the Host header to Supabase's hostname. Without this, Supabase will reject the request because the Host header would be your custom domain, which Supabase does not recognize.

6. Step 3: Add WebSocket support

Supabase Realtime uses WebSockets. To proxy them, you need to detect WebSocket upgrade requests, open a connection to Supabase's WebSocket server, and relay messages in both directions using Cloudflare's WebSocketPair API.

src/index.ts (WebSocket handler)
async function handleWebSocket(
request: Request,
env: Env
): Promise<Response> {
const url = new URL(request.url);
const upstream = new URL(env.SUPABASE_URL);
upstream.pathname = url.pathname;
upstream.search = url.search;
// Convert https: to wss: for WebSocket
const wsUrl = upstream.toString()
.replace("https:", "wss:")
.replace("http:", "ws:");
// Connect to Supabase WebSocket
const upResp = await fetch(wsUrl, {
headers: request.headers,
});
const upWs = upResp.webSocket;
if (!upWs) {
return new Response(
"WebSocket connection failed", { status: 502 }
);
}
upWs.accept();
// Create client-side WebSocket pair
const [client, server] =
Object.values(new WebSocketPair());
server.accept();
// Relay messages bidirectionally
upWs.addEventListener("message", (e) => {
try { server.send(e.data); }
catch { /* client gone */ }
});
server.addEventListener("message", (e) => {
try { upWs.send(e.data); }
catch { /* upstream gone */ }
});
// Relay close events
upWs.addEventListener("close", (e) => {
try { server.close(e.code, e.reason); }
catch { }
});
server.addEventListener("close", (e) => {
try { upWs.close(e.code, e.reason); }
catch { }
});
return new Response(null, {
status: 101,
webSocket: client,
});
}

This creates a WebSocketPair - one end (client) goes back to the browser, the other (server) stays in the Worker. Every message from Supabase gets relayed to the browser and vice versa. Close and error events are also forwarded to ensure clean disconnects.

7. Step 4: Configure CORS

Browsers send a CORS preflight (OPTIONS request) before any cross-origin API call. Your proxy needs to handle these correctly or every request from your frontend will be blocked by the browser itself.

src/index.ts (CORS handlers)
function isOriginAllowed(
origin: string,
allowed: string
): boolean {
if (allowed === "*") return true;
return allowed.split(",")
.map(o => o.trim())
.includes(origin);
}
function handlePreflight(
request: Request,
env: Env
): Response {
const origin = request.headers.get("Origin") || "*";
const headers = new Headers();
if (isOriginAllowed(origin, env.ALLOWED_ORIGINS)) {
headers.set("Access-Control-Allow-Origin", origin);
headers.set("Access-Control-Allow-Credentials", "true");
headers.set("Access-Control-Allow-Methods",
"GET, POST, PUT, PATCH, DELETE, OPTIONS");
headers.set("Access-Control-Allow-Headers",
request.headers.get("Access-Control-Request-Headers") || "*");
headers.set("Access-Control-Max-Age", "86400");
}
return new Response(null, { status: 204, headers });
}
function addCorsHeaders(
respHeaders: Headers,
request: Request,
env: Env
): void {
const origin = request.headers.get("Origin");
if (origin && isOriginAllowed(origin, env.ALLOWED_ORIGINS)) {
respHeaders.set("Access-Control-Allow-Origin", origin);
respHeaders.set("Access-Control-Allow-Credentials", "true");
respHeaders.set("Access-Control-Expose-Headers", "*");
}
}

Security note: lock down origins

Avoid setting ALLOWED_ORIGINS to "*" in production. Specify your exact frontend domains to prevent other sites from using your proxy. You can comma-separate multiple origins in the environment variable.

8. Step 5: Deploy and configure DNS

Deploy the Worker to Cloudflare with a single command:

terminal
# Login to Cloudflare (first time only)
wrangler login
# Deploy the Worker
wrangler deploy

After deployment, configure DNS in your Cloudflare dashboard. Create a DNS record that points your subdomain to the Worker:

TypeNameContentProxy
AAAAapi100::Proxied

The AAAA record with 100:: is a Cloudflare-specific placeholder for Worker routes - it tells Cloudflare to run your Worker for that hostname. Make sure the orange cloud (Proxied) is enabled. Your Worker should now be live at api.yourapp.com.

9. Step 6: Update your Supabase client code

The only change in your frontend is the Supabase URL. Replace the default *.supabase.co URL with your custom proxy domain:

lib/supabase.ts
import { createClient } from '@supabase/supabase-js'
// Before (blocked on Indian ISPs):
// const supabaseUrl = 'https://xyz.supabase.co'
// After (routed through your proxy):
const supabaseUrl = 'https://api.yourapp.com'
const supabaseKey = 'your-anon-key'
export const supabase = createClient(
supabaseUrl,
supabaseKey
)

That is it. The Supabase client library does not care what domain serves the API - as long as the endpoints respond with the expected Supabase format. Your anon key stays the same. All auth, database, storage, and realtime functionality works exactly as before.

Tip: use an environment variable

Store the URL in an environment variable like VITE_SUPABASE_URL or NEXT_PUBLIC_SUPABASE_URL so you can switch between direct and proxied URLs per environment without changing code.

10. Testing the proxy

Before deploying to production, verify each layer of the proxy works. Use these curl commands:

terminal
# 1. Health check
curl https://api.yourapp.com/__health
# Expected: {"status":"ok"}
# 2. Test REST API (list tables)
curl https://api.yourapp.com/rest/v1/ \
-H "apikey: YOUR_ANON_KEY" \
-H "Authorization: Bearer YOUR_ANON_KEY"
# 3. Test CORS preflight
curl -X OPTIONS https://api.yourapp.com/rest/v1/ \
-H "Origin: https://yourapp.com" \
-H "Access-Control-Request-Method: POST" \
-v 2>&1 | grep -i access-control
# Expected: Access-Control-Allow-Origin: https://yourapp.com
# 4. Test Auth endpoint
curl https://api.yourapp.com/auth/v1/settings \
-H "apikey: YOUR_ANON_KEY"
# 5. Verify proxy header
curl -I https://api.yourapp.com/rest/v1/ \
-H "apikey: YOUR_ANON_KEY" | grep X-Proxied-By
# Expected: X-Proxied-By: supabase-proxy

To test WebSocket/Realtime, open your browser DevTools and check the Network tab for ws:// connections. You should see WebSocket frames flowing through your proxy domain. If any test fails, check your wrangler tail logs:

terminal
# Stream live Worker logs
wrangler tail --format pretty

11. Or just use JioBase

Building and maintaining a Supabase proxy is completely doable - but it takes time. You need to handle CORS edge cases, WebSocket reliability, rate limiting, monitoring, and keep the Worker updated as Supabase evolves its API. If you would rather skip all that and get a production-ready proxy in 60 seconds, that is exactly what JioBase does.

What you get with JioBase

  • Full HTTP + WebSocket proxy
  • CORS configuration dashboard
  • Custom subdomains (myapp.jiobase.com)
  • Real-time analytics and monitoring
  • Rate limiting built-in
  • Custom domain support (Pro plan)
  • Cloudflare's 300+ edge locations
  • Free tier: 50k requests/month

Setup is literally one line of code:

lib/supabase.ts
import { createClient } from '@supabase/supabase-js'
export const supabase = createClient(
//'https://xyz.supabase.co'
'https://xyz.jiobase.com',
'your-anon-key'
)

Frequently Asked Questions

Do I need a paid Cloudflare plan?
No. The Cloudflare Workers free tier includes 100,000 requests per day, which is sufficient for most development and production apps. You only need a paid plan if you exceed that limit.
Will Supabase Auth (cookies, sessions) work through the proxy?
Yes. The proxy forwards all headers, cookies, and request bodies unchanged. Supabase Auth (sign up, sign in, OAuth, magic links, password reset) works exactly the same through the proxy. Make sure your CORS configuration includes your frontend domain.
Can I use this with Supabase Storage file uploads?
Yes. File uploads, downloads, signed URLs, and bucket management all work through the proxy. The Worker streams request and response bodies, so even large file uploads are handled efficiently.
How much latency does the proxy add?
Typically 1-5ms. Cloudflare Workers execute at the edge location closest to your users (300+ global locations), so the additional hop is minimal. This is significantly less than a VPN (50-200ms+).
Can I skip the manual setup and use a managed solution?
Yes. JioBase provides a managed Supabase reverse proxy with all the features described in this tutorial, plus a dashboard, analytics, rate limiting, and automatic updates. Free tier includes 50,000 requests/month.
Want a quick start without Wrangler CLI?
Try our Worker Generator Tool. Enter your Supabase URL, copy the generated code, and paste it directly into Cloudflare's dashboard editor. No CLI needed.

Summary

In this tutorial, you built a complete Cloudflare Worker reverse proxy for Supabase that handles HTTP API requests, WebSocket Realtime connections, and CORS preflight. The key steps were:

  1. 1 Scaffold the Worker with wrangler and configure your Supabase URL.
  2. 2 Write the HTTP proxy handler with Host header rewriting and redirect rewriting.
  3. 3 Add WebSocket support using WebSocketPair for Supabase Realtime.
  4. 4 Handle CORS preflight and response headers for allowed origins.
  5. 5 Deploy to Cloudflare and configure DNS routing for your custom domain.
  6. 6 Swap the Supabase URL in your client code and test.

If you prefer a managed solution that handles all of this automatically - plus analytics, rate limiting, and a dashboard - check out JioBase. Free tier includes 50,000 requests per month.

Sunith VS

Written and verified by

Sunith VS

Building tools that help Indian developers ship without ISP interference. Creator of JioBase.