Skip to content

Security basics

Supabase PGRST204: "Could not find the column in the schema cache"

Supabase "Could not find the column in the schema cache" usually means a name mismatch. Four checks in order, and the one-line reload.

Vlad Tkachenko11 min read
A lobby directory board with one empty slot in its list of rooms, and a delivery slip in front of it with one line lit in amber.

In short

  • Supabase "Could not find the column in the schema cache" (PGRST204) means your app tried to save a field the table does not list. PostgREST checks every key in an insert or update against its own copy of your schema, letter for letter.
  • The usual cause is the name: phoneNumber sent to a column called phone_number, or a key that was never a column, such as related rows nested inside an insert.
  • Supabase reloads that copy by itself whenever a table changes. When it really is out of date, NOTIFY pgrst, 'reload schema'; in the SQL Editor reloads it while the app keeps running.

You asked your builder to add a phone number to the profile form. The column is there in the Table Editor. You press Save, nothing happens, and the browser console shows this:

{
  "code": "PGRST204",
  "details": null,
  "hint": null,
  "message": "Could not find the 'phoneNumber' column of 'profiles' in the schema cache"
}

Supabase "Could not find the column in the schema cache" means your app tried to save a field that the table, as PostgREST sees it, does not have. PostgREST is the part of Supabase that turns your app's supabase.from(...) calls into database queries. It keeps its own list of every table and every column, called the schema cache, so it does not have to ask the database about its shape on each request.

Here is the part most answers get wrong: they tell you to reload the schema cache. On Supabase the cache reloads itself every time a table changes. So if you have just added a column and you still see this error, the cache is the least likely reason. The name usually is.

What does "Could not find the column in the schema cache" mean?

It means one key in a write your app sent has no matching column in that table.

Think of the schema cache as the directory board in a building's lobby: a line for every office, which is a table, and under it every room, which is a column. Each request your app sends is a delivery slip. PostgREST reads every line of the slip against the board, and a room the board does not list is refused at the front desk, before anything reaches your data.

The message names both sides of that comparison. In Could not find the 'phoneNumber' column of 'profiles' in the schema cache, phoneNumber is what your app sent and profiles is the table it went to. Three more things are fixed in PostgREST's own code:

  • the code is PGRST204 and the status is HTTP 400, which means the request itself was wrong
  • it comes from writes only: .insert(), .update() and .upsert(). A read that names a missing column fails with a different error, written by Postgres
  • it carries no hint. For a missing table PostgREST suggests the nearest name it knows; for a missing column hint is null, and the comparison is yours

Every key in the object you pass to one of those three calls has to be a column of that table, spelled the way the table spells it.

PostgREST compares each key your app sends with the columns it has listed, letter for letter. phoneNumber and phone_number are two different names.

Cause 1: the name does not match, letter for letter

The check is exact. To PostgREST phoneNumber, phone_number and phonenumber are three different columns, and your table has at most one of them.

There are two common ways to end up here. The first is two naming habits meeting: JavaScript usually writes a field as phoneNumber, and a Postgres column is usually phone_number. The code your builder wrote and the SQL it ran can each be right on their own terms and still disagree.

The second is the quoting trap. In SQL, a name typed without double quotes is folded to lower case. Postgres's own documentation says it plainly: "Quoting an identifier also makes it case-sensitive, whereas unquoted names are always folded to lower case."

What you ran in the SQL EditorThe column it createdWhat your app has to send
alter table profiles add column phone_number text;phone_numberphone_number
alter table profiles add column phoneNumber text;phonenumberphonenumber
alter table profiles add column "phoneNumber" text;phoneNumberphoneNumber

The middle row is the one that catches people. The SQL looks like it made phoneNumber, the code sends phoneNumber, and the column is phonenumber.

To see the real names, open the table in the Table Editor and read the column headers, or run this in the SQL Editor:

select column_name
from information_schema.columns
where table_schema = 'public' and table_name = 'profiles';

Then look at what your app sent. In your browser's developer tools, open the Network tab, click the failed request to /rest/v1/profiles, and read its payload.

The safer fix is to change the app, because it touches nothing in your data. Ask your builder to use the column's exact name everywhere that field is saved: "the column in profiles is phone_number; use that name in every insert and update". Renaming the column instead is a change to a live table, and any other screen still using the old name breaks the first time it runs.

