Supabase’s Postgres database is actually a distributed system, and Fly.io’s platform is designed to run that database close to your users. Cloudflare Workers are the key to making it feel like a single, fast, global database.

Here’s a Supabase Postgres instance running on Fly.io, with a Cloudflare Worker acting as the edge access layer.

{
  "services": [
    {
      "name": "supa-db-your-app",
      "image": "supabase/postgres:15",
      "mount": {
        "source": "data",
        "destination": "/data"
      },
      "env": {
        "POSTGRES_PASSWORD": "YOUR_SECURE_PASSWORD",
        "POSTGRES_USER": "postgres",
        "POSTGRES_DB": "supabase_db"
      },
      "ports": {
        "5432": "5432"
      },
      "processes": ["app"],
      "checks": {
        "pg_up": {
          "type": "tcp",
          "port": 5432,
          "grace_period": "10s"
        }
      },
      "restart": {
        "policy": "always"
      }
    }
  ],
  "deploy": {
    "regions": ["lhr", "sfo", "iad", "sin"],
    "strategy": "rolling"
  }
}

This Fly.io configuration defines a Supabase Postgres service. regions specifies where your database could be deployed, and strategy: "rolling" ensures updates don’t cause downtime. The mount directive is crucial: it tells Fly.io to persist the /data directory, which is where Postgres stores its data files. Without this, your database would reset on every restart.

Now, let’s look at the Cloudflare Worker. This worker will intercept incoming requests, authenticate them, and then proxy them to the appropriate Fly.io Postgres instance.

// index.js (Cloudflare Worker)
addEventListener('fetch', event => {
  event.respondWith(handleRequest(event.request));
});

async function handleRequest(request) {
  const url = new URL(request.url);
  const pathSegments = url.pathname.split('/').filter(Boolean); // Remove empty segments

  // Example: /api/v1/users -> route to us-east-1 instance for users table
  if (pathSegments.length >= 3 && pathSegments[0] === 'api' && pathSegments[1] === 'v1') {
    const region = getRegionFromPath(pathSegments[2]); // 'users' table maps to 'iad' region
    const targetUrl = `https://${region}.fly.dev/`; // Or your specific Fly.io service name

    // Basic authentication check (replace with your actual auth logic)
    const authHeader = request.headers.get('Authorization');
    if (!authHeader || authHeader !== 'Bearer YOUR_SECRET_TOKEN') {
      return new Response('Unauthorized', { status: 401 });
    }

    // Construct the new request to forward to Fly.io
    const newRequest = new Request(targetUrl + pathSegments.slice(2).join('/'), {
      method: request.method,
      headers: request.headers,
      body: request.body,
      redirect: 'follow'
    });

    // Fetch from Fly.io and return the response
    const response = await fetch(newRequest);
    return response;
  }

  return new Response('Not Found', { status: 404 });
}

function getRegionFromPath(tableName) {
  // Simple mapping: In a real app, this would be more sophisticated (e.g., based on user location or data sharding)
  const regionMap = {
    'users': 'iad',
    'products': 'sfo',
    'orders': 'lhr'
  };
  return regionMap[tableName.toLowerCase()] || 'iad'; // Default to one region
}

This worker acts as a smart router. It inspects the incoming request URL. If it matches a pattern like /api/v1/<resource>, it determines which Fly.io region is best to serve that request. This isn’t just about latency; it’s about data locality. If your users are primarily in Europe, you want their data requests to hit the Postgres instance in lhr. The worker handles the authentication and then forwards the request to the correct Fly.io endpoint.

The real magic is how Supabase and Fly.io work together for distributed Postgres. Fly.io’s regions setting allows you to deploy your Postgres instance in multiple geographic locations. When a user in, say, Singapore makes a request, your Cloudflare Worker can detect this (perhaps by geo-IP lookup or by routing based on the pathSegments) and direct the request to the Supabase instance deployed in the sin region. This drastically reduces latency because the data travels a much shorter distance.

The mount directive in the Fly.io configuration is absolutely critical. It ensures that the /data directory, where Postgres stores all its tables, indexes, and transaction logs, is persisted across container restarts or updates. Without this, every time your Fly.io instance restarts, it would be like booting up a brand new, empty database server. This persistence is what makes the Fly.io Postgres instance behave like a real, stateful database.

To make this truly distributed and fault-tolerant, you’d typically use Supabase’s replication features within Fly.io. While the example above shows a single instance, you’d configure read replicas in different regions. Your Cloudflare Worker would then intelligently route read requests to the closest replica and write requests to a primary instance (potentially using a consensus mechanism or a designated primary region). The worker’s getRegionFromPath function is a placeholder for this sophisticated routing logic.

The grace_period in the pg_up check is important for database deployments. Postgres can take a little while to fully initialize and become ready to accept connections after a restart, especially if it’s recovering from a crash or applying WAL (Write-Ahead Logging) entries. The grace period gives Postgres time to start up without Fly.io incorrectly assuming the service is unhealthy and restarting it repeatedly.

One of the most powerful aspects of this setup is the ability to control data residency. By strategically placing your Fly.io Postgres instances in specific regions and having your Cloudflare Worker route traffic accordingly, you can ensure that user data stays within certain geographic boundaries to comply with regulations like GDPR. The worker becomes your global traffic cop, enforcing data governance policies at the edge.

The next hurdle you’ll likely face is implementing robust replication and failover strategies for your Postgres instances on Fly.io, and refining your Cloudflare Worker’s routing logic to handle complex distributed database patterns.

Want structured learning?

Take the full Supabase course →