Back to Blog
DatabaseAug 7, 20268 min read

Fix Supabase Tenant or User Not Found (Prisma Guide)

Fix Supabase Tenant or User Not Found (Prisma Guide)
ST
SynchSoft Team
SynchSoft Team

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)
  • psql available locally — brew install libpq on 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-js on 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

ErrorCauseFix
FATAL: (ENOTFOUND) tenant/user postgres.<ref> not foundWrong pooler region or aws-N prefixScan 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 IPv4Use the pooler, or buy the IPv4 add-on
Host <ref>.supabase.co not found: 3(NXDOMAIN)Project deleted or never existedCheck the dashboard; the ref is wrong
psql: error: invalid URI query parameter: "pgbouncer"psql rejects Prisma's ?pgbouncer=trueUse DIRECT_URL for psql, DATABASE_URL for the app
.env:43: parse error near '\n'source .env chokes on multiline valuesParse the key you need with grep, don't source
unknown option --skip-generate on prisma db pushFlag removed in Prisma 7Drop the flag
Prepared statement errors under loadMissing ?pgbouncer=true on port 6543Add 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:

  1. Prove the project exists (curl the REST endpoint, expect 401)
  2. Check whether the direct host is IPv6-only (dig AAAA)
  3. Find the real pooler region — the dashboard, or scan
  4. Split DATABASE_URL (6543, pgbouncer=true) from DIRECT_URL (5432)
  5. Revoke the anon grants db push left behind, and verify with set 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.

SupabasePrismaPostgreSQLConnection Pooling
Share this article:

Stay Updated

Get the latest insights delivered to your inbox.

No spam, unsubscribe anytime.

Need Help With Your Project?

Let's discuss how we can help bring your vision to life.

Get in Touch

Ready to Start Your Project?

Let's discuss how we can help transform your ideas into reality.