Supabase Auth · 401
Invalid API key (Supabase) — which key belongs where
Luca Urti
Invalid API key
Why does Supabase return “Invalid API key”?
The key you sent does not belong to the project you sent it to, or it did not arrive at all. In practice it is one of four things: the key and the project URL come from different Supabase projects, the environment variable is undefined at runtime and the client is sending the literal string undefined, the value was truncated or wrapped when it was pasted into an .env file, or the key was rotated in the dashboard and the deployment still holds the old one. It is not a permissions problem — a valid key with no rights produces an RLS error or an empty result, never this.
Check the environment variable actually reached the bundle
The most common cause in a deployed app is a variable that exists locally and not on the host. In Next.js only names prefixed with NEXT_PUBLIC_ are inlined into client code; in Vite it is VITE_. A client-side createClient reading a variable without the prefix receives undefined, and the request goes out with that as the key.
Log the key's length and last four characters, never the key. If the length is wrong the paste was truncated; if it is zero the variable is missing; if it is right, compare the project ref inside the JWT against the ref in your project URL.
Which key belongs where
The anon (publishable) key belongs in the browser. It is public by design, it is constrained by RLS, and it is the correct key for anything running on a user's device. The service_role (secret) key belongs only on a server you control, in an environment variable with no public prefix, and it bypasses RLS completely.
There is no third option and no middle ground. If a request needs more rights than the anon key has, the work belongs on the server — not in a client holding a stronger key.
The fix
confirm the key without printing it
// Safe to leave in during debugging: length and suffix identify a key
// without disclosing one. Never log the key itself — logs get shared,
// pasted into issues, and shipped to third-party error trackers.
const key = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY;
console.log("supabase key:", key?.length ?? "MISSING", key?.slice(-4));
console.log("supabase url:", process.env.NEXT_PUBLIC_SUPABASE_URL);
// The project ref is encoded in the JWT payload — it must match the URL.
// echo "$KEY" | cut -d. -f2 | base64 -dThe fix that works and costs you the database
The service_role key never produces this error, so swapping it in “just to check whether the key is the problem” always works — and that temporary diagnostic is how the key ends up committed. It bypasses RLS on every table in the project, and in a client bundle it is readable by anyone. If you have done this even briefly and pushed it, rotate the key in the dashboard: a key that has been in a public repository is compromised whether or not the commit was reverted, because the object stays in the git history and GitHub's commit API serves it.
Whether the trap is already in your repo is a question you can answer
Sentris reads the SQL and the client code, so it reports the shortcut above where it was actually taken — a service_role key in a browser bundle, a policy that is using (true), a table with RLS switched off. A scan needs no account and no card. How often it is wrong is measured and published.