DEV Community

Cover image for Supabase "__cf_bm" Cookie Rejected for Invalid Domain: Fix
Mahdi BEN RHOUMA
Mahdi BEN RHOUMA

Posted on Originally published at iloveblogs.blog

Supabase "__cf_bm" Cookie Rejected for Invalid Domain: Fix

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.
Enter fullscreen mode Exit fullscreen mode

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 GMT
Enter fullscreen mode Exit fullscreen mode

A 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 <psl-maintainers@supabase.io>
supabase.co
realtime.supabase.co
storage.supabase.co
supabase.in
supabase.net
Enter fullscreen mode Exit fullscreen mode

Supabase 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/image URLs are unauthenticated GETs. The image in the issue returns 200 with Content-Type: image/jpeg alongside the rejected cookie. Signed URLs carry their token in the query string, not in a cookie.
  • Auth and PostgREST. supabase-js never relies on a cookie on the API domain. It sends your anon or publishable key in the apikey header and the user's JWT in Authorization: Bearer. The only auth cookies that matter, sb-<ref>-auth-token, are written by @supabase/ssr on your domain (www.example.eu, not supabase.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')
Enter fullscreen mode Exit fullscreen mode

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.
          }
        },
      },
    }
  )
}
Enter fullscreen mode Exit fullscreen mode

Two cookie-domain mistakes produce a symptom that looks like this thread:

  • Passing cookieOptions: { domain: ... } to createServerClient with a value that is itself a public suffix. Preview deployments on vercel.app or pages.dev are the usual case: domain: 'vercel.app' is rejected for exactly the reason __cf_bm is, and the session never persists. Leave domain unset unless you are sharing a session across subdomains of a domain you own.
  • A proxy.ts (or middleware.ts on Next.js 15) that builds a fresh NextResponse and forgets to copy the refreshed cookies. The example's comment is blunt: return the supabaseResponse object 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

  1. Reproduce from the terminal so the browser is out of the picture:
   curl -sI "https://<ref>.supabase.co/storage/v1/object/public/<bucket>/<path>" \
     | grep -iE "^(HTTP|content-type|set-cookie)"
Enter fullscreen mode Exit fullscreen mode

200 plus the __cf_bm header means the asset is fine and the console
line is cosmetic.

  1. Any 4xx: read the JSON body with curl -s (drop -I). 403 with "Feature not enabled in tenant" is the plan; 403 with an RLS message is a policy; 400 on upload is usually MIME or size.
  2. Realtime problems: inspect the WebSocket frames, not the handshake headers.
  3. SSR auth problems: look for sb-<ref>-auth-token on your domain in the Application panel. If it is missing, the bug is in setAll or the proxy, never in supabase.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 a Domain attribute 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-Status you see on the response.
  • Track the upstream issue for the internal-fix label to close; that is the only path to a clean console on supabase.co domains.

Related


Originally published at https://www.iloveblogs.blog

Top comments (0)