QuotaGuard and Supabase Network Restrictions Integration Guide
Table of contents
- The Problem: Network Restrictions Block Dynamic IPs
- Native Supabase Options (What They Cover)
- Getting Started
- Choose Your Connection Route First
- Configuring Supabase Network Restrictions
- Configuring Your Application
- Using QGTunnel
- Direct SOCKS5 Configuration
- Testing Your Implementation
- Latency Considerations
- Troubleshooting
- Connection times out, no authentication error
- Connection works intermittently, fails at other times
password authentication failed- SOCKS authentication failed
- TLS certificate verification fails after adding the tunnel
- The Network Restrictions section is missing in the dashboard
- A CLI update wiped an existing allowlist entry
ECONNREFUSED 127.0.0.1- Edge Functions stopped reaching the database after enabling restrictions
- Environment Variables Reference
- Security Best Practices
- QuotaGuard Static vs QuotaGuard Shield
- Ready to Get Started?
QuotaGuard Static IPs let your cloud application connect to a Supabase Postgres database that has Network Restrictions enabled. You allowlist the two static IPs assigned to your subscription, and every connection from your app arrives from a known address regardless of which platform it runs on.
You do not need QuotaGuard for local development or for infrastructure with stable IPs. This guide is for applications on Heroku, Render, Railway, Fly.io, Vercel, AWS Lambda, Google Cloud Run, Kubernetes, and other platforms where outbound IPs are dynamic.
This is the inbound direction: your app connecting into Supabase. If you are looking for outbound traffic from a Supabase Edge Function to an external API, see the Supabase Edge Functions guide.
The Problem: Network Restrictions Block Dynamic IPs
Supabase Network Restrictions let you specify which IP ranges may connect to your Postgres database and its connection pooler. The restrictions are enforced before traffic reaches the database, and before any credential check. A connection from a disallowed address is refused regardless of whether the password is correct.
That is exactly the control a security review or a SOC 2 questionnaire asks for. It is also unusable on a platform with rotating egress IPs, because there is no stable CIDR to enter.
When your app connects from an address outside the allowlist, you see a connection failure rather than an authentication failure:
Error: connect ETIMEDOUT
psycopg2.OperationalError: connection to server at "db.xxxxxxxx.supabase.co", port 5432 failed:
Connection timed out
Is the server running on that host and accepting TCP/IP connections?
PG::ConnectionBad: connection to server failed: Connection timed out
The tell is that the error is a timeout or a refused connection, not password authentication failed. A credential error means you reached the database. A timeout at this layer usually means the allowlist rejected you first.
The Common Workaround
The usual response is to allowlist 0.0.0.0/0, or to never turn Network Restrictions on at all.
That leaves your Postgres instance reachable from any address on the internet, with username and password as the only barrier. It also means the control your compliance reviewer asked about is not actually in place, whatever the policy document says.
The QuotaGuard Solution
Route your Postgres connections through QuotaGuard’s SOCKS5 proxy. Traffic exits from one of the two static IP addresses assigned to your subscription. Add those two addresses to your Supabase allowlist and remove the wildcard.
- Network-level protection is real, not nominal
- The same two IPs work from every platform in your stack
- Moving from Heroku to Render, or adding a worker on Fly.io, changes nothing on the Supabase side
- Your allowlist stops being a maintenance burden
Native Supabase Options (What They Cover)
Supabase offers its own networking features. Some of them fit certain architectures better than a proxy does.
| Option | Requirements | Best for |
|---|---|---|
| Dedicated IPv4 Address | Paid add-on | Giving your database a stable inbound IPv4 address. Does not give your application a stable outbound address. |
| PrivateLink | Enterprise, AWS VPC Lattice | Teams already running in AWS who want to remove public internet exposure entirely |
| Network Restrictions alone | Any paid project | Applications that already run on infrastructure with fixed egress IPs |
The Dedicated IPv4 add-on is worth understanding clearly, because it is often confused with what QuotaGuard does. It fixes the address of your database. It does nothing about the address your application connects from, which is the side the allowlist checks.
Use QuotaGuard when:
- Your application runs on a platform with dynamic egress IPs and you cannot enumerate them
- Your stack spans several platforms and you want one allowlist entry pair for all of them
- You use serverless functions where a VPC configuration adds cold start latency
- You want Network Restrictions on today without an infrastructure project
- You need the same static identity for other destinations too, not just Supabase
Getting Started
After creating a QuotaGuard account, you will be redirected to your dashboard, where you can find your proxy credentials and the two static IP addresses assigned to your subscription.
Choose the right proxy region. Match your QuotaGuard region to the AWS region hosting your Supabase project. Postgres is latency-sensitive on connection setup and on every round trip, so this matters more here than it does for a single API call.
QuotaGuard proxies run in 12 AWS regions:
| Region | Location |
|---|---|
| US-East-1 | N. Virginia |
| US-East-2 | Ohio |
| US-West-2 | Oregon |
| CA-Central-1 | Montreal |
| EU-West-1 | Ireland |
| EU-West-2 | London |
| EU-Central-1 | Frankfurt |
| AP-Northeast-1 | Tokyo |
| AP-Southeast-1 | Singapore |
| AP-Southeast-2 | Sydney |
| AP-South-1 | Mumbai |
| SA-East-1 | Sao Paulo |
To find your Supabase project region, open Project Settings > General in the Supabase dashboard. Pick the matching QuotaGuard region, or the nearest one available, from the region list in your dashboard.
Your QuotaGuard SOCKS5 credentials look like this:
Host: <region>-static-01.quotaguard.com
Port: 1080
Username: your-username
Password: your-password
For example, us-east-static-01.quotaguard.com. Use the exact host and credentials shown in your QuotaGuard dashboard rather than assembling a hostname by hand.
Finding your static IPs: both addresses are shown in the QuotaGuard dashboard. Allowlist both. The dashboard pair is the single source of truth. Do not resolve the proxy hostname to obtain an address, and do not allowlist only one of the two.
Choose Your Connection Route First
Supabase exposes more than one way to reach the same database, and the choice affects whether an IPv4 allowlist entry will work. Get this right before you configure anything else.
Network restrictions apply to Postgres and to the connection pooler, on both pooled and direct routes. They do not apply to the HTTPS APIs, which means PostgREST, Storage, and Auth, and they do not apply to supabase-js calls against those APIs. If your application only uses supabase-js against the REST API, Network Restrictions do not affect you and you do not need this guide.
The IPv6 consideration. If direct connections to your database resolve to an IPv6 address, Supabase’s documentation states you must add both IPv4 and IPv6 CIDRs to the allowlist, with two exceptions: projects on the IPv6 migration, and projects with the IPv4 add-on. QuotaGuard static IPs are IPv4. That gives you three workable paths:
| Route | Works with an IPv4 allowlist entry | Notes |
|---|---|---|
| Supavisor pooler | Yes | The recommended route for most applications. IPv4-reachable. |
| Direct connection with the IPv4 add-on | Yes | Requires the paid Supabase add-on |
| Direct connection resolving to IPv6 | No | Your QuotaGuard IPv4 pair cannot satisfy an IPv6 source requirement |
Unless you have a specific reason to use a direct connection, use the pooler connection string. Copy the exact string from Project Settings > Database > Connection string in the Supabase dashboard and pick the pooler tab rather than the direct tab.
Supabase’s pooler generally exposes session mode and transaction mode on separate ports, commonly 5432 and 6543 respectively. Read the port from your own connection string rather than assuming, because this is the value you will use for the QGTunnel local port mapping below.
Configuring Supabase Network Restrictions
Step 1: Open Network Restrictions
- Open your Supabase project
- Go to Project Settings > Database
- Find the Network Restrictions section
If the section is not there, Supabase’s documentation says to update your Postgres version under Infrastructure Settings first.
Step 2: Add Your QuotaGuard IPs
Add both static IPs from your QuotaGuard dashboard as /32 entries:
203.0.113.10/32
203.0.113.11/32
Substitute the actual addresses shown in your dashboard. A /32 mask means exactly that one address.
If your application also connects from other known locations, an office network or a CI runner, add those CIDRs in the same pass. Anything not on the list is refused.
Step 3: Verify Before You Lock Down
Do not remove an existing permissive entry until you have confirmed a successful connection through the proxy. Add the QuotaGuard pair, deploy the proxy configuration, confirm the application connects, and only then tighten the list.
Configuring with the CLI
The Supabase CLI can read and set restrictions.
Check what is currently in effect:
supabase network-restrictions --project-ref <your-project-ref> get --experimental
If restrictions have never been applied, the allowed CIDR list is empty and the output reports that restrictions were not applied successfully, which means all addresses can connect:
DB Allowed CIDRs: []
Restrictions applied successfully: false
Apply your restrictions:
supabase network-restrictions --project-ref <your-project-ref> update \
--db-allow-cidr 203.0.113.10/32 \
--db-allow-cidr 203.0.113.11/32 \
--experimental
The CIDRs you pass replace the entire existing list. They are not merged. To keep an existing entry, include it in the same command alongside the new ones. This is the most common way people lock themselves out during a change.
Configuring Your Application
Postgres uses its own wire protocol on a TCP port. This needs a SOCKS5 proxy, not an HTTP proxy. QuotaGuard’s SOCKS5 service runs on port 1080.
There is an important practical difference from HTTP-based integrations. Most Postgres drivers have no proxy option at all, because they wrap libpq or open a raw socket directly. Monkey-patching Python’s socket module with PySocks does not reach psycopg2, because libpq opens its socket in C and never passes through Python’s socket layer.
For that reason QGTunnel is the primary path for Postgres, and direct SOCKS5 configuration is the exception rather than the rule. Use the table below to pick.
| Language or stack | Recommended approach |
|---|---|
Node.js (pg, Prisma, Sequelize, TypeORM, Knex) |
QGTunnel, or a custom stream on pg |
Python (psycopg2, psycopg, SQLAlchemy, Django) |
QGTunnel |
| Ruby (ActiveRecord, Sequel) | QGTunnel |
Go (pgx, lib/pq) |
Direct SOCKS5 with a custom dialer, or QGTunnel |
| Java (JDBC) | QGTunnel |
| PHP (PDO) | QGTunnel |
| Elixir (Ecto, Postgrex) | QGTunnel |
Using QGTunnel
QGTunnel is a wrapper process that maps a local port to a remote destination through the QuotaGuard proxy. Your application connects to localhost. No driver support for proxies is required, and in most cases no application code changes are required either.
Step 1: Download QGTunnel
curl https://s3.amazonaws.com/quotaguard/qgtunnel-latest.tar.gz | tar xz
This creates bin/qgtunnel and supporting files under vendor/nss_wrapper/.
The QGTunnel binary targets Linux. If you develop on macOS or Windows, test tunnel configuration inside a Linux container or VM so your local environment matches production.
Step 2: Create the Tunnel in Your Dashboard
In your QuotaGuard dashboard, go to Setup (the gear icon, top-right) > QGTunnel Configuration > Create a Tunnel.
Configure it against your Supabase host:
| Setting | Value |
|---|---|
| Remote Destination | tcp://<your-supabase-host>:<port> |
| Local Port | the same port your application expects |
| Transparent | false |
| Encrypted | false |
Take the host and port from your Supabase connection string, not from this page. Postgres connections to Supabase are already TLS-encrypted end to end, so the tunnel’s own encryption option is not needed here.
If you use transparent mode instead, QGTunnel overrides DNS for the Supabase hostname and routes it locally, which lets you keep your original connection string untouched. That is the better option when the hostname must stay the same, for example when TLS certificate verification would fail against localhost.
Step 3: Download the Configuration File
In the dashboard, download the configuration file and save it as .qgtunnel in your project root, then commit it. With the file present, your application does not depend on reaching the QuotaGuard API at startup.
Step 4: Point Your Application at the Tunnel
In non-transparent mode, change the host in your connection string to localhost:
Original:
postgresql://postgres.<ref>:<password>@<your-supabase-host>:6543/postgres
Through the tunnel:
postgresql://postgres.<ref>:<password>@localhost:6543/postgres
If your driver verifies the TLS certificate against the hostname, connecting to localhost will fail verification. Use transparent mode in that case so the original hostname is preserved.
Step 5: Run Your Application Under QGTunnel
Local or generic:
bin/qgtunnel node server.js
Heroku (Procfile):
web: bin/qgtunnel node server.js
Render (start command):
bin/qgtunnel npm start
Railway (start command):
bin/qgtunnel npm start
Fly.io (fly.toml):
[processes]
app = "bin/qgtunnel npm start"
Docker:
ENTRYPOINT ["/app/bin/qgtunnel"]
CMD ["node", "server.js"]
Kubernetes (container spec):
command: ["/app/bin/qgtunnel"]
args: ["node", "server.js"]
QGTunnel must be the entrypoint so it is listening before your application opens its first connection.
Direct SOCKS5 Configuration
Where the driver supports a custom dialer or socket, you can skip QGTunnel.
Go with pgx
Go’s standard library proxy package gives you a SOCKS5 dialer, and pgx accepts a custom dial function.
go get golang.org/x/net/proxy
package main
import (
"context"
"fmt"
"log"
"net"
"os"
"github.com/jackc/pgx/v5"
"golang.org/x/net/proxy"
)
func main() {
auth := &proxy.Auth{
User: os.Getenv("QUOTAGUARD_SOCKS_USER"),
Password: os.Getenv("QUOTAGUARD_SOCKS_PASS"),
}
socksAddr := fmt.Sprintf("%s:%s",
os.Getenv("QUOTAGUARD_SOCKS_HOST"),
os.Getenv("QUOTAGUARD_SOCKS_PORT"),
)
dialer, err := proxy.SOCKS5("tcp", socksAddr, auth, proxy.Direct)
if err != nil {
log.Fatal("socks5 dialer:", err)
}
config, err := pgx.ParseConfig(os.Getenv("DATABASE_URL"))
if err != nil {
log.Fatal("parse config:", err)
}
config.DialFunc = func(ctx context.Context, network, addr string) (net.Conn, error) {
return dialer.Dial(network, addr)
}
conn, err := pgx.ConnectConfig(context.Background(), config)
if err != nil {
log.Fatal("connect:", err)
}
defer conn.Close(context.Background())
var now string
if err := conn.QueryRow(context.Background(), "select now()::text").Scan(&now); err != nil {
log.Fatal("query:", err)
}
fmt.Println("Connected to Supabase through QuotaGuard at", now)
}
The connection string keeps the real Supabase hostname, so TLS verification behaves normally.
Node.js with a custom stream on pg
The pg client accepts a stream factory, which lets you hand it a socket opened through SOCKS5.
npm install pg socks
const { Client } = require('pg');
const { SocksClient } = require('socks');
const { URL } = require('url');
const dbUrl = new URL(process.env.DATABASE_URL);
const client = new Client({
connectionString: process.env.DATABASE_URL,
ssl: { rejectUnauthorized: true },
stream: () => {
// socks returns a promise, so this pattern requires a driver version
// that accepts an async stream factory. Verify against your pg version.
return SocksClient.createConnection({
proxy: {
host: process.env.QUOTAGUARD_SOCKS_HOST,
port: Number(process.env.QUOTAGUARD_SOCKS_PORT),
type: 5,
userId: process.env.QUOTAGUARD_SOCKS_USER,
password: process.env.QUOTAGUARD_SOCKS_PASS,
},
command: 'connect',
destination: {
host: dbUrl.hostname,
port: Number(dbUrl.port),
},
}).then((info) => info.socket);
},
});
client.connect()
.then(() => client.query('select now()'))
.then((res) => console.log('Connected to Supabase through QuotaGuard', res.rows[0]))
.catch((err) => console.error('Connection failed:', err));
Support for an asynchronous stream factory varies across pg releases. Confirm the behavior on the version you have pinned before relying on it in production. If it does not hold, QGTunnel is the reliable path for Node.
Testing Your Implementation
Confirm the Connection
Node.js:
const { Client } = require('pg');
const client = new Client({ connectionString: process.env.DATABASE_URL });
client.connect()
.then(() => client.query('select inet_client_addr() as source_ip'))
.then((res) => {
console.log('Supabase sees this source IP:', res.rows[0].source_ip);
return client.end();
})
.catch((err) => console.error('Connection failed:', err.message));
Python:
import os
import psycopg2
conn = psycopg2.connect(os.environ["DATABASE_URL"])
cur = conn.cursor()
cur.execute("select inet_client_addr()")
print("Supabase sees this source IP:", cur.fetchone()[0])
cur.close()
conn.close()
inet_client_addr() returns the address Postgres sees on the other end of the connection. Through a correctly configured proxy it should match one of the two addresses in your QuotaGuard dashboard.
Run it several times. Both of your addresses should appear across repeated connections.
Confirm the Outbound IP Separately
If you want a second check that does not depend on the database connection at all, make an HTTP request through the HTTP proxy from the same runtime:
import os
import requests
proxy_url = os.environ["QUOTAGUARDSTATIC_URL"]
proxies = {"http": proxy_url, "https": proxy_url}
response = requests.get("https://ip.quotaguard.com", proxies=proxies)
print("Outbound IP:", response.json()["ip"])
A connection that succeeds while reporting an address that is not in your dashboard means the proxy configuration was not applied to that connection. A successful query is not by itself proof that traffic went through the proxy, so check the address rather than assuming.
Latency Considerations
Routing through a proxy adds a network hop to every database connection, and Postgres is more sensitive to this than a one-off API call.
To keep the impact small:
- Match all three regions. Your application, your QuotaGuard proxy, and your Supabase project should be in the same AWS region wherever possible.
- Use connection pooling. Let the driver hold persistent connections rather than opening one per request. This matters more through a proxy, because you pay the setup cost on every new connection.
- Use the Supabase pooler for serverless. Serverless runtimes create and discard connections aggressively, which is the worst case for per-connection overhead.
Measure against your own workload and region pairing rather than assuming a fixed figure.
Troubleshooting
Connection times out, no authentication error
The allowlist is refusing you before credentials are checked. Work through this order:
- Confirm both QuotaGuard IPs are in the Supabase restriction list, not just one
- Confirm the addresses match your dashboard exactly, including the
/32mask - Confirm the connection is actually going through the proxy, using
inet_client_addr()above - Confirm you are using SOCKS5 on port 1080, not the HTTP proxy on 9293
Connection works intermittently, fails at other times
The classic symptom of an IPv6 route. If your direct connection resolves to IPv6 and only an IPv4 CIDR is allowlisted, behavior varies by resolution. Move to the pooler connection string, or add the Supabase IPv4 add-on.
password authentication failed
This is a Postgres credential error, not a proxy or allowlist problem. You reached the database. Check the username, the password, and whether your pooler connection string requires the tenant-qualified username format shown in the Supabase dashboard.
SOCKS authentication failed
- Check the SOCKS username and password against the dashboard
- Check for characters in the password that need URL encoding when embedded in a URL
- Confirm SOCKS5, not SOCKS4
TLS certificate verification fails after adding the tunnel
You are connecting to localhost while the certificate is issued for the Supabase hostname. Switch QGTunnel to transparent mode so the original hostname is preserved, rather than disabling certificate verification.
The Network Restrictions section is missing in the dashboard
Supabase’s documentation says to update your Postgres version in Infrastructure Settings and then check again.
A CLI update wiped an existing allowlist entry
Expected behavior. The CIDRs passed to update replace the whole list. Re-run the command with every CIDR you want, including the ones you had before.
ECONNREFUSED 127.0.0.1
QGTunnel is not listening. It either did not start, started after your application, or does not have a mapping for that local port. Make QGTunnel the process entrypoint so it initializes first.
Edge Functions stopped reaching the database after enabling restrictions
Supabase’s documentation states that with network restrictions applied, Edge Functions lose direct access to the database. That is a separate consideration from your application’s connection and is not something the proxy changes.
Environment Variables Reference
# SOCKS5 proxy (port 1080) - used for Postgres and other TCP connections
QUOTAGUARD_SOCKS_HOST=<region>-static-01.quotaguard.com
QUOTAGUARD_SOCKS_PORT=1080
QUOTAGUARD_SOCKS_USER=your-username
QUOTAGUARD_SOCKS_PASS=your-password
# HTTP proxy (port 9293) - used for HTTP and HTTPS requests
QUOTAGUARDSTATIC_URL=http://username:password@<region>-static-01.quotaguard.com:9293
# Your Supabase connection string, copied from the dashboard
DATABASE_URL=postgresql://postgres.<ref>:<password>@<your-supabase-host>:6543/postgres
Use the exact values from your QuotaGuard dashboard and your Supabase connection string.
Security Best Practices
- Remove any permissive entry once the proxy is confirmed working. An allowlist containing
0.0.0.0/0provides no protection. - Never commit credentials. Use your platform’s secret storage for both the database URL and the proxy credentials.
- Allowlist both static IPs. Allowlisting one produces intermittent failures that are hard to diagnose.
- Use least-privilege database roles. Network restrictions are one layer, not the only one.
- Re-run the CLI
getcommand after any change to confirm the resulting list is what you intended. - Rotate credentials periodically, on both the Supabase and QuotaGuard sides.
QuotaGuard Static vs QuotaGuard Shield
| Feature | QuotaGuard Static | QuotaGuard Shield |
|---|---|---|
| Protocol | HTTP / HTTPS / SOCKS5 | HTTPS / SOCKS5 over TLS |
| Customer-to-proxy hop | Plaintext | TLS-encrypted |
| HTTPS payload | Tunneled end-to-end, never decrypted at the proxy | Tunneled end-to-end, never decrypted at the proxy |
| Best for | Most apps | Regulated data or environments that require TLS on every hop |
| Starting price (direct) | $19/month | $29/month |
Postgres connections to Supabase carry their own TLS, so the payload is protected in both cases. Static is right for most applications connecting to Supabase. Choose Shield if the workload handles regulated data under HIPAA, PCI-DSS, or SOC 2, or if your environment requires TLS between your app and the proxy itself.
Ready to Get Started?
Turn on Network Restrictions without locking your own application out.