Основи безпеки
Supabase "relation does not exist", хоча таблиця на місці
Supabase "relation does not exist" або PGRST205: жодна таблиця не має такої назви в цій схемі. Чотири перевірки, і на що зважити, відкриваючи схему.

Коротко
- Supabase "relation does not exist" (код Postgres 42P01) означає, що в схемах, які переглянув запит, немає жодної таблиці чи view саме з такою назвою. У вашому застосунку на сучасному проєкті та сама проблема читається як "Could not find the table in the schema cache" (PGRST205).
- Спершу перевірте назву літера за літерою, разом із великими літерами, потім схему, потім проєкт. Усередині функції з порожнім search_path кожна таблиця потребує схеми попереду: public.invoices.
- Відкрита схема прибирає помилку й дає API доступ до кожної таблиці в ній, тож перевірте Row Level Security на кожній із цих таблиць, перш ніж її відкривати.
Ваш білдер додав таблицю invoices. Ви бачите її в Table Editor, разом із рядками,
а сторінка, яка мала б їх показати, лишається порожньою. Консоль браузера показує
ось це:
{
"code": "PGRST205",
"details": null,
"hint": "Perhaps you meant the table 'public.Invoices'",
"message": "Could not find the table 'public.invoices' in the schema cache"
}
Supabase "relation does not exist" означає, що Postgres не знайшов жодної таблиці
чи view саме з такою назвою в схемах, які переглянув запит. PGRST205 дає ту саму
відповідь, але від PostgREST, частини Supabase, яка перетворює виклики
supabase.from(...) вашого застосунку на запити до бази, ще до того, як запит
дійде до Postgres.
Порада, яку для цієї помилки передають з рук у руки, звучить так: додати схему в Exposed schemas у налаштуваннях проєкту. Вона виправляє одну з чотирьох причин нижче. А ще вона відкриває кожну таблицю цієї схеми будь-кому, хто має публічний ключ вашого застосунку, тож прочитайте, що саме вона відкриває, перш ніж її вмикати.
Що означає "relation does not exist" у Supabase?
База шукала таблицю саме з такою назвою в схемах, які їй сказали переглянути, і не
знайшла жодної. "Relation" у Postgres позначає все, з чого виходять рядки, тобто
таблицю або view. Код помилки 42P01, і
Postgres називає його undefined_table.
Уявіть свою базу як будинок. У ньому кожна схема займає окремий поверх: public
на першому поверсі, а auth, де Supabase тримає ваших користувачів, на іншому.
Кожна таблиця схожа на кімнату з назвою на дверях. Запит, який називає лише кімнату,
шукає її на поверхах зі свого шляху пошуку. Запити вашого застосунку їдуть ліфтом,
а ліфт зупиняється лише на поверхах, які відкриває ваш проєкт. Помилка означає, що
на перевірених поверхах ніхто не знайшов дверей із такою назвою.
Яке формулювання ви побачите, залежить від того, де виконувався запит:
| Де виконувався запит | Що повертається |
|---|---|
| SQL Editor, міграція, функція або тригер | relation "invoices" does not exist, код 42P01 |
| Ваш застосунок, PostgREST 13 або новіший | Could not find the table 'public.invoices' in the schema cache, код PGRST205, HTTP 404 |
| Ваш застосунок, старіший PostgREST | читання отримує relation "public.invoices" does not exist, HTTP 404, а insert отримує порожню помилку |
| Ваш застосунок просить схему, яку проєкт не відкриває | Invalid schema: billing, код PGRST106, HTTP 406 |
Changelog PostgREST
датує нове формулювання версією 13.0.0, що вийшла 8 травня 2025 року: "Querying
non-existent table now returns PGRST205 error". До того, як
пояснив один із його мейнтейнерів у issue #3697,
читання йшло в Postgres без перевірки й поверталося як 42P01.
Посібник Supabase про захист Data API
каже, що версію PostgREST свого проєкту можна перевірити в Dashboard.
Власний код PostgREST
визначає решту: підказка PGRST205, якщо вона є, називає найближчу таблицю, яку
PostgREST знає на тому самому поверсі.
Якщо ж у повідомленні стоїть permission denied for table, таблицю знайшли, а
ваш застосунок зупинили біля дверей. Для цього випадку є
інше виправлення.
Причина 1: назва на дверях має велику літеру
Назва має збігатися точно, разом із великими літерами, і два з інструментів, якими ви користуєтеся, поводяться з великими літерами не так, як третій.
Документація Postgres вміщує правило в одне речення: "Quoting an identifier also makes it case-sensitive, whereas unquoted names are always folded to lower case." Два інші зберігають те, що ви набрали:
- Table Editor у Supabase створює таблицю саме з тією назвою, яку ви їй дали.
Його код
бере назву в лапки, коли їх потрібно, тож
InvoicesлишаєтьсяInvoices - PostgREST бере в подвійні лапки кожну назву, яку надсилає ваш застосунок
(
pgFmtIdent), тож.from('invoices')проситьinvoicesі нічого іншого
| Як створили таблицю | У SQL пишіть | У застосунку пишіть |
|---|---|---|
create table invoices або Invoices, набране в SQL без лапок | invoices | .from('invoices') |
create table "Invoices" або Invoices, набране в Table Editor | "Invoices", разом із лапками | .from('Invoices') |
Другий рядок дає цю помилку з обох боків. Ваш застосунок викликає
.from('invoices') і отримує PGRST205 зі справжньою назвою в підказці, як у
повідомленні на початку цієї сторінки. Або застосунок працює, а функція чи
policy, що називає Invoices без лапок, падає з
relation "invoices" does not exist, бо Postgres перевів назву в малі літери ще
до пошуку.
Щоб побачити кожну таблицю з такою назвою, на будь-якому поверсі й з будь-якими великими літерами, виконайте це в SQL Editor:
select table_schema, table_name
from information_schema.tables
where table_name ilike 'invoices';
Виправлення, яке нічого не змінює у ваших даних, лежить у коді: пишіть
"Invoices" з лапками в SQL і точні великі літери в застосунку. Якщо натомість
перейменувати таблицю, зламається кожен екран, що досі використовує стару назву.
Версія цієї помилки для колонок має ту
саму пастку з назвами колонок.
Причина 2: таблиця на іншому поверсі
Ваш застосунок шукає в public, якщо йому не сказати інакше, тож таблиці в будь-якій
іншій схемі для нього немає. Тому повідомлення й каже public.invoices: public
означає поверх, куди поїхав ліфт.
Таке буває трьома способами:
- ваш білдер створив таблицю у власній схемі, як-от
billingчиprivate, а застосунок викликає звичайне.from('invoices') - схема записана в самій назві, як
.from('billing.invoices'). PostgREST читає весь рядок як одну назву таблиці й шукає її вpublic - таблиця називається
auth.users. Посібник Supabase про дані користувачів каже: "For security, the Auth schema is not exposed in the auto-generated API." Його відповідь полягає у власній таблиці вpublic, у його прикладіprofiles, яку заповнює тригер, коли хтось реєструється, і цей тригер може падати по-своєму
Таблицю на іншому поверсі читають через .schema('billing').from('invoices'), і ця
схема має бути в списку Exposed schemas у налаштуваннях API проєкту, як показує
посібник Supabase про власні схеми.
Доти відповідь змінюється на PGRST106:
{
"code": "PGRST106",
"details": null,
"hint": "Only the following schemas are exposed: public, graphql_public",
"message": "Invalid schema: billing"
}
Підказка перелічує поверхи, на яких ліфт таки зупиняється. PostgREST 12 формулює
ту саму помилку інакше, зі списком просто в повідомленні:
The schema must be one of the following: public, graphql_public. Саме такий
текст дає його код для цієї версії,
а changelog переніс назву схеми в повідомлення у 13.0.7.
Ваш секретний ключ їде тим самим ліфтом. Відкриття схеми належить до налаштувань
API, тож серверний код, що використовує supabase-js із цим ключем, отримує той
самий PGRST106.
Що відкриває відкрита схема?
Ліфт починає зупинятися на цьому поверсі, а з дозволами з посібника Supabase кожен відвідувач отримує ключ до кожної кімнати там. Row Level Security тоді лишається єдиним, що вирішує, що кожен із них може прочитати чи змінити.
Посібник про власні схеми має два кроки. Перший полягає в налаштуванні Exposed schemas. Другий складається з блоку SQL, і ось його перші два рядки:
GRANT USAGE ON SCHEMA myschema TO anon, authenticated, service_role;
GRANT ALL ON ALL TABLES IN SCHEMA myschema TO anon, authenticated, service_role;
anon стоїть за кожним запитом від того, хто не ввійшов. ALL означає читати,
додавати, змінювати й видаляти. Далі блок продовжується через
ALTER DEFAULT PRIVILEGES, що дає такий самий доступ кожній таблиці, створеній на
цьому поверсі пізніше.
Це безпечно там, де Row Level Security, правила для кожного рядка, які Supabase перевіряє на кожному запиті, увімкнена й написана для кожної таблиці. Посібник Supabase про захист Data API каже: "Tables created through the Supabase Dashboard have RLS enabled by default. Enable RLS explicitly for tables created in the SQL Editor or through another tool." Схема, яку ваш білдер налаштував у SQL, може містити таблиці без неї. Відкрита з такими дозволами, така таблиця віддає кожен рядок будь-кому з вашим публічним ключем і приймає ще й його зміни.
Перевірте перед відкриттям:
select tablename, rowsecurity
from pg_tables
where schemaname = 'billing';
Кожен рядок зі значенням false позначає таблицю зовсім без правил.
Увімкнути Row Level Security на кожній таблиці
пояснює, як її ввімкнути й написати перше правило, а
"RLS disabled in public" пояснює відкрите
правило читання, з яким таблиця лишається так само досяжною навіть із увімкненою
Row Level Security.
Можна дати й менше дозволів, ніж у посібнику. Поверху з однією таблицею, яку читають лише користувачі після входу, вистачить двох рядків:
grant usage on schema billing to authenticated;
grant select on billing.invoices to authenticated;
Чи відповідають ваші таблиці незнайомцю, можна перевірити ззовні. Наше безкоштовне сканування читає ваш робочий застосунок тим самим публічним ключем, який отримує відвідувач, показує, які таблиці віддають рядки, і не потребує облікового запису.
Відкрити схему чи перенести таблицю?
Вирішуйте за тим, хто читає таблицю. Таблиці, яку ваш застосунок читає з
браузера, потрібен дозвіл для ролі, що її читає, і Row Level Security з правилом,
де б вона не лежала. Тож перенесення в public заощадить вам лише крок із
відкриттям схеми.
Якщо переносите:
alter table billing.invoices set schema public;
Таблиця зберігає свої дозволи й правила. Функція, у тілі якої названо
billing.invoices, після цього падає з тією самою помилкою, бо тіло зберігає назву
такою, якою її написали.
Таблиця, потрібна лише серверному коду, може лишитися на поверсі, куди ліфт ніколи
не їздить. Функція в public може зробити з нею ту одну дію, яка потрібна вашому
застосунку, а
виклик функції Supabase пояснює, як забрати
execute у відвідувачів без входу.
Причина 3: функція з порожнім шляхом пошуку
Функція з search_path = '' не переглядає жодного поверху, тож кожна таблиця в
ній потребує схеми попереду: public.invoices, ніколи не просто invoices.
Security Advisor у Supabase просить саме такого налаштування. Його
попередження function_search_path_mutable
каже: "We recommend pinning functions' search_path to an empty string". Та сама
сторінка закінчується словами: "once you set the search_path = '' all references
to tables/functions/views/etc in your function's body must be qualified with a
schema name."
Отже, попередження виправляють одним рядком, а функція після цього падає на першій назві таблиці без схеми:
create or replace function public.count_invoices()
returns bigint
language plpgsql
set search_path = ''
as $$
begin
return (select count(*) from invoices); -- relation "invoices" does not exist
end;
$$;
Напишіть from public.invoices, і вона знову запрацює. Залиште налаштування: воно
не дає тому, хто викликає функцію, спрямувати назву без схеми на власну таблицю.
Коли функцію викликають із застосунку через .rpc(), помилка проходить крізь
PostgREST як 42P01 з HTTP 404, тим статусом, який
код PostgREST
дає невідомій таблиці. Тригер на нових реєстраціях, що падає так само, видно як
"Database error saving new user".
Причина 4: таблиці ще немає в цьому проєкті
Таблиця існує в іншій базі або лише у вашому коді.
Один обліковий запис Supabase може мати кілька проєктів, а гілка є окремим проєктом із власною базою. Таблиця, створена в SQL Editor не того проєкту, це кімната в іншому будинку. Версія цієї помилки для колонок показує, як порівняти посилання на проєкт в адресі Supabase вашого застосунку з тим, що в dashboard.
Якщо ви будуєте з Lovable, перегляньте чат у пошуках SQL, який вас просили схвалити. Документація Lovable про Supabase каже, що він "writes the SQL, shows it to you, and asks for your approval in the project chat before running it". Поки ви не схвалили, таблиця існує лише в коді, який її читає.
Перенесення таблиці, від якої залежить ваш застосунок
Збережіть копію бази з часу до перенесення й дізнавайтеся, коли змінюється щось, що відкриває ваш застосунок.
Reeve Care зберігає копію вашої бази даних Supabase:
- зашифрована копія за розкладом, у власному сховищі Reeve в ЄС, поза вашим обліковим записом Supabase
- кожна схема, яку ви створили,
publicта інші, з їхніми таблицями й рядками такими, якими вони були до вашої зміни - кожна копія перевірена, перш ніж її зараховують, підрахунком рядків у кожній таблиці
- ваші завантажені файли теж, щойно ви підключите облікові дані Storage
- відновлення одним кліком, перед яким береться страхувальна копія поточного стану
- усе, що робить Monitor
Reeve Monitor і далі перевіряє ззовні:
- чи доступний ваш застосунок, з перевіркою щохвилини
- повна перевірка безпеки щогодини, до трьох застосунків
- повідомлення, коли результат змінюється, тож деплой, який змінює те, що відкриває ваш застосунок, дійде до вас, і вам не доведеться йти дивитися
- щомісячний звіт простою мовою про те, що він побачив
Що зробити зараз
Що робити
- Прочитайте повідомлення: назва в лапках показує, що саме просили, а
public.перед нею показує, яку схему переглянули. - Порівняйте назву з Table Editor, разом із великими літерами. Таблиця з великою літерою в назві потребує подвійних лапок у SQL.
- Знайдіть, у якій схемі лежить таблиця, запитом до
information_schema.tablesі викликайте таблицю позаpublicчерез.schema('назва').from(...). - Перш ніж відкривати схему, виконайте запит до
pg_tablesі ввімкніть Row Level Security на кожній таблиці зі значеннямfalse. - Усередині функції з
search_path = ''пишіть кожну таблицю якpublic.назва. - Перевірте, що проєкт, який викликає ваш застосунок, той самий, де ви створили таблицю.
Почніть із двох назв у повідомленні, таблиці та схеми перед нею, бо вони точно кажуть, які двері просили і на якому поверсі. Перш ніж переносити, перейменовувати або відкривати щось, чим користуються люди, збережіть копію бази з часу до зміни.
Поширені запитання
Чому Supabase каже, що моєї таблиці не існує?
Бо в схемі, яку переглянув запит, немає таблиці саме з такою назвою. Перевірте чотири речі по черзі: великі літери в назві, схему, у якій лежить таблиця (ваш застосунок шукає в public, якщо йому не сказати інакше), проєкт, з яким говорить ваш застосунок, і чи виконується запит усередині функції з порожнім search_path, де кожна таблиця потребує схеми попереду.
Що означає "the schema must be one of the following"?
Так звучить помилка PGRST106 у старіших версіях PostgREST. Ваш застосунок попросив схему, яку проєкт не відкриває, наприклад через .schema('назва'), а список після цих слів перелічує відкриті схеми. Новіші версії кажуть "Invalid schema: назва" і кладуть той самий список у підказку. Відкрита схема виправляє помилку, і це варто вирішувати свідомо.
Чи безпечно відкривати схему в Supabase?
Лише тоді, коли кожна таблиця в ній має ввімкнену Row Level Security і написане правило. Посібник Supabase про власні схеми дає anon, ролі кожного відвідувача без входу, повний доступ до кожної таблиці схеми, зокрема до тих, що з'являться там пізніше. Його посібник з безпеки додає, що таблиці, створені в SQL Editor, не отримують Row Level Security за замовчуванням.
Чи мають значення великі літери в назві таблиці Supabase?
Так. Назва, написана в SQL без подвійних лапок, переводиться в малі літери, а Table Editor і supabase-js зберігають великі літери саме так, як їх написали. Таблицю, створену як Invoices, у SQL пишуть "Invoices", у лапках, а у вашому застосунку .from('Invoices').
Може, краще перенести таблицю в public?
Перенесення виправляє ту половину помилки, що стосується схеми, і нічого не дає безпеці: таблиці, яку ваш застосунок читає з браузера, потрібен дозвіл для ролі, що її читає, і Row Level Security з правилом, де б вона не лежала. Таблиця, потрібна лише серверному коду, може лишитися в закритій схемі, а до неї дістанеться функція в public, що робить одну потрібну вашому застосунку дію.