If you build with Lovable, look back through the chat for a database change you were asked to approve. Lovable's Supabase documentation says it "writes the SQL, shows it to you, and asks for your approval in the project chat before running it". Until you approve it, the column does not exist, whatever the code expects.

Cause 2: a key that was never a column

The request carries something the table was never meant to store: related rows nested inside the insert, or a field the form keeps for itself.

The nested insert is the one people expect to work:

await supabase.from('orders').insert({
  customer_id: user.id,
  total: 42,
  order_items: [{ product_id: 7, quantity: 2 }],
})

That fails with Could not find the 'order_items' column of 'orders' in the schema cache. Reading the same shape works: .select('*, order_items(*)') returns an order with its items attached, so the reverse looks like it should too. PostgREST reads every key in a write as a column of the one table the request is for. A Supabase user reported it as a bug, and the answer, before the issue was closed on 18 October 2026, was that nested inserts are a missing feature. The request for them has been open on PostgREST's tracker since February 2017.

Two other shapes of the same problem:

  • a row read together with its related rows, edited in a form and sent back whole in .update(), so the attached order_items or profiles goes with it
  • a form's whole state passed straight to .insert(), carrying fields the form needs and the table does not, such as confirmPassword

The fix is to write each table on its own, parent first. Insert the order and ask for its id back with .select('id').single(), then insert the items with that order_id. Those are two requests, so if the second one fails the order is still saved without its items. When that must never happen, put both inserts in one Postgres function and call it with .rpc(), which the reply on that issue calls the standard pattern. Then all of it is saved or none of it is. If that call answers PGRST202 instead, the function version of this error explains why.

A nested slip is read as one more column of the first table. Sent separately, parent first, each one finds its own board.

Cause 3: the change went into another project

The column exists, in a database your app never talks to. One Supabase account can hold several projects, and a branch is a separate project with its own database. A column added in the SQL Editor of the wrong one changes a board in a different building.

On Supabase's issue #42183, a report that the cache "does not update" whatever was tried, one of PostgREST's developers replied that the report named two different project references, and asked the reporter to confirm the change had been made in the project the requests were going to.

The check takes two addresses. Your app calls https://<reference>.supabase.co, which you can see on any request in the Network tab. The dashboard shows the project you are in at supabase.com/dashboard/project/<reference>. The two references must be the same string. Which project is mine covers finding it when someone else set Supabase up.

Cause 4: the cache really is out of date

This is the rare one on Supabase, because Supabase reloads the cache for you. Every Supabase database is set up with an event trigger, pgrst_ddl_watch, that runs after each change to the shape of the database, ALTER TABLE included, and sends NOTIFY pgrst, 'reload schema': the message that tells PostgREST to rebuild its list. You can read it in Supabase's database setup. Adding a column in the Table Editor or the SQL Editor repaints the board by itself.

It goes wrong when that message never arrives. Supabase's troubleshooting guide describes the case: "an underlying Postgres notification queue issue prevents PostgREST from receiving cache reload signals after schema changes". PostgREST's log then shows Failed listening for database notifications on the "pgrst" channel, and the board stays as it was however many columns you add.

Each ALTER TABLE on Supabase sends the reload message by itself. A stale cache means that message did not arrive.

How do I reload the Supabase schema cache?

Run one line in the SQL Editor:

NOTIFY pgrst, 'reload schema';

Supabase's page for it says to expect "Success. No rows returned" under Results. It is safe on a live app. PostgREST's documentation says "Requests will wait until the schema cache reload is done", so nobody using the app sees an error while it runs.

If the notification queue is the problem, the reload message gets stuck the same way the automatic one did. The troubleshooting guide gives a second line for that:

select pg_notification_queue_usage();

Supabase says it prompts PostgREST to update its cache and "does not require a database instance or PostgREST service restart". Then try the save again.

When reloading does not help

Restart the project, and if the error survives that, go back to causes 1 to 3.

A restart always gives PostgREST a fresh list. The same PostgREST developer wrote on #42183 that a restarted PostgREST "won't go live if it doesn't load a new schema cache". Restart project sits under Project Settings, then General, and how to restart a Supabase project walks through it, including the few minutes it is offline.

So an error that survives a restart comes from a fresh, accurate list: that table, in that project, has no column by that name. #42183 itself, opened in January 2026 by someone who had tried a reload, a restart and dropping and re-adding the column, was still open when we read it on 21 October 2026. The developer's reply there also suggested upgrading to PostgREST 14 from the project's Infrastructure settings.

Leave drop and re-add off your list. alter table … drop column deletes every value stored in that column, and the new column with the same name starts empty. If you are about to change a table that real users write to, keep a copy of the database from before the change. Reeve Care makes one on a schedule and keeps it encrypted, on Reeve's own storage in the EU.

Keeping your generated types in step

Regenerate your TypeScript types after every change to a table, and a misspelled column name can show up in the editor before it reaches the live site.

Supabase builds the file from your actual columns, either from the dashboard or with one command from its guide to generating types:

npx supabase gen types typescript --project-id "$PROJECT_REF" --schema public > database.types.ts

When the client is created with those types, createClient<Database>(…), each table's Insert type lists the columns .insert() accepts. A misspelled column in an object written out in the call is then marked as an error before you publish. Lovable's documentation says it "regenerates the TypeScript types your app uses" after a database change. A types file generated before your last change to the table still lists the old columns.

Changing a live table without losing what is in it

Keep a copy from before the change, and know straight away whether the app still answers after it.

Reeve Care keeps a copy of your Supabase database:

  • an encrypted copy on a schedule, on Reeve's own storage in the EU, outside your Supabase account
  • the last copy before your change holds the tables and columns as they were, rows included
  • each copy verified before it counts, by counting the rows in every table
  • your uploaded files as well, once you connect a Storage credential
  • a one-click restore, with a safety copy of the current state taken first
  • everything Monitor does

Reeve Monitor keeps checking from outside:

  • whether your app is up, checked every minute, so a broken save button and an outage are easy to tell apart
  • the full security check every hour, on up to three apps
  • a message when a result changes, so a deploy that changes what your app exposes reaches you without you going to look
  • a monthly report, in plain language, of what it saw

What to do now

What to do

  • Read the two names in the message: the key your app sent and the table it went to.
  • Compare that key with the column in the Table Editor, letter for letter. A name typed in SQL without quotes is stored in lower case.
  • Look for keys that are not columns: related rows nested in an insert, or a whole row sent back with its related rows. Write each table on its own, or put both writes in one function.
  • Check that the reference in your app's Supabase address matches the project you changed.
  • If all of that matches, run NOTIFY pgrst, 'reload schema';, then select pg_notification_queue_usage();, and restart the project only if neither helps.
  • Regenerate your TypeScript types after every change to a table.

Start with the column name the message quotes, because it is exactly what your app sent, and the Table Editor shows exactly what the table has. Before you rename or drop anything in a table people use, keep a copy of the database from before the change.

FAQ

What does PGRST204 mean?

PGRST204 is the code PostgREST, the part of Supabase that answers your app's database calls, gives a write that names a column the table does not have. It comes back with HTTP 400 and the message "Could not find the 'x' column of 'y' in the schema cache", where x is the key your app sent and y is the table it sent it to. Inserts, updates and upserts produce it.

I added the column and it still fails. Why?

Usually the name. Check that your app sends exactly the stored name (a column added in SQL as phoneNumber, without quotes, is stored as phonenumber), that you added it in the project your app calls, and that nothing else in the request is a key the table lacks. Supabase reloads the schema cache by itself after every ALTER TABLE, so a stale cache is the last thing to suspect.

How do I reload the Supabase schema cache?

Run NOTIFY pgrst, 'reload schema'; in the SQL Editor. If that changes nothing, Supabase's troubleshooting guide gives a second command, select pg_notification_queue_usage();, for when the reload messages are stuck. Requests that arrive during the reload wait for it to finish.

Does pausing the project clear it?

Yes. PostgREST builds a fresh schema cache every time it starts, and resuming a paused project starts it again. Restart project, under Project Settings and then General, does the same without the pause. If the error survives either, the cache was right all along: the column is missing from that table in that project.

Why do nested inserts fail with this error?

Because PostgREST reads every key in an insert as a column of one table, so a key holding related rows is a column it cannot find. Reading related rows in one request works, and writing them that way has never been supported. Insert the parent first and the children with its id, or do both inside one Postgres function called with rpc().

Written by

Vlad Tkachenko

Founder, Reeve

I spend my time looking at apps built with Lovable, Bolt, v0, Cursor and Replit, and at the short list of mistakes that keep turning up in them.

More about the author

Read next

All articles

Back up your Supabase database before your next change

Reeve keeps an encrypted copy on a schedule and restores it for you if something breaks.

See Supabase backups

Automated external check, not a full audit. Absence of findings is not a guarantee of safety.