Back to Blog

Your Postgres Schema Isn't Only public — Does Your AI See the Rest?

Django, Rails, and warehouse-style Postgres apps put tables outside public. If AI list_tables only shows public, whole schemas disappear. How schema.table works with MCP — and how to check.

You connected Postgres. The AI listed tables. Half your app is missing.

Not a credential bug. Not RLS. Often it's this: your tables aren't in public, and the tool only looked there.

Django apps, some Rails setups, and anything that grew past a single schema put real tables in app, analytics, prod_django, or a per-tenant namespace. Humans open DBeaver, switch schema, and move on. Models call list_tables, get twenty public leftovers, and invent the rest — or tell you the table "doesn't exist."

If you've already hit invented columns on a live connection, read why AI hallucinates your database schema. This post is the quieter sibling: the catalog was real; the namespace was wrong.

What "schema" means here

In Postgres, one database can hold many schemas. Each schema holds tables (and views, etc.). The default schema is public. A table's full name is schema.table — for example analytics.page_views or prod_django.core_user.

That is unrelated to "database schema" as in "the shape of my tables." Same word, two meanings. Here we mean the Postgres namespace.

How the failure shows up

Symptom 1 — Empty or thin list_tables. You know orders lives under app, but the tool returns only public scaffolding (spatial_ref_sys, leftover migrations, nothing you care about).

Symptom 2 — describe_table fails on a name that works in psql. You ask for prod_django.core_business (or paste that from Django). The tool replies that the table wasn't found in the public schema — a giveaway it never looked elsewhere.

Symptom 3 — The model invents a public twin. It "finds" users in public (or invents one) while production identity lives in prod_django.auth_user. Wrong joins, wrong counts, confident tone.

Symptom 4 — Raw SQL works, tools don't. execute_sql with SELECT * FROM prod_django.core_user LIMIT 5 succeeds because Postgres accepts qualified names, while query_table with table_name: core_user still hits public only. Inconsistent tool behavior confuses both you and the model.

None of this looks like a timeout or a plan limit. It looks like "AI can't see my database" when it can — just not that folder.

Why connectors get stuck on public

information_schema.tables includes a table_schema column. The simplest demo query is:

SELECT table_name
FROM information_schema.tables
WHERE table_schema = 'public'
  AND table_type = 'BASE TABLE';

That's fine for a tutorial database with five tables in public. It's a silent filter for everyone else.

A connector that wants multi-schema apps to work should:

  1. List tables from schemas other than pg_catalog and information_schema
  2. Return non-default tables as schema.table so names round-trip
  3. Accept schema.table in describe_table and query_table
  4. Quote identifiers properly ("prod_django"."core_user")

Synra's managed Postgres path does that. Self-hosted MCP servers vary — if yours only documents public, assume you're on the thin path until you prove otherwise.

How to check in five minutes

  1. In your SQL client, run:
SELECT table_schema, table_name
FROM information_schema.tables
WHERE table_type = 'BASE TABLE'
  AND table_schema NOT IN ('pg_catalog', 'information_schema')
ORDER BY 1, 2;
  1. Note which schemas hold the tables you actually query.

  2. In the AI client, call list_tables (or ask: "List every table with its schema.").

  3. Compare. If the AI list is only public and step 1 shows app / prod_django / etc., the connector — not your question — is the bottleneck.

  4. Try describe_table on a qualified name from step 1. Success means the round-trip works. A "public schema" error means it doesn't.

How to work with multi-schema DBs once tools support it

Prefer qualified names in prompts. "Use analytics.events and app.accounts — both are outside public." Saves a guessing loop.

Don't dump every schema into one allowlist blindly. Multi-schema setups often mix app tables with junk or sensitive archives. Pair this with table allowlists — allow analytics.* style surfaces you care about, keep identity schemas off until you mean it.

Staging still wins. A full prod catalog with five schemas is a lot of surface. Staging first plus a subset of schemas is calmer than production primary on day one.

For metrics, qualify joins. public.users vs prod_django.auth_user is a classic double-count. Ask the model to show the SQL and confirm both sides of the join use the schemas you intend — see verify AI database answers.

What this is not

  • Not a substitute for read-only. Schema visibility is about finding tables. Writes are a separate gate — why read-only matters.
  • Not column-level security. Seeing app.customers still means every column on that table is readable unless you use views or grants.
  • Not "AI is broken at SQL." Unqualified names and public-only tools are environmental. Fix the environment; the same model suddenly looks competent.

Quick setup reminder

If you're wiring this through Synra: add the Postgres connection, confirm Test connection, then in Claude / Cursor / ChatGPT ask for a full list_tables and check for schema.table entries. Missing schemas after that usually means the DB role itself can't see them (USAGE on the schema + SELECT on tables) — grant at the database, not in the chat.

For client setup, use your usual guide (Cursor + Postgres, Claude + Postgres, ChatGPT + Postgres). The schema issue sits after connect: connection green, catalog incomplete.

Bottom line

Postgres apps outgrow public. AI tools that only list public make half the database invisible and push the model back into guessing. Require schema.table support, verify with a real information_schema query, then prompt with qualified names. The tables were there the whole time — the namespace was the missing piece.