Zum Inhalt springen

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.

Vlad Tkachenko10 Min. Lesezeit
Eine Anfrage trifft auf ein hohes Tor mit leerem Ausweisschlitz und einem Riegel davor, und die Datenbank dahinter, zu der sie wollte, bleibt unberührt.

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.

Das Gateway liest den Schlüssel. Die Regeln deiner Tabelle liegen eine Station weiter, und eine abgewiesene Anfrage erreicht sie nie.

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.

Die Browser-Sperre betrifft den Browser. Derselbe Schlüssel in derselben Datei funktioniert aus allem, was keiner ist.

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 sollWie viele der acht AntwortenWas das tatsächlich ändert
Die Site URL unter Auth1Wo eine Anmelde-Weiterleitung landet
Eine Policy oder ein Grant3Wer deine Zeilen lesen und schreiben darf
Die Abfrage oder die Session3Dein eigener Code
Der apikey-Header1Wonach 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.

Was ein Fremder lesen kann, über die Apps, die geantwortet haben. Das gestrichelte Band sind die Apps, die nichts geantwortet haben, und das ist etwas anderes als eine App, bei der nichts offen ist.

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, der anon-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.

Geschrieben von

Vlad Tkachenko

Gründer, Reeve

Ich schaue mir den ganzen Tag Apps an, die mit Lovable, Bolt, v0, Cursor und Replit gebaut wurden, und die kurze Liste von Fehlern, die darin immer wieder auftauchen.

Mehr über den Autor

Weiterlesen

Alle Artikel

Unsicher, wie es um deine eigene App steht?

Starte einen kostenlosen Scan und erhalte in rund 20 Sekunden eine verständliche Note von A bis F. Ohne Konto, ohne Karte.

App kostenlos scannen

Automatisierte externe Prüfung, kein vollständiges Audit. Das Fehlen von Funden ist keine Garantie für Sicherheit.