Introduction
You point your app at a fresh Supabase project, run a query, and get this:
psql: error: connection to server at "aws-1-ap-south-1.pooler.supabase.com" (3.111.225.200),
port 5432 failed: FATAL: (ENOTFOUND) tenant/user postgres.abcdefghijklmnopqrst not found
The Supabase tenant or user not found error is almost never about your password, and almost always about your hostname. This guide walks through diagnosing it properly, why db.<project-ref>.supabase.co often refuses to resolve at all, how to wire the result into Prisma, and the security gotcha that prisma db push leaves behind on a Supabase project.
It's written for the case we hit most: migrating an existing app to a new Supabase project, where every connection string is a hand-edited copy of the old one.
Prerequisites
- A Supabase project (this guide assumes the free or Pro tier, no IPv4 add-on)
psqlavailable locally —brew install libpqon macOS- Prisma 6 or 7 if you're following the ORM sections
- Your database password from Dashboard → Project Settings → Database
Why "Tenant or User Not Found" Happens
Supabase's connection pooler (Supavisor) is multi-tenant and regional. Every project in a given AWS region shares one pooler hostname, and your project ref is carried in the username, not the host:
postgresql://postgres.<project-ref>:<password>@aws-0-<region>.pooler.supabase.com:6543/postgres
When Supavisor receives a connection, it looks up the tenant from that username within the region it's running in. If your project lives in ap-southeast-2 but you connected to the ap-south-1 pooler, that region has genuinely never heard of your tenant — so it returns ENOTFOUND, not an auth failure.
This is why the error is so misleading. It reads like a credentials problem. It's a routing problem.
The single most common cause: copying an old project's connection string and swapping only the project ref, leaving the previous project's region in the host.
Why db.project-ref.supabase.co Won't Resolve
The obvious workaround is the direct connection string the dashboard shows:
psql "postgresql://postgres:[YOUR-PASSWORD]@db.<project-ref>.supabase.co:5432/postgres"
On many machines that fails before it even opens a socket:
psql: error: could not translate host name "db.<project-ref>.supabase.co" to address:
nodename nor servname provided, or not known
Check the DNS records and the reason becomes obvious:
dig +short AAAA db.<project-ref>.supabase.co
# 2406:da1c:10e4:6402:f9d4:cf88:300d:e4a5
dig +short A db.<project-ref>.supabase.co
# (empty)
There's an AAAA record but no A record. Direct connections to Supabase are IPv6-only unless you buy the IPv4 add-on. If your ISP, office network, CI runner, or Docker bridge is IPv4-only, that hostname is unreachable no matter what you put in the password field.
Supavisor, by contrast, is IPv4-reachable on every tier. So on an IPv4-only machine the pooler isn't just the recommended path — it's the only one.
Don't trust the AAAA record to tell you the region. In our case the IPv6 address sat in an AWS Mumbai range while the project was actually in Sydney. Resolve the region from the pooler, not from DNS.
Step 1: Confirm the Project Is Actually Alive
Before debugging connection strings, prove the project exists. Hit the REST endpoint:
curl -s -o /dev/null -w "%{http_code}\n" https://<project-ref>.supabase.co/rest/v1/
# 401
A 401 is the success case. It means the API is up and rejecting you for having no key. Compare with a deleted or non-existent project:
host <old-project-ref>.supabase.co
# Host <old-project-ref>.supabase.co not found: 3(NXDOMAIN)
NXDOMAIN means the project is gone — no amount of connection-string fiddling will help. This check takes two seconds and tells you which problem you actually have.
Step 2: Find Your Real Pooler Region
The reliable answer is in Dashboard → Connect → Transaction pooler, which prints the exact string. If you'd rather not leave the terminal, you can find the region by brute force — the error is instant and unambiguous, so scanning every region takes seconds:
PW='your-database-password'
REF='your-project-ref'
for r in us-east-1 us-east-2 us-west-1 us-west-2 ca-central-1 \
eu-west-1 eu-west-2 eu-west-3 eu-central-1 eu-central-2 eu-north-1 \
ap-south-1 ap-southeast-1 ap-southeast-2 ap-northeast-1 ap-northeast-2 sa-east-1; do
for p in aws-0 aws-1; do
( out=$(PGCONNECT_TIMEOUT=6 psql \
"postgresql://postgres.$REF:$PW@$p-$r.pooler.supabase.com:5432/postgres" \
-tAc "select current_user" 2>&1 | head -1)
case "$out" in
postgres*) echo "*** HIT $p-$r" ;;
*"not found"*) : ;;
*) echo "?? $p-$r => $(echo "$out" | cut -c1-80)" ;;
esac ) &
done
done
wait
Output:
*** HIT aws-0-ap-southeast-2
Note the aws-0 / aws-1 prefix — regions have more than one pooler cluster, and the prefix is part of the hostname. Guessing the region right but the prefix wrong produces the identical ENOTFOUND error.
Reading the password from your environment rather than typing it inline keeps it out of your shell history:
PW=$(grep '^DIRECT_URL=' .env | sed -E 's#.*://[^:]+:([^@]+)@.*#\1#')
Step 3: Configure Prisma for the Supabase Pooler
Prisma needs two URLs against Supabase, because migrations and queries want different pooling behaviour:
# .env
# Transaction mode (6543) — for application queries
DATABASE_URL="postgresql://postgres.<ref>:<password>@aws-0-<region>.pooler.supabase.com:6543/postgres?pgbouncer=true"
# Session mode (5432) — for migrations and DDL
DIRECT_URL="postgresql://postgres.<ref>:<password>@aws-0-<region>.pooler.supabase.com:5432/postgres"
The ?pgbouncer=true flag matters: it tells Prisma to stop using prepared statements, which transaction-mode pooling cannot support.
In Prisma 7, point the CLI at the direct URL via prisma.config.ts:
import 'dotenv/config'
import { defineConfig, env } from 'prisma/config'
export default defineConfig({
earlyAccess: true,
schema: 'prisma/schema.prisma',
migrations: { path: 'prisma/migrations' },
datasource: {
url: env('DIRECT_URL'),
},
})
Then push your schema:
npx prisma db push
Datasource "db": PostgreSQL database "postgres", schema "public"
at "aws-0-ap-southeast-2.pooler.supabase.com:5432"
🚀 Your database is now in sync with your Prisma schema. Done in 18.89s
Step 4: Lock Down the Tables prisma db push Created
This is the step most migration guides skip, and it's the one that matters.
Supabase auto-enables Row Level Security on new tables in public, which is good. But the postgres role that Prisma connects as carries default privileges that grant the anon and authenticated roles full DML on anything it creates. Check for yourself:
psql "$DIRECT_URL" -tAc "select table_name, string_agg(distinct privilege_type,',')
from information_schema.role_table_grants
where grantee='anon' and table_schema='public' group by 1"
Lead|DELETE,INSERT,REFERENCES,SELECT,TRIGGER,TRUNCATE,UPDATE
Your leads table just granted TRUNCATE to the anonymous role. RLS with zero policies still blocks it today — grants and policies are two separate gates, and reads return nothing without a policy. But it means you're exactly one permissive policy away from a public data leak, on a table you never intended to expose.
If your app talks to Postgres through Prisma rather than supabase-js, nothing needs those grants. Revoke them:
revoke all on all tables in schema public from anon, authenticated;
revoke all on all sequences in schema public from anon, authenticated;
alter default privileges in schema public revoke all on tables from anon, authenticated;
That third line is the one that keeps this fixed — without it, the next prisma db push re-grants everything on any new table.
Prisma keeps working because the postgres role has the BYPASSRLS attribute:
psql "$DIRECT_URL" -tAc "select rolname, rolbypassrls from pg_roles
where rolname in ('postgres','anon','authenticated','service_role')"
anon|f
authenticated|f
postgres|t
service_role|t
Only do this if you're not using
supabase-json the client. If your frontend queries Supabase directly with the anon key, you need those grants plus properly scoped RLS policies instead.
Step 5: Verify Everything Works
Never trust "it built fine." Test both the happy path and the thing you just locked down:
# Prisma's connection can read
psql "$DIRECT_URL" -tAc 'select count(*) from "Lead";'
# 0
# The anonymous role cannot
psql "$DIRECT_URL" -tAc 'set role anon; select count(*) from "Lead";'
# ERROR: permission denied for table Lead
Two commands, and you've confirmed the connection works and the exposure is closed.
Troubleshooting: Exact Errors and Fixes
| Error | Cause | Fix |
|---|---|---|
FATAL: (ENOTFOUND) tenant/user postgres.<ref> not found | Wrong pooler region or aws-N prefix | Scan regions (Step 2) or copy from Dashboard → Connect |
could not translate host name "db.<ref>.supabase.co" | Direct host is IPv6-only, you're on IPv4 | Use the pooler, or buy the IPv4 add-on |
Host <ref>.supabase.co not found: 3(NXDOMAIN) | Project deleted or never existed | Check the dashboard; the ref is wrong |
psql: error: invalid URI query parameter: "pgbouncer" | psql rejects Prisma's ?pgbouncer=true | Use DIRECT_URL for psql, DATABASE_URL for the app |
.env:43: parse error near '\n' | source .env chokes on multiline values | Parse the key you need with grep, don't source |
unknown option --skip-generate on prisma db push | Flag removed in Prisma 7 | Drop the flag |
| Prepared statement errors under load | Missing ?pgbouncer=true on port 6543 | Add it to DATABASE_URL |
A Note on Free-Tier Pausing
Free Supabase projects pause after 7 days without database activity — and it's database activity, not HTTP traffic to your site. A project that only writes on contact-form submissions will happily pause during a quiet week, and the first thing you'll notice is production 500s.
If that's a risk, give the database something real to do. A first-party pageview table written on each navigation is genuinely useful analytics and keeps the project awake, without a cron job whose only purpose is faking activity.
Conclusion
The Supabase tenant or user not found error is a routing problem wearing an authentication costume. Work through it in this order and it takes minutes rather than an afternoon:
- Prove the project exists (
curlthe REST endpoint, expect 401) - Check whether the direct host is IPv6-only (
dig AAAA) - Find the real pooler region — the dashboard, or scan
- Split
DATABASE_URL(6543,pgbouncer=true) fromDIRECT_URL(5432) - Revoke the
anongrantsdb pushleft behind, and verify withset role anon
That last step is the one worth adding to your checklist permanently. Every schema push against Supabase quietly re-opens it on new tables unless you've altered the default privileges.
If you're building on Postgres and want the schema, RLS policies, and connection topology reviewed before they reach production, our web development team does exactly this work. You may also find our guide to building real-time applications with Node.js useful for the layer above the database.
Need help with Supabase, Prisma, or a Postgres migration? Get in touch to discuss your project.
