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.

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
PGRST204and 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
hintisnull, 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.
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 Editor | The column it created | What your app has to send |
|---|---|---|
alter table profiles add column phone_number text; | phone_number | phone_number |
alter table profiles add column phoneNumber text; | phonenumber | phonenumber |
alter table profiles add column "phoneNumber" text; | phoneNumber | phoneNumber |
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 attachedorder_itemsorprofilesgoes with it - a form's whole state passed straight to
.insert(), carrying fields the form needs and the table does not, such asconfirmPassword
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.
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.
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';, thenselect 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().