Start a Project
All guides

Dev Stack

Supabase from South Africa: latency, regions and caching patterns.

Supabase does not host in South Africa. The closest regions are eu-west-1 (Ireland) and eu-central-1 (Frankfurt), and your cold Postgres query round trip from a Johannesburg fibre line sits around 170–180ms before you add TLS and connection overhead. That is fine for an internal tool; it is painful for a consumer app where a logged-in dashboard fires five queries on mount. This guide is the honest version — we run Supabase on several .co.za products, most visitors come through Vumatel, Openserve or MTN fibre, and the fix is not magic, it is caching reads at Cloudflare's JNB and CPT edges and keeping writes thin. We cover region choice, Supavisor connection pooling, edge function placement and the free-tier traps that will bite a SA indie builder before they even launch.

Updated 15 April 2026 · 11 min read · Supabase · Joshua Kaplan

Prerequisites

  • A Supabase account (free tier is fine to start)
  • Node 20+ for local dev
  • A Cloudflare account if you want edge caching (free plan covers most SA indie apps)
  • Basic Postgres and RLS knowledge

Step 1. Pick Ireland or Frankfurt, not US-East

When creating a new Supabase project, the region picker defaults to US-East. For South African visitors, choose eu-west-1 (Ireland) or eu-central-1 (Frankfurt). Johannesburg → Ireland is ~170ms, JNB → Frankfurt is ~175ms, JNB → US-East is ~235ms. Cape Town numbers are roughly the same. The difference between Ireland and Frankfurt is negligible — pick based on where your other infrastructure lives.

Step 2. Use Supavisor (transaction pooler) from edge runtimes

If you call Supabase from a Cloudflare Worker or Vercel Edge function, do not use the direct Postgres connection string — use the Supavisor pooler on port 6543 with transaction mode. It handles the connection churn edge runtimes create and avoids blowing through Postgres's connection limit on the free tier (60 connections).

.env
# Direct (for server with persistent connections)
DATABASE_URL="postgresql://postgres:xxx@db.xxx.supabase.co:5432/postgres"

# Supavisor transaction mode (for edge / serverless)
DATABASE_POOL_URL="postgresql://postgres.xxx:xxx@aws-0-eu-west-1.pooler.supabase.com:6543/postgres"

Step 3. Set up the client with anon key for the browser

In a React/Astro/Hydrogen frontend, the anon key is public and safe to ship to the browser — RLS policies are your actual security boundary. Never ship the service_role key to the client.

src/lib/supabase.ts
import { createClient } from '@supabase/supabase-js';

export const supabase = createClient(
  import.meta.env.PUBLIC_SUPABASE_URL,
  import.meta.env.PUBLIC_SUPABASE_ANON_KEY,
  { auth: { persistSession: true, autoRefreshToken: true, detectSessionInUrl: true } },
);

Step 4. Cache reads in Cloudflare KV or the edge cache

For read-heavy public data (product catalogues, blog posts, location lists) wrap Supabase reads in a Cloudflare Worker that caches responses in KV with a TTL. A cache hit at the JNB PoP is under 10ms versus 180ms to Frankfurt. This is the single biggest latency win available to SA Supabase users.

worker/src/index.ts
export default {
  async fetch(req: Request, env: Env) {
    const url = new URL(req.url);
    const key = `products:${url.searchParams.get('category') ?? 'all'}`;
    const cached = await env.CACHE.get(key, 'json');
    if (cached) return Response.json(cached, { headers: { 'x-cache': 'HIT' } });

    const res = await fetch(`${env.SUPABASE_URL}/rest/v1/products?select=*`, {
      headers: { apikey: env.SUPABASE_ANON_KEY, Authorization: `Bearer ${env.SUPABASE_ANON_KEY}` },
    });
    const data = await res.json();
    await env.CACHE.put(key, JSON.stringify(data), { expirationTtl: 300 });
    return Response.json(data, { headers: { 'x-cache': 'MISS' } });
  },
};

Step 5. Configure auth redirect URLs for .co.za

In Supabase dashboard → Authentication → URL Configuration, add your production .co.za URL as both the Site URL and an allowed redirect URL. Magic links and OAuth callbacks will silently fail on production if you forget. Localhost is whitelisted by default.

Step 6. Write RLS policies before opening any table

Row Level Security is off by default on new tables. Turn it on, write the policies, then expose the table through PostgREST. A typical SA SaaS pattern: tenant_id column + policy using auth.jwt() ->> 'tenant_id'. This is your only real defence if the anon key is scraped.

alter table public.orders enable row level security;

create policy "tenants read own orders"
  on public.orders for select
  using (tenant_id = (auth.jwt() ->> 'tenant_id')::uuid);

create policy "tenants insert own orders"
  on public.orders for insert
  with check (tenant_id = (auth.jwt() ->> 'tenant_id')::uuid);

Step 7. Keep realtime channels narrow

Realtime runs over WebSockets — the WS handshake from Johannesburg to Ireland is one RTT (~170ms), but the channel stays open afterwards. Subscribe to filtered channels only (filter on tenant_id at minimum), not entire tables. A wide subscription from hundreds of SA users will rack up your free-tier 200 concurrent connection cap fast.

Step 8. Monitor the free-tier ceilings

Supabase free tier: 500MB database, 1GB file storage, 50k monthly active users on auth, 200 concurrent realtime connections, 500k edge function invocations. A small .co.za indie project will not hit these for months. A B2B app with 20 SA tenants each hitting the dashboard hourly can exhaust realtime connections in a day — budget for Pro ($25/month USD) sooner than you expect.

SA gotchas

  • Read replicas are Pro-plan-only. On free tier, every read hits the primary, so the 170–180ms round trip is unavoidable without an edge cache in front.
  • Connection pooling mode matters: transaction mode (port 6543) for edge/serverless, session mode (port 5432) for long-lived servers. Mixing them up causes prepared statement errors that look like a driver bug — they are not.
  • Supabase Edge Functions run on Deno Deploy, which has global PoPs including a Johannesburg one — so function execution can be sub-20ms, but the function calling Postgres still round-trips to Ireland or Frankfurt. Edge functions are not a latency silver bullet on their own.
  • Auth session refresh fires every hour on an idle tab. If your app is left open overnight by an SA user on ADSL or LTE, the refresh can fail silently — wrap the refresh listener and surface a reconnect UI.
  • Free-tier databases pause after 7 days of inactivity. The first request after pause takes 5–10s while it spins up. For low-traffic .co.za side projects, either upgrade to Pro or run a daily cron ping from a Cloudflare Worker to keep it warm.
  • Postgres row count on free tier is soft-capped — you will get email warnings before enforcement. Plan your ingestion before you scrape 5M rows into a table and lose write access.

Frequently asked questions

Does Supabase have a South Africa region?

No. As of 2026, Supabase hosts in AWS regions including eu-west-1 (Ireland) and eu-central-1 (Frankfurt). Either is ~170–180ms round trip from Johannesburg and Cape Town. There is no announced SA region on the roadmap.

What latency should I expect for a South African user?

Cold query from Johannesburg to Ireland: 170–180ms one-way, ~350ms for a full round trip with TLS. Cape Town is within 5ms of Johannesburg in either direction. First render of a dashboard doing 3–5 queries in parallel lands around 500–700ms total — acceptable for logged-in apps, sluggish for public pages. Cache public reads at the Cloudflare edge to fix this.

Should I self-host Supabase in South Africa instead?

Only if you have genuine regulatory pressure. Self-hosting Supabase means running Postgres, PostgREST, GoTrue, Storage, Realtime and Studio yourself on a Cape Town VPS — probably R600–2000/month on Afrihost Cloud or Xneelo Turbo, plus your time. For most SA builders, managed Supabase in Ireland + Cloudflare edge caching beats self-hosting.

Can I use Supabase with Cloudflare Workers from South Africa?

Yes, and it is the recommended pattern for public pages. Run a Worker at the Cloudflare edge (JNB/CPT PoP), cache read responses in KV with a short TTL, pass writes through to Supabase directly. SA users see cache hits in under 15ms; writes still cost the Ireland round trip but those are rarer.

Is POPIA a problem with data in Ireland or Frankfurt?

POPIA (Section 72) allows cross-border transfer if the receiving jurisdiction has comparable data protection law — the EU GDPR qualifies. Document the transfer in your processing records, mention it in your privacy notice, and you are compliant. This is not legal advice; check with a POPIA-competent attorney for regulated industries.