Supabase

Cookie "__cf_bm" has been 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.

Mahdi Ben RhoumaMahdi Ben Rhouma
Updated
8 min read

Open any file served from a Supabase Storage bucket in Firefox and the console prints one line per asset:

text
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 three situations from the thread where the warning sat on top of a real failure, plus one auth case that looks similar.

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):

text
set-cookie: __cf_bm=zjsStpIsHSib...; HttpOnly; SameSite=None; Secure;
  Path=/; Domain=supabase.co; Expires=Sun, 06 Sep 2026 23:34:48 GMT

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:

text
// Supabase : https://supabase.io
// Submitted by Supabase Security <psl-maintainers@supabase.io>
supabase.co
realtime.supabase.co
storage.supabase.co
supabase.in
supabase.net

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#

The two "it broke" reports in the thread that were resolved 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:

ts
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 MIME-type restriction. The commenter had already listed application/json, text/plain and binary/octet-stream and the upload still failed, so adding types was not enough there: turning the restriction off was the only workaround reported. Read the JSON body of the failed POST /storage/v1/object/... response first; if you disable the restriction, check the file type on your own server before uploading. 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). The Realtime not receiving events checklist covers RLS and refreshing the token. That report was never resolved in the thread.

"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:

ts
// 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: ... } 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:

    bash
    curl -sI "https://<ref>.supabase.co/storage/v1/object/public/<bucket>/<path>" \
      | grep -iE "^(HTTP|content-type|set-cookie)"

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

  2. 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.

  3. Realtime problems: inspect the WebSocket frames, not the handshake headers.

  4. 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.

Frequently Asked Questions

|

Have more questions? Contact us

Still stuck? Paste your error.

Paste the error message or stack trace — we check it against every fix and tool on the site.

Matching happens in your browser. When you search, a cleaned error signature is sent so we can spot errors we do not cover yet — never the raw text, your IP address or your e-mail. Do not paste passwords or keys. Details

Written by

Mahdi Ben Rhouma
Mahdi Ben Rhouma

Full-Stack Dev — Next.js & Supabase

Solo developer building SaaS products with Next.js and Supabase. Writing about production patterns the official docs skip.

Remote

One email a month — the latest explained errors

What the site published that month — mostly Next.js, Supabase and Node errors, explained. You confirm by email; unsubscribe any time in one click.