Sicherheitsgrundlagen
"No API key found in request" in Supabase, und die falsche Lösung
"No API key found in request" heißt, deine Supabase-Anfrage kam ohne Schlüssel an. Die meisten Antworten zeigen stattdessen auf deine Datenbankregeln.

Kurz gesagt
- "No API key found in request" heißt, deine Anfrage hat Supabase ohne apikey-Header erreicht und wurde am Gateway abgewiesen, bevor deine Datenbank überhaupt gefragt wurde.
- Die richtige Lösung ist dein Publishable Key in genau diesem Header, also der Schlüssel, der öffentlich sein soll. Die Client-Bibliothek von Supabase schickt ihn bei jeder Anfrage für dich mit.
- Die zwei Lösungen, zu denen Leute stattdessen greifen, sind der geheime Schlüssel und eine lockerere Regel auf der Tabelle. Beide beenden die Meldung, und beide machen deine Zeilen für jeden lesbar, der den Schlüssel aus deiner App hat.
Gestern lief deine App noch. Heute bleibt eine Liste leer, die sich sonst mit Zeilen füllt, und wenn du die Browser-Konsole öffnest, steht dort eine kurze Antwort von Supabase, wo die Daten sein sollten:
{
"message": "No API key found in request",
"hint": "No `apikey` request header or url param was found."
}
Mehr bekommst du nicht. Es steht nicht da, welche Tabelle du gelesen hast, was du geschickt hast oder was du ändern sollst.
Hier ist der Punkt, den Anleitung um Anleitung falsch darstellt: "No API key found in request" handelt von einem fehlenden Header, und die meisten Ratschläge, die du findest, handeln von deinen Datenbankregeln. Im Diskussionsforum von Supabase hat diese Meldung einen eigenen Thread, sechzehn Leute schlagen darin acht verschiedene Mittel vor, und drei davon sagen, du sollst eine Regel auf einer deiner Tabellen ergänzen oder lockern. Alle, die es probiert haben, berichten, dass die Meldung aufhörte. Ob eine Regel jemals die Ursache war, ist eine andere Frage, und der Thread klärt sie nie. Sicher ist danach nur, dass mehr Leute die Tabelle lesen können als vorher.
Was "No API key found in request" bedeutet
Supabase hat deine Anfrage an der Haustür abgewiesen, bevor deine Datenbank überhaupt gefragt wurde.
Jede Anfrage an dein Projekt landet zuerst an einem Gateway, das Supabase vor
alles andere stellt. Seine Aufgabe ist in diesem Moment eng: den apikey-Header
lesen, den Wert gegen die Schlüssel deines Projekts prüfen und die Anfrage
weiterreichen. Wenn es keinen Schlüssel zu lesen gibt, hört es dort auf und
antwortet 401, dem Statuscode für "Ich weiß nicht, wer du bist". Die
Gateway-Referenz von Supabase sagt selbst, dass ein fehlender oder ungültiger
Schlüssel so abgewiesen wird.
Stell dir das Gateway als Pförtner an der Haustür vor, nicht als Schloss an einem Aktenschrank. Der Pförtner bittet jeden, der ankommt, um einen Ausweis. Die Regeln, die entscheiden, wer welchen Schrank öffnen darf, liegen zwei Stockwerke höher, und hier hat sie niemand zu Rate gezogen, weil nichts am Eingang vorbeikam.
Für einen Fehler ist dieser also mild. Nichts wurde gelesen, nichts geschrieben, und deine Daten sind genau so, wie sie waren. Eine Anfrage wurde abgewiesen.
Warum kam meine Anfrage ohne Schlüssel an?
Weil der Wert, den deine App schicken sollte, in der Anfrage fehlte, die dein Browser tatsächlich gestellt hat. Vier Ursachen decken fast alle Fälle ab.
Der Schlüssel kam nie in der gebauten App an. Dein Code liest ihn aus einer
Umgebungsvariablen, der Build, aus dem deine Live-Seite entstanden ist, hatte
diese Variable nicht gesetzt, und createClient bekam eine leere Zeichenkette.
Alles kompiliert, die App lädt, und jede Anfrage geht ohne Schlüssel hinaus. In
einem Vite- oder Next-Projekt muss die Variable außerdem das Präfix tragen, das
sie als browsersicher markiert, was
eine eigene Falle ist.
Du rufst den REST-Pfad von Hand auf. Ein fetch gegen
/rest/v1/deine_tabelle schickt die Header, die du getippt hast, und sonst
nichts. Die Client-Bibliothek von Supabase ergänzt apikey für dich; eine von
Hand geschriebene Anfrage hat das, was du ihr mitgegeben hast.
Etwas dazwischen hat ihn verworfen. Eine Rewrite-Regel, ein Proxy oder ein eigenes API-Gateway sitzt zwischen deiner App und Supabase und leitet die Anfrage ohne den Header weiter.
Du siehst eine Weiterleitung und keine Datenanfrage. Wenn die Meldung
erscheint, nachdem sich jemand angemeldet oder auf einen Bestätigungslink geklickt
hat, und in der Adressleiste noch deine Supabase-URL steht, ist die Einstellung,
die du brauchst, die Site URL unter Auth. Die Antwort mit den meisten Reaktionen
im ganzen Supabase-Thread kommt von jemandem, der seine Seitenadresse dort ohne
https:// davor eingetragen hatte.
Die Lösung, in einer Zeile
Schick deinen Publishable Key im apikey-Header.
import { createClient } from '@supabase/supabase-js'
export const supabase = createClient(
'https://dein-projekt.supabase.co',
'sb_publishable_…',
)
Legst du den Client so an, setzt die Bibliothek den Schlüssel bei jeder Anfrage in
den richtigen Header, und du tippst den Header nie selbst. Die Referenz von
Supabase ist eindeutig, welcher Header das ist: Publishable und Secret Keys reisen
im apikey-Header statt in Authorization: Bearer, denn sie sind kurze,
undurchsichtige Zeichenketten, und alles, was einen davon als JWT prüfen will,
scheitert.
Dieser Schlüssel gehört in deine App. Er sagt, zu welchem Projekt eine Anfrage gehört, und was ein Besucher mit ihm erreicht, entscheiden die Regeln auf deinen Tabellen, weshalb er überhaupt öffentlich sein darf. Welche API-Schlüssel im Frontend sicher sind geht den Rest der Familie durch.
Die Lösung, die alles schlimmer macht
Der geheime Schlüssel passt in denselben Header, und bei einem älteren Projekt beendet er die Meldung und arbeitet weiter.
Das ist ein einziger Fehlklick. Dein Dashboard listet beide Schlüssel auf derselben Seite, und das ältere Paar sieht gleich aus: gleiche Form, gleiche Länge, einer unter dem anderen. Wie schlimm der Klick ist, hängt davon ab, welches Paar dein Projekt ausgibt.
Ein neuerer geheimer Schlüssel wird im Browser abgewiesen. Supabase blockiert
sb_secret_… anhand des User-Agent-Headers und antwortet 401. Ihn ins Frontend
zu kopieren beendet den Fehler also nicht, was hier das beste verfügbare Ergebnis
ist.
Ein alter service_role-Schlüssel hat keine solche Sperre. Er funktioniert.
Die Liste füllt sich, die App verhält sich genau wie vorige Woche, und der
Schlüssel liegt jetzt in einer Datei, die jeder Besucher herunterladen kann. Dort
ignoriert er jede Regel, die du geschrieben hast: service_role trägt das
Postgres-Attribut BYPASSRLS, eine Policy gilt für ihn also nie. Jede Zeile in
jeder Tabelle, lesbar und schreibbar für den, der die Datei öffnet.
Die Anmerkung von Supabase zur Browser-Sperre lohnt zweimal gelesen zu werden. Die Sperre antwortet 401, und ein Angreifer kann den Schlüssel trotzdem mit anderen Werkzeugen benutzen. Geschützt sind die Anfragen deiner eigenen App; derselbe Schlüssel in derselben Datei beantwortet eine Anfrage aus einem Skript.
Der andere Irrweg, und warum er so naheliegt
Eine Regel auf der Tabelle zu lockern beendet die Meldung ebenfalls, und es ändert, wer deine Zeilen lesen kann.
Hier ist dieser Supabase-Thread vollständig, gruppiert danach, was die jeweilige Antwort dich ändern lässt:
| Was geändert werden soll | Wie viele der acht Antworten | Was das tatsächlich ändert |
|---|---|---|
| Die Site URL unter Auth | 1 | Wo eine Anmelde-Weiterleitung landet |
| Eine Policy oder ein Grant | 3 | Wer deine Zeilen lesen und schreiben darf |
| Die Abfrage oder die Session | 3 | Dein eigener Code |
Der apikey-Header | 1 | Wonach das Gateway gefragt hat |
Die letzte Zeile ist die, die den Hinweis in der Meldung beantwortet. Sie ist der unterste Kommentar im Thread und hat eine einzige Stimme.
Keine der anderen sieben ist in böser Absicht geschrieben, und genau das macht es schwer. Eine Meldung erscheint wirklich in mehreren Situationen, die antwortende Person hat ihre eigene App wirklich zum Laufen gebracht, und eine Policy, die ohnehin fehlte, ist wirklich etwas zu reparieren. Schief geht die Reihenfolge: Eine Regel wird umgeschrieben, um eine Meldung über einen Header zu beenden, und die Regel ist der Teil dieses Paares, den danach niemand prüft. Deine App läuft so oder so, also sagt dir nichts, welches von beidem du getan hast.
Von außen ist das Ergebnis sichtbar. Unser Scanner liest Live-Apps so, wie ein Fremder es täte, und von den 31.056 Apps, die er bewertet hat, haben drei einen geheimen Supabase-Schlüssel veröffentlicht. Damit ist es der seltenste Fund, den wir haben, und der schwerste.
Der Reflex daneben ist viel häufiger. Hinter 8.435 dieser Apps haben wir ein Supabase-Projekt gefunden. Bei 3.680 davon bekam die Frage "kann ein Fremder diese Tabelle lesen" überhaupt eine brauchbare Antwort, und 2.096 antworteten mit ja: Mindestens eine Tabelle gab Zeilen an eine Anfrage von außen heraus, bei der nirgends ein Konto beteiligt war. Bei 394 davon war die offene Tabelle wie eine Tabelle mit Menschen benannt.
Ein Teil davon ist Absicht. Eine veröffentlichte Speisekarte, eine Seite mit Anzeigen und ein öffentliches Changelog stehen alle in Tabellen, die ein Fremder lesen soll, und unser Scanner kann sie nicht von einer Kundentabelle unterscheiden, die versehentlich geöffnet wurde. Er berichtet, was geantwortet hat, und überlässt das Urteil dir, weshalb Row Level Security anzuschalten nicht dasselbe ist wie geschützt zu sein.
Wenn du deine eigene Antwort lieber sehen als erschließen willst: Unser kostenloser Scan liest deine Live-Seite und sagt dir, was er von außen erreicht. Er dauert rund 20 Sekunden und braucht kein Konto: App scannen.
Woran du erkennst, welchen Schlüssel du eingefügt hast
Lies den Anfang der Zeichenkette.
sb_publishable_… gehört in deine App. sb_secret_… niemals. Es gibt nichts zu
decodieren, denn wofür der Schlüssel da ist, steht vorne drauf.
Ein langer Wert, der mit eyJ beginnt, ist einer aus dem älteren Paar, und genau
dort werden die beiden schwer zu unterscheiden. Der mittlere Abschnitt eines
solchen Schlüssels ist lesbare Information und keine Verschlüsselung, und er trägt
ein Feld, das die Frage entscheidet:
{
"iss": "supabase",
"role": "anon", ← das ist das entscheidende
"iat": 1750000000
}
anon ist der veröffentlichbare der beiden. service_role ist der, den du heute
rotieren solltest. Die Dokumentation von Supabase sagt es deutlicher, als wir es
täten: Wenn ein Tutorial oder ein KI-Assistent dir sagt, du sollst einen langen
Schlüssel kopieren, der mit eyJ beginnt, wurde es für die alten Schlüssel
geschrieben.
Beide Paare können gleichzeitig aktiv sein, und das ist das Detail, über das Leute
stolpern. Einen Publishable oder Secret Key anzulegen lässt deine anon- und
service_role-Schlüssel genau dort, wo sie waren, und sie funktionieren weiter,
bis du sie im Dashboard in einem eigenen Schritt deaktivierst.
Was sich mit den neueren Schlüsseln geändert hat
behandelt diese Umstellung, und
wo jeder Wert in deinem Dashboard liegt
ist die Seite, die du öffnest, falls du nie dort warst.
Wenn der geheime Schlüssel schon in deiner App steckt
Rotiere ihn, bevor du irgendetwas änderst, und arbeite in der Reihenfolge, die Supabase vorgibt.
Lege im Dashboard einen neuen geheimen Schlüssel an. Ersetze den alten überall,
wo dein Projekt ihn benutzt. Prüfe, dass jeder Teil deiner App auf dem neuen
Schlüssel läuft. Erst dann nimm den alten außer Betrieb, und der letzte Schritt
unterscheidet sich danach, welches Paar du hältst: Einen Secret Key zu löschen
lässt sich nicht rückgängig machen, während das Deaktivieren eines alten anon-
und service_role-Paares reversibel ist, du kannst sie also wieder anschalten,
wenn du einen Client übersehen hast.
Ob du zuerst rotierst oder zuerst das Leck reparierst, hängt davon ab, ob eine Kopie schon öffentlich ist. Einen Supabase-service_role-Schlüssel rotieren geht beide Reihenfolgen durch und sagt, was danach in deinen Logs steht.
So bleibt es, wenn die Fehlermeldung weg ist
Die Korrektur hält bis zum nächsten Prompt oder zur nächsten Änderung an deinem Supabase-Setup. Ein einziger Prompt kann einen Schlüssel wieder hineinsetzen oder eine Regel wieder lockern, und deine App läuft in beiden Fällen weiter, also fällt die Änderung erst auf, wenn jemand nachsieht.
Reeve Monitor sieht für dich nach:
- alle neun Prüfungen stündlich, für bis zu drei Apps
- eine E-Mail an dem Tag, an dem ein erneuter Scan deine App schlechter bewertet als der vorige
- ob die App läuft, alle 60 Sekunden
- ein Monatsbericht über das, was er gesehen hat
Ein Schlüssel, der jede Zeile schreiben kann, kann auch jede Tabelle leeren. Reeve Care bewahrt eine Kopie deiner Supabase-Datenbank dort auf, wo kein Schlüssel aus deiner App hinkommt.
- jeden Tag eine verschlüsselte Kopie, aufbewahrt außerhalb deines Supabase-Kontos
- jede Kopie geprüft, bevor sie zählt: Wir zählen die Zeilen jeder Tabelle nach
- eine Wiederherstellung per Klick, die den aktuellen Stand sichert, bevor sie etwas ersetzt
- auch deine hochgeladenen Dateien, sobald du einen Storage-Zugang verbindest
- alles, was Monitor macht
Was du heute tun kannst
Was zu tun ist
- Lies die Meldung als Abweisung an der Tür. Deine Zeilen wurden nicht angefasst und es muss nichts wiederhergestellt werden.
- Schau im Netzwerk-Tab des Browsers die fehlgeschlagene Anfrage an und prüfe, ob überhaupt ein
apikey-Header dabei ist. Dieser eine Blick sagt dir, ob es ein Schlüsselproblem oder ein Weiterleitungsproblem ist. - Lege deinen Supabase-Client mit dem Publishable Key an und lass die Bibliothek den Header schicken.
sb_publishable_…bei einem neueren Projekt, deranon-Schlüssel bei einem älteren. - Steckt schon ein geheimer Schlüssel in deiner App, rotiere ihn zuerst. Ihn aus dem Code zu löschen schließt die Tür nicht, denn die alte Fassung steht weiter in deiner Versionsgeschichte und in jeder zwischengespeicherten Kopie deiner Seite.
- Setz jede Regel zurück, die du auf der Jagd danach gelockert hast, und prüfe sie dann von außen statt aus der Vorschau deines Builders.
Fang mit der einen Tabelle an, aus der der Fehler kam, und lies danach die Policies jeder Tabelle, die du in derselben Woche angefasst hast. Die 10-Minuten-Sicherheitscheckliste deckt ab, was in einer frisch gestarteten App sonst noch offen bleibt, und der Supabase-Sicherheitsleitfaden geht den Rest durch, was ein Fremder erreichen kann.
FAQ
Was bedeutet "No API key found in request"?
Es bedeutet, dass deine Anfrage Supabase ohne apikey-Header erreicht hat, sodass das Gateway vor deinem Projekt sie abgewiesen hat, bevor deine Datenbank beteiligt war. Nichts wurde gelesen, nichts geschrieben, und an deinen Daten hat sich nichts geändert. Die Antwort trägt einen Hinweis, der dasselbe mit anderen Worten sagt: no apikey request header or url param was found.
Welcher Supabase-Schlüssel gehört in den apikey-Header?
Dein Publishable Key, bei einem neueren Projekt also sb_publishable_ und bei einem älteren der anon-Schlüssel. Supabase stellt ihn genau dafür bereit und rechnet damit, dass er in deiner App sichtbar ist. Die Client-Bibliothek setzt ihn bei jeder Anfrage in den Header, also tippst du den Header nie selbst, wenn du den Client mit dem Publishable Key anlegst.
Ist es sicher, meinen anon-Schlüssel im Browser zu haben?
Ja, und er muss dort sein, damit deine App überhaupt mit deinem Projekt sprechen kann. Der anon-Schlüssel sagt nur, zu welchem Projekt eine Anfrage gehört; was ein Besucher mit ihm tatsächlich erreicht, entscheiden die Row-Level-Security-Regeln auf deinen Tabellen. Welche API-Schlüssel im Frontend sicher sind geht die ganze Familie der Schlüssel durch und zeigt, wie man sie liest.
Ich habe den service_role-Schlüssel benutzt und es hat funktioniert. Ist das ein Problem?
Ja, und es ist das, was sich heute zu erledigen lohnt. Ein service_role-Schlüssel trägt das Postgres-Attribut BYPASSRLS, deine Policies gelten für ihn also nie. In deiner App ausgeliefert liegt er in einer Datei, die jeder Besucher herunterladen kann, und wer sie herunterlädt, kann jede Zeile in jeder Tabelle lesen und schreiben. Rotiere den Schlüssel zuerst und verschiebe danach das, wofür er gebraucht wurde, auf einen Server.
Woran erkenne ich, welchen Schlüssel ich eingefügt habe?
Lies den Anfang der Zeichenkette. sb_publishable_ gehört in deine App und sb_secret_ niemals. Ein langer Wert, der mit eyJ beginnt, ist einer aus dem älteren Paar, und die beiden sehen gleich aus, also musst du hineinschauen: Der mittlere Abschnitt decodiert zu lesbaren Daten mit einem role-Feld, und darin steht entweder anon oder service_role.