Основи безпеки
Supabase invalid JWT: який ключ перестав працювати?
Помилка Supabase invalid JWT — це PGRST301: підписаний токен не пройшов перевірку. У новому проєкті йдеться про сесію, а не про ключ API.

Коротко
- Помилка Supabase invalid JWT приходить як PGRST301 із HTTP 401 і означає, що підпис токена, який пішов із вашим запитом, не вдалося перевірити. Нічого не шукали в списку, щоб потім відмовити.
- Ключі publishable і secret — це короткі рядки без підпису, тож у проєкті, який уже перейшов на них, ця помилка стосується вашого залогіненого користувача, а не ключа API.
- PostgREST 13 замінив одну розпливчасту фразу коротким переліком конкретних. Читайте те повідомлення, яке ви справді отримали: кожне відкидає іншу причину.
Учора ваш застосунок працював. Сьогодні вранці кожен запит повертається з 401, у коді нічого не змінилося, а у відповіді йдеться про якийсь JWT, якого ви ніколи нікуди не вписували.
{
"code": "PGRST301",
"message": "JWT cryptographic operation failed"
}
Помилка Supabase invalid JWT — це та, що з'являється після того, як ви зробили все правильно. Ви замінили ключ, бо так радив якийсь посібник, або перевели проєкт на новіші ключі, і застосунок замовк.
І ось де посібник за посібником помиляється: ключ, про який говорить це
повідомлення, часто не той, який ви щойно змінили. Пошукайте помилку, і вас
відправлять перевіряти ключ anon. У проєкті, що перейшов на ключі publishable
і secret, ключ API взагалі не є підписаним токеном, тож перевірку не пройшло щось
інше у запиті.
Що означає помилка Supabase invalid JWT?
Щось у вашому запиті було підписаним токеном, і підпис не збігся.
Пароль — це слово, яке хтось шукає у списку. JWT — ні. Це лист, запечатаний сургучем: людина біля дверей не шукає вас ніде, вона тримає штемпель і перевіряє, чи печатку на вашому листі зроблено саме ним. Якщо так, ви проходите, і ніхто не читає жодного списку.
Лист із печаткою від іншого штемпеля відхиляють, хоча кожне слово всередині правильне. А щойно двері міняють штемпель, жоден лист, що вже в дорозі, більше не відкривається, і при цьому ніхто листів не торкався.
Ваш запит — це лист. Ваш проєкт Supabase тримає штемпель. 401 тут означає, що ці двоє не зійшлися.
У PostgREST 13 повідомлення змінилося, а код — ні
Перевіряйте PGRST301. Яка фраза стоятиме поруч, залежить від версії PostgREST,
на якій працює ваш проєкт, і новіші дають коротий перелік таких фраз.
Довідник помилок PostgREST описує PGRST301 як отриманий JWT, який не вдалося
декодувати або який недійсний, із HTTP 401, і у версії 13, і у версії 14. Ця
фраза — опис у таблиці документації. Це не те, що потрапляє у ваше поле
message. За Error.hs у гілці main PostgREST, це поле несе одну з таких:
| Що каже повідомлення | Що це вам говорить |
|---|---|
Empty JWT is sent in Authorization header | Заголовок надіслали, і він був порожній. |
Expected 3 parts in JWT; got 1 | Те, що прийшло, не є JWT. JWT складається з трьох частин, розділених крапками. |
No suitable key or wrong key type | У сервера немає ключа, яким можна перевірити токен такого типу. |
Wrong or unsupported encoding algorithm | Токен називає алгоритм підпису, якого сервер не використовує. |
JWT cryptographic operation failed | Токен побудовано правильно, а підпис не збігається. |
Unsupported token type | Токен має форму, яку PostgREST не читає. |
JWT cryptographic operation failed — це та, що означає застарілий ключ. Токен
зібрано правильно й підписано чимось, чого проєкт уже не тримає, і саме це
лишається після ротації.
Старіші проєкти бачать зовсім інше, і звідси походить той один рядок, який
трапляється всюди в результатах пошуку. До версії 13 PostgREST друкував те, що
сказала його JWT-бібліотека, точно так, як вона сказала. Звідти й береться
JWSError JWSInvalidSignature: це надрукована величина з бібліотеки, а не
фраза, яку хтось написав для вас. Changelog PostgREST до 13.0.0 описує цю зміну
як діагностичні повідомлення про помилки замість відкритих внутрішніх значень.
Поки ви тут, варто знати сусідній код. PGRST303, також 401, ви отримуєте, коли
підпис перевірено, а вміст токена відхилено: протермінований, ще не дійсний або
з не тією audience. Якщо у вас саме цей код, припиніть дивитися на ключі й
подивіться на годинник і на сесію.
Який це ключ: ваш ключ API чи токен вашого користувача?
Це залежить від того, чи ваш проєкт досі користується anon і service_role.
Документація Supabase про ключі API прямо називає різницю. Старі ключі anon і
service_role — це довгоживучі JWT, тому вони й починаються з eyJ. Новіші
ключі publishable і secret — це короткі рядки, і вони взагалі не JWT.
Цей один факт розділяє помилку навпіл:
- Ваш проєкт досі користується
anonіservice_role. Ключ API у вашому запиті сам є підписаним токеном, тож ключ з іншого проєкту, ключ із часів до ротації чи обрізана вставка провалюють перевірку підпису так само, як провалила б її зіпсована сесія. Будь-який із двох токенів у запиті може бути тим, що не пройшов. - Ваш проєкт перейшов на
sb_publishable_іsb_secret_. Ключ API перестав бути токеном, тож на ньому немає підпису, який щось могло б відхилити. Перевіряти лишається access token вашого залогіненого користувача, який Supabase Auth видає окремо від ключа API.
На практиці перше питання не в тому, який ключ неправильний. Воно в тому, чи був хтось залогінений, коли запит провалився. Якщо сторінка без входу працює, а сторінка з входом — ні, ви дивитеся на сесії. Якщо падає все, зокрема й сторінка, яка не просить нічого особистого, ви дивитеся на ключ API, а ваш проєкт — зі старих.
Якщо йдеться про сесії, а лог Auth ще й показує Invalid Refresh Token, ваших
користувачів викидає тому, що
сесія за їхнім токеном завершилася.
Якщо відповідь прийшла від Supabase Auth і звучить як invalid claim: missing sub claim, токен пройшов перевірку підпису й нікого не називав. Це ключ,
надісланий туди, де має бути токен людини, і
це повідомлення має окремий допис.
Invalid JWT — це не "No API key found in request"
Це два різні повідомлення з двома різними причинами, і виправлення в них не мають нічого спільного.
Invalid JWT означає, що токен прийшов і підпис не збігся. "No API key found in
request" означає, що заголовка apikey не
прийшло взагалі, тож запит відхилили на шлюзі ще до того, як щось запитали у
вашої бази.
Сама відповідь і вирішує, що саме у вас. Відсутній ключ відхиляє шлюз перед
вашим проєктом, тож тіло відповіді несе message і hint і жодного code.
Invalid JWT зайшов достатньо далеко, щоб його прочитав PostgREST, і тому
повертається з написаним PGRST301.
Де ще лежить старий ключ, по порядку
Чотири місця, і перевіряти їх варто саме в цьому порядку, бо він приблизно відповідає тому, як часто кожне з них виявляється відповіддю.
- Налаштування проєкту у вашому конструкторі. Lovable, Bolt, v0 і Replit тримають кожен свій набір змінних середовища, а якщо ви будували з Cursor чи Claude Code, той самий набір лежить там, куди ви деплоїте. Це та копія, яку оновлює більшість, і єдина, яку оновлює більшість.
- Ключ, вписаний просто в код. Він їде всередині JavaScript, який завантажують ваші відвідувачі, тож зміна в панелі нічого не змінює, доки файл не відредагують і сайт не перезберуть. Де ця сторінка і що робить кожне значення на ній, якщо ви її ще не відкривали.
- Секрети Edge Function. Функція тримає власний набір, окремий від набору вашого фронтенду, тож вона й далі користується старим ключем, коли все решта вже переїхало. Supabase також попереджає, що функція з увімкненим налаштуванням Verify JWT може зламатися під час ротації ключа підпису.
- Деплой, який ніхто не перезібрав. Ключ запекли всередину під час останнього збирання, тож робоча копія надсилає його й далі, доки щось не запустить нове збирання. Ваша панель при цьому весь час показує новий ключ, і саме це робить цей випадок важким: місце, куди ви йдете перевіряти, — єдине, де вже все правильно.
Який ключ ваш живий застосунок насправді надсилає
Відкрийте опублікований сайт, а не попередній перегляд, і прочитайте, що сторінка надсилає.
Старий ключ Supabase — це довгий рядок, що починається з eyJ. Новіші
повідомляють про себе першими символами: sb_publishable_ місце у вашому
застосунку, а sb_secret_ — ніколи. Якщо знайдете другий на сторінці, яку може
завантажити відвідувач, облиште цю статтю і
замініть його раніше за все інше. Який
із двох у вас і
що змінилося, коли Supabase їх
перейменував, варто з'ясувати, перш ніж починати
підмінювати значення.
Щоб побачити, що ваш проєкт видає зараз: усі ключі тепер лежать в одному місці панелі, у Settings, а тоді API Keys, разом зі старими.
Якщо вам не хочеться читати мініфікований бандл, наше безкоштовне сканування читає ваш живий застосунок так, як його прочитав би сторонній, і називає ключі, видимі ззовні. Це займає близько 20 секунд і не потребує облікового запису: просканувати застосунок.
Ротація ключа підпису без того, щоб вилогінити всіх
Який із двох шляхів ротації ви обрали, і вирішує, чи помітять це ваші користувачі.
Документація Supabase про ключі підпису ставить їх поруч. Ротація старого
JWT-секрета одразу вилогінює всіх активних користувачів, а anon і
service_role доводиться ротувати одночасно. Новіші JWT signing keys
працюють інакше: новий ключ починає як standby, ротація робить його поточним, а
той, якого він замінив, стає попереднім. Доки це так, підписи обох ключів і далі
приймаються, тож листи, запечатані минулого тижня, усе ще відкриваються. Відкликання
старого ключа — це окремий крок, який цьому кладе край.
Два попередження в тій самій документації й ловлять людей:
- Компонент, який сам перевіряє токени проти старого JWT-секрета, бібліотекою на кшталт jose чи jsonwebtoken, ламається під час ротації. Він тримає старий штемпель, і ніхто не сказав йому змінити його.
- Edge Function з увімкненим Verify JWT може зламатися з тієї самої причини.
Створення ключів publishable і secret теж не відкликає ваших старих ключів. Вони працюють, доки ви їх не вимкнете, і документація позначає це як окремий крок. Тож застосунок, що переніс лише половину своїх копій, працює далі тижнями, а день, коли хтось вимикає стару пару, стає днем, коли друга половина починає відповідати 401.
Як наглядати за застосунком, що почав відповідати 401
Застосунок, який віддає 401 на кожен запит, і далі доступний, і далі віддає сторінку, і далі відповідає на будь-яку перевірку працездатності. У вашій панелі нічого не червоніє. Ви дізнаєтеся про це, коли вам хтось напише.
Reeve Monitor дізнається першим:
- усі дев'ять перевірок щогодини, для трьох застосунків
- лист того дня, коли повторне сканування оцінить застосунок гірше за попереднє
- чи застосунок відповідає, кожні 60 секунд
- місячний звіт про побачене
Ключ, який довелося міняти поспіхом, зазвичай був ключем, що лежав не там, де мав, і звідси ви не побачите, що ним прочитали чи змінили, поки він був назовні. Reeve Care тримає копію вашої бази Supabase, зроблену до цього.
- зашифрована копія за розкладом, на власному сховищі Reeve у ЄС, поза вашим акаунтом Supabase
- кожна копія перевіряється, перш ніж її зарахувати, підрахунком рядків у кожній таблиці
- відновлення одним кліком, яке зберігає теперішній стан, перш ніж його замінити
- також ваші завантажені файли, щойно ви під'єднаєте доступ до Storage
- усе, що вміє Monitor
Що зробити сьогодні
Що робити
- Читайте поле
code, а не фразу.PGRST301— це підпис, який не збігся,PGRST303— токен, чий вміст відхилили, наприклад протермінований. - З'ясуйте, який саме токен не пройшов, перш ніж міняти ключ. Якщо сторінки без входу працюють, а сторінки з входом — ні, це сесія, а не ваш ключ API.
- Перевірте всі чотири місця, де ховається старий ключ: налаштування конструктора, сам код, секрети Edge Function і деплой, який ніхто не перезібрав.
- Робіть заміну за один підхід. Кожна залишена копія — це застосунок, що працює наполовину, і живий сайт надсилає старий ключ, доки його не перезберуть.
- На нових ключах підпису саме відкликання припиняє старі підписи. Сама лише ротація лишає їх дійсними.
Почніть із питання, чи сторінка без входу досі завантажується, бо ця одна відповідь вирішує, яка половина статті стосується вас. Якщо виявиться, що secret-ключ лежав відкрито, його заміна описує порядок дій, а які ключі взагалі було безпечно публікувати розповідає, чому ці двоє стали такі схожі.
Поширені запитання
Що означає PGRST301?
PGRST301 — це відповідь PostgREST із HTTP 401, бо підписаний токен у вашому запиті не вдалося перевірити. PostgREST — це сервер, який перетворює ваші таблиці Supabase на HTTP API, з яким розмовляє ваш застосунок. Його власний довідник помилок описує цей код як отриманий JWT, який не вдалося декодувати або який недійсний, а з версії 13 поле message несе точнішу фразу про те, яка саме частина перевірки не пройшла.
Чому я отримую 401 після заміни ключа Supabase?
Бо десь досі надсилається копія старого ключа. Звичні місця — налаштування проєкту у вашому конструкторі, ключ, вписаний просто в код, секрет Edge Function і деплой, зібраний до заміни й відтоді не перезібраний. Токен, підписаний ключем, який сервер уже не приймає, не проходить перевірку, і до вас це доходить як 401 на кожен запит, а не як повідомлення про ключ.
Чим це відрізняється від No API key found in request?
Це два різні повідомлення з різними причинами. No API key found in request означає, що запит прийшов узагалі без заголовка apikey і його відхилили на шлюзі ще до того, як щось запитали у вашої бази. Invalid JWT означає, що токен був і його підпис не збігся. Перше — це лист без адреси, друге — лист із не тією печаткою.
Чи треба оновити ще й мої Edge Functions?
Так, і саме цю копію забувають найчастіше, бо Edge Function тримає власні секрети, окремо від тих, якими користується ваш фронтенд. Supabase також попереджає, що функція з увімкненим налаштуванням Verify JWT може зламатися під час ротації ключа підпису, і що будь-який компонент, який сам перевіряє токени проти старого JWT-секрета, зламається з тієї самої причини.
Як побачити, який ключ насправді надсилає мій застосунок у продакшені?
Відкрийте опублікований сайт, а не попередній перегляд, і подивіться, що сторінка насправді надсилає. Старий ключ Supabase — це довгий рядок, що починається з eyJ, новіші починаються з sb_publishable_ або sb_secret_. Наше безкоштовне сканування читає живий застосунок так, як його прочитав би сторонній, і називає ключі, видимі ззовні, а отже й показує, чи досі надсилається старий.