Supabase "__cf_bm" Cookie Rejected for Invalid Domain: Fix
The warning fires on every image, upload and WebSocket that touches <ref>.supabase.co in Firefox. It is Cloudflare's bot-management cookie being scoped to a Public Suffix List domain, so every browser drops it. Here is the verified cause, the cases where the noise hides a real 403 or auth bug, and how to tell them apart in two minutes.
Photo by Sasun Bughdaryan on Unsplash
Open any file served from a Supabase Storage bucket in Firefox and the console prints one line per asset:
Cookie "__cf_bm" has been rejected for invalid domain.That is the warning in
the GitHub issue supabase/supabase#37312,
reported for a public image loaded through the Storage image-transformation
endpoint (/storage/v1/render/image/public/...) on a <ref>.supabase.co
project. The thread since collected the same line on plain object URLs,
bucket uploads, and the Realtime WebSocket at /realtime/v1/websocket. The
reporter called it "inconsequential, but it happens for each image loaded on
the page", and that is the right summary: the warning is harmless, and it is
also the first thing you see when something else has gone wrong, which is why
it keeps getting blamed.
This article explains, with the actual response headers and the actual Public Suffix List entry, why the cookie is rejected by every browser, then walks through the four situations from the thread where the warning sat on top of a real failure.
Root cause: a Cloudflare cookie scoped to a public suffix#
Three verifiable facts combine here.
1. __cf_bm is Cloudflare's cookie, not Supabase's. Cloudflare's
cookie reference
states that it "places the __cf_bm cookie on end-user devices that access
customer sites protected by Bot Management or Bot Fight Mode", that it
"expires after 30 minutes of continuous inactivity", and that "a separate
__cf_bm cookie is generated for each site that an end user visits".
Supabase's API domain sits behind Cloudflare, so every response from Storage,
Auth, PostgREST and Realtime can carry it.
2. The cookie is scoped to supabase.co. Requesting the image from the
issue with curl -sI on 6 September 2026 returns HTTP/1.1 200 OK,
Server: cloudflare, and this header (value shortened):
set-cookie: __cf_bm=zjsStpIsHSib...; HttpOnly; SameSite=None; Secure;
Path=/; Domain=supabase.co; Expires=Sun, 06 Sep 2026 23:34:48 GMTA commenter pasted the same shape a year earlier with domain=.supabase.co.
Note the Domain attribute: the apex, not the project subdomain.
3. supabase.co is on the Public Suffix List. The
PSL carries these
lines in its private section:
// Supabase : https://supabase.io
// Submitted by Supabase Security <[email protected]>
supabase.co
realtime.supabase.co
storage.supabase.co
supabase.in
supabase.netSupabase submitted its own domain so that each project ref is treated as a
separate site, the same way vercel.app, pages.dev and netlify.app are
listed. The point is tenant isolation: a cookie set by
project-a.supabase.co must never be readable by project-b.supabase.co.
Put the three together. Cloudflare answers from <ref>.supabase.co with a
cookie whose Domain is supabase.co. Browsers implement the cookie
processing rules in RFC 6265 section 5.3:
when the Domain attribute is a public suffix and does not exactly match the
request host, the cookie is ignored. Firefox logs that decision as "rejected
for invalid domain". Chromium makes the same decision silently, which is why a
commenter saw only an orange warning icon on the Set-Cookie header in
Chrome's Network panel and no console line.
So this is not a Firefox bug, not a Storage bug, and not something your
@supabase/supabase-js configuration touches. It is Supabase's own PSL entry
doing its job against a cookie it never asked for. The issue is labelled
external-issue and internal-fix for that reason; the change has to happen
in how Cloudflare is configured for the zone, and until it does, every
supabase.co project shows the same line.
Why it is harmless for Storage, Auth and Realtime#
The rejection happens after the response has arrived. Nothing about the request, the status code, or the body changes.
- Storage. Public object URLs and
render/imageURLs are unauthenticated GETs. The image in the issue returns200withContent-Type: image/jpegalongside the rejected cookie. Signed URLs carry their token in the query string, not in a cookie. - Auth and PostgREST.
supabase-jsnever relies on a cookie on the API domain. It sends your anon or publishable key in theapikeyheader and the user's JWT inAuthorization: Bearer. The only auth cookies that matter,sb-<ref>-auth-token, are written by@supabase/ssron your domain (www.example.eu, notsupabase.co). - Realtime. The WebSocket upgrade response carries the same
Set-Cookie. Dropping it has no effect on the upgraded connection, because the socket is already established and authenticates by sending the access token over the channel.
If everything on the page works and the only symptom is console volume,
you are done: add -__cf_bm to the Firefox console filter box during
development and move on.
When the warning is sitting on a real failure#
Every "it broke" report in the thread turned out to have a second, quieter error underneath. Check these before you spend another minute on the cookie.
403 "Feature not enabled in tenant" on image transforms#
One commenter's images stopped loading entirely and returned
403 Feature not enabled in tenant from /storage/v1/render/image/....
Removing the transform brought the assets back. Supabase's
image transformation docs
list the feature as Pro plan and above. A project downgraded to Free (or a
new project created on Free after copying URLs from a paid one) keeps working
for /object/public/ URLs and fails for /render/image/ ones.
Fix: upgrade the project, or drop the transform option so getPublicUrl
returns the untransformed object URL:
import { createClient } from '@supabase/supabase-js'
const supabase = createClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY!
)
// Pro plan and above only:
const transformed = supabase.storage
.from('artwork')
.getPublicUrl('6f55179b.jpg', { transform: { width: 800, quality: 75 } })
// Works on every plan:
const plain = supabase.storage.from('artwork').getPublicUrl('6f55179b.jpg')If you need resizing on the Free plan, resize on upload with sharp in a
Route Handler and store the variants; the
Storage upload guide
covers that pipeline.
Uploads failing with restricted MIME types#
A June 2026 comment describes uploads of .kicad_pcb files failing with the
cookie line in Firefox and failing silently in Chromium, until the bucket's
"Restrict MIME types" toggle was disabled. The cookie was irrelevant; the
upload was rejected by the bucket's allowed-MIME-type list because the file
was detected as binary/octet-stream, which was not on the list. Read the
JSON body of the failed POST /storage/v1/object/... response, then either
add the detected type to the allow-list or pass an explicit contentType to
upload(). A 403 with new row violates row-level security policy on the
same request is a different problem, covered in
the Storage RLS fix.
Realtime "prevents real-time functionality from working"#
A November 2025 comment blamed the cookie for a Realtime WebSocket that never
delivered events. The Set-Cookie on the handshake cannot do that. Look at
the WebSocket frames in the Network panel instead: a CHANNEL_ERROR or a
close code points at Realtime authorisation (RLS on the table, private
channels without a policy, or a stale token), and the
Realtime not receiving events checklist
walks through each one.
"Auth session missing" in a Next.js server component#
The one place where a cookie really is the problem is server-side auth, and
it is your cookie, not Cloudflare's. If getClaims() or getUser() returns
nothing on the server while the browser is logged in, check that
sb-<ref>-auth-token exists on your own domain in DevTools, and that your
server client is wired the way the current @supabase/ssr example does it:
// utils/supabase/server.ts
import { createServerClient } from '@supabase/ssr'
import { cookies } from 'next/headers'
export async function createClient() {
const cookieStore = await cookies()
return createServerClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY!,
{
cookies: {
getAll() {
return cookieStore.getAll()
},
setAll(cookiesToSet) {
try {
cookiesToSet.forEach(({ name, value, options }) =>
cookieStore.set(name, value, options)
)
} catch {
// Called from a Server Component; the proxy refreshes sessions.
}
},
},
}
)
}Two cookie-domain mistakes produce a symptom that looks like this thread:
- Passing
cookieOptions: { domain: ... }tocreateServerClientwith a value that is itself a public suffix. Preview deployments onvercel.apporpages.devare the usual case:domain: 'vercel.app'is rejected for exactly the reason__cf_bmis, and the session never persists. Leavedomainunset unless you are sharing a session across subdomains of a domain you own. - A
proxy.ts(ormiddleware.tson Next.js 15) that builds a freshNextResponseand forgets to copy the refreshed cookies. The example's comment is blunt: return thesupabaseResponseobject as it is. The AuthSessionMissingError fix and the SSR session guide cover the matcher and cookie-forwarding details, and getClaims() vs getSession() explains which call to trust on the server.
A two-minute triage#
-
Reproduce from the terminal so the browser is out of the picture:
bashcurl -sI "https://<ref>.supabase.co/storage/v1/object/public/<bucket>/<path>" \ | grep -iE "^(HTTP|content-type|set-cookie)"200plus the__cf_bmheader means the asset is fine and the console line is cosmetic. -
Any
4xx: read the JSON body withcurl -s(drop-I).403with "Feature not enabled in tenant" is the plan;403with an RLS message is a policy;400on upload is usually MIME or size. -
Realtime problems: inspect the WebSocket frames, not the handshake headers.
-
SSR auth problems: look for
sb-<ref>-auth-tokenon your domain in the Application panel. If it is missing, the bug is insetAllor the proxy, never insupabase.co.
Production notes#
- Do not buy the custom-domain add-on to make this warning go away. It
changes which hostname serves your API and Storage (
api.example.eu), but whether Cloudflare still attaches aDomainattribute on that zone is not something this article can confirm, and the warning costs you nothing. - If your EU users load many images per page, the Firefox console noise is a
developer-only cost. It does not affect Core Web Vitals, caching, or the
CF-Cache-Statusyou see on the response. - Track the upstream issue for the
internal-fixlabel to close; that is the only path to a clean console onsupabase.codomains.
Related#
Frequently Asked Questions
One email a month — no fluff
RLS gotchas, Next.js cache debugging, and the one Supabase setting that bit me last month.
Continue Reading
Supabase Auth Session Disappears on Refresh? Fix (2026)
Supabase auth sessions mysteriously disappearing after page refresh? Learn the exact cause and fix it in 5 minutes with this tested solution.
Fix Supabase RLS Infinite Recursion (Production 2026)
If your Supabase query returns `infinite recursion detected in policy for relation "X"`, your RLS policy is querying the same table it protects. Here's exactly why it loops, and three production-grade fixes that don't leak data.
Supabase redirectTo Failing on Vercel Previews: SITE_URL Fix
The redirect lands on your production domain instead of the preview URL, or you get a "redirect URL not allowed" error. Here is the exact Supabase dashboard config and the Vercel env var pattern.
Browse by Topic
Find stories that matter to you.
