<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Ritvik Dayal</title>
    <description>The latest articles on DEV Community by Ritvik Dayal (@ritvikdayal).</description>
    <link>https://dev.to/ritvikdayal</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3799639%2Fa5eb24a9-8e3d-4d08-a7a8-f7efa56e6ce9.jpeg</url>
      <title>DEV Community: Ritvik Dayal</title>
      <link>https://dev.to/ritvikdayal</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/ritvikdayal"/>
    <language>en</language>
    <item>
      <title>Your Python Environment Might Be Compromised by litellm (And Here's How to Check)</title>
      <dc:creator>Ritvik Dayal</dc:creator>
      <pubDate>Fri, 27 Mar 2026 04:10:44 +0000</pubDate>
      <link>https://dev.to/ritvikdayal/your-python-environment-might-be-compromised-by-litellm-and-heres-how-to-check-5f8p</link>
      <guid>https://dev.to/ritvikdayal/your-python-environment-might-be-compromised-by-litellm-and-heres-how-to-check-5f8p</guid>
      <description>&lt;h2&gt;
  
  
  What Happened to LiteLLM
&lt;/h2&gt;

&lt;p&gt;On March 24, 2026, someone published two malicious versions of the popular &lt;code&gt;litellm&lt;/code&gt; Python package to PyPI. Versions &lt;strong&gt;1.82.7&lt;/strong&gt; and &lt;strong&gt;1.82.8&lt;/strong&gt; contained a full-blown backdoor that harvested credentials, established persistence, and phoned home to a command and control server.&lt;/p&gt;

&lt;p&gt;The kicker? The attacker didn't hack PyPI directly. They poisoned a &lt;strong&gt;security scanner&lt;/strong&gt; (Trivy) that LiteLLM's own CI/CD pipeline trusted. The scanner stole the PyPI publish token, and the attacker used it to push compromised packages that looked completely legitimate.&lt;/p&gt;

&lt;p&gt;The malicious versions were live for about three hours before PyPI pulled them. Three hours is a long time when you have automated deployments.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Attack Chain
&lt;/h2&gt;

&lt;p&gt;Here's how the whole thing unfolded, step by step:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fja44a5z66wl6o8kr9wp1.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fja44a5z66wl6o8kr9wp1.png" alt="Attack-Chain" width="450" height="1472"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Two things make this especially nasty:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Version 1.82.7&lt;/strong&gt; embedded the payload in &lt;code&gt;litellm/proxy/proxy_server.py&lt;/code&gt;. It triggered when you imported the module. Standard stuff for malicious packages.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Version 1.82.8&lt;/strong&gt; went further. It dropped a &lt;code&gt;litellm_init.pth&lt;/code&gt; file into your &lt;code&gt;site-packages&lt;/code&gt; directory. Python's &lt;code&gt;.pth&lt;/code&gt; startup hook mechanism means this code executes &lt;strong&gt;every time any Python interpreter launches on that system&lt;/strong&gt;, even if you never import litellm. Just running &lt;code&gt;python3 -c "print('hello')"&lt;/code&gt; would trigger the backdoor.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the Backdoor Actually Does
&lt;/h2&gt;

&lt;p&gt;This wasn't a crypto miner or a simple credential stealer. The payload operates in three stages:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fi9pf3tpx500211fef8sn.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fi9pf3tpx500211fef8sn.png" alt="Backdoor-exploit-stage-1" width="800" height="69"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ffz25na3jcppgweswkzhf.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ffz25na3jcppgweswkzhf.png" alt="Backdoor-exploit-stage-2" width="800" height="179"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fisd4stbh0631t9dfpzc8.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fisd4stbh0631t9dfpzc8.png" alt="Backdoor-exploit-stage-3" width="800" height="242"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The Kubernetes bit is particularly clever. If the compromised package runs inside a pod with sufficient permissions, it deploys privileged pods named &lt;code&gt;node-setup-{node_name}&lt;/code&gt; across every node in the cluster via &lt;code&gt;kube-system&lt;/code&gt;. That's full host-level access on every node.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why "Just Uninstall It" Isn't Enough
&lt;/h2&gt;

&lt;p&gt;If you had either compromised version installed at any point, even briefly, uninstalling the package doesn't undo the damage. The backdoor:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Already exfiltrated your credentials&lt;/li&gt;
&lt;li&gt;Installed a persistent systemd service that survives package removal&lt;/li&gt;
&lt;li&gt;Dropped &lt;code&gt;.pth&lt;/code&gt; files that survive &lt;code&gt;pip uninstall&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;In Kubernetes environments, deployed pods that live outside your application entirely&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;You need to check for all of these things. Across every Python environment on your system. That includes PyEnv versions, virtualenvs, conda environments, system Python, and the pip cache.&lt;/p&gt;

&lt;p&gt;Doing this manually is tedious and error-prone. So I wrote a script.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Script: litellm-sweep
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;litellm-sweep.sh&lt;/code&gt; is a single bash script that scans your entire system across 10 phases:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fofl3zmd8b2n9npryiw1k.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fofl3zmd8b2n9npryiw1k.png" alt="litellm-sweep-functioning" width="800" height="1253"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  What It Catches
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Package installations&lt;/strong&gt; across pyenv, virtualenvs, system Python, conda, Homebrew, and the pip cache. Every found version is checked against the known compromised versions (1.82.7, 1.82.8) and flagged accordingly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Source code references&lt;/strong&gt; in &lt;code&gt;.py&lt;/code&gt; files, &lt;code&gt;requirements.txt&lt;/code&gt;, &lt;code&gt;pyproject.toml&lt;/code&gt;, &lt;code&gt;Pipfile&lt;/code&gt;, &lt;code&gt;Dockerfile&lt;/code&gt;, and more. These are reported but never auto-edited because modifying source code automatically is asking for trouble.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Persistence artifacts&lt;/strong&gt; from the actual backdoor payload:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;~/.config/sysmon/sysmon.py&lt;/code&gt; (the backdoor itself)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;~/.config/systemd/user/sysmon.service&lt;/code&gt; (persistence via systemd)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;litellm_init.pth&lt;/code&gt; in any site-packages (the v1.82.8 startup hook)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;/tmp/tpcp.tar.gz&lt;/code&gt;, &lt;code&gt;/tmp/session.key&lt;/code&gt;, &lt;code&gt;/tmp/payload.enc&lt;/code&gt; (staging files)&lt;/li&gt;
&lt;li&gt;On macOS, it also checks LaunchAgents for suspicious plists&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Network indicators&lt;/strong&gt; including DNS resolution of the C2 domains (&lt;code&gt;models.litellm.cloud&lt;/code&gt;, &lt;code&gt;checkmarx.zone&lt;/code&gt;), active connections via lsof, and references in your shell history.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Kubernetes indicators&lt;/strong&gt; if kubectl is available: &lt;code&gt;node-setup-*&lt;/code&gt; pods in &lt;code&gt;kube-system&lt;/code&gt; and privileged container detection.&lt;/p&gt;

&lt;h3&gt;
  
  
  Usage
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Full scan: home directory + common locations + all environments&lt;/span&gt;
./litellm-sweep.sh

&lt;span class="c"&gt;# Add extra paths to scan&lt;/span&gt;
./litellm-sweep.sh &lt;span class="nt"&gt;--include&lt;/span&gt; /opt/ml /srv/apps

&lt;span class="c"&gt;# Only scan specific paths, skip all environment scans&lt;/span&gt;
./litellm-sweep.sh &lt;span class="nt"&gt;--only&lt;/span&gt; /opt/ml /srv/apps
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Example 1: Clean System (Nothing Found)
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;./litellm-sweep.sh

litellm-sweep — scanning &lt;span class="k"&gt;for &lt;/span&gt;all traces of litellm
Report will be saved to: litellm-sweep-report-2026-03-27.txt
Mode: full scan &lt;span class="o"&gt;(&lt;/span&gt;home + common locations + environments&lt;span class="o"&gt;)&lt;/span&gt;

── Phase 1: pyenv ──
  CLEAN  Python 3.10.14
  CLEAN  Python 3.11.9
  CLEAN  Python 3.12.4

── Phase 2: virtualenvs ──
  CLEAN  /home/dev/projects/api-service/.venv
  CLEAN  /home/dev/projects/ml-pipeline/.venv

── Phase 3: System Python ──
  CLEAN  /usr/bin/python3

── Phase 4: pip cache ──
  CLEAN  No litellm &lt;span class="k"&gt;in &lt;/span&gt;pip cache

── Phase 5: Conda ──
  SKIP   conda not installed

── Phase 6: Homebrew ──
  CLEAN  Not &lt;span class="k"&gt;in &lt;/span&gt;Homebrew formulae

── Phase 7: Source code references ──
  Scanning /home/dev ...
  CLEAN  No &lt;span class="nb"&gt;source &lt;/span&gt;code references found

── Phase 8: Persistence artifacts &lt;span class="o"&gt;(&lt;/span&gt;TeamPCP backdoor&lt;span class="o"&gt;)&lt;/span&gt; ──
  CLEAN  /home/dev/.config/sysmon/sysmon.py
  CLEAN  /home/dev/.config/systemd/user/sysmon.service
  CLEAN  /tmp/tpcp.tar.gz
  CLEAN  /tmp/session.key
  CLEAN  /tmp/payload.enc
  CLEAN  sysmon.service not registered
  Scanning &lt;span class="k"&gt;for &lt;/span&gt;litellm_init.pth files &lt;span class="o"&gt;(&lt;/span&gt;v1.82.8 startup hook&lt;span class="o"&gt;)&lt;/span&gt;...
  CLEAN  No persistence artifacts found

── Phase 9: Network IOCs &lt;span class="o"&gt;(&lt;/span&gt;C2 domains&lt;span class="o"&gt;)&lt;/span&gt; ──
  CLEAN  No network IOCs found

── Phase 10: Kubernetes IOCs ──
  CLEAN  No node-setup-&lt;span class="k"&gt;*&lt;/span&gt; pods &lt;span class="k"&gt;in &lt;/span&gt;kube-system

══════════════════════════════════════════
  SCAN SUMMARY
══════════════════════════════════════════
  pyenv:                    0 finding&lt;span class="o"&gt;(&lt;/span&gt;s&lt;span class="o"&gt;)&lt;/span&gt;
  virtualenvs:              0 finding&lt;span class="o"&gt;(&lt;/span&gt;s&lt;span class="o"&gt;)&lt;/span&gt;
  System Python:            0 finding&lt;span class="o"&gt;(&lt;/span&gt;s&lt;span class="o"&gt;)&lt;/span&gt;
  pip cache:                0 finding&lt;span class="o"&gt;(&lt;/span&gt;s&lt;span class="o"&gt;)&lt;/span&gt;
  Conda:                    0 finding&lt;span class="o"&gt;(&lt;/span&gt;s&lt;span class="o"&gt;)&lt;/span&gt;
  Homebrew:                 0 finding&lt;span class="o"&gt;(&lt;/span&gt;s&lt;span class="o"&gt;)&lt;/span&gt;
  Source references:        0 finding&lt;span class="o"&gt;(&lt;/span&gt;s&lt;span class="o"&gt;)&lt;/span&gt;
  Persistence artifacts:    0 finding&lt;span class="o"&gt;(&lt;/span&gt;s&lt;span class="o"&gt;)&lt;/span&gt;
  Malicious .pth files:     0 finding&lt;span class="o"&gt;(&lt;/span&gt;s&lt;span class="o"&gt;)&lt;/span&gt;
  Network IOCs:             0 finding&lt;span class="o"&gt;(&lt;/span&gt;s&lt;span class="o"&gt;)&lt;/span&gt;
  Kubernetes IOCs:          0 finding&lt;span class="o"&gt;(&lt;/span&gt;s&lt;span class="o"&gt;)&lt;/span&gt;
  ──────────────────────────────────────
  TOTAL: 0 findings — system is clean

Nothing to remove. Done.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You want to see this output. This means you're good.&lt;/p&gt;

&lt;h3&gt;
  
  
  Example 2: Safe Version Installed (Not Compromised, But You Probably Want It Gone)
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;./litellm-sweep.sh

litellm-sweep — scanning &lt;span class="k"&gt;for &lt;/span&gt;all traces of litellm
Report will be saved to: litellm-sweep-report-2026-03-27.txt
Mode: full scan &lt;span class="o"&gt;(&lt;/span&gt;home + common locations + environments&lt;span class="o"&gt;)&lt;/span&gt;

── Phase 1: pyenv ──
  CLEAN  Python 3.11.9
  FOUND  Python 3.12.4 — litellm 1.61.2
  CLEAN  Python 3.10.14

── Phase 2: virtualenvs ──
  FOUND  /home/dev/projects/llm-gateway/.venv — litellm 1.61.2

── Phase 7: Source code references ──
  Scanning /home/dev ...
  FOUND  /home/dev/projects/llm-gateway/requirements.txt:14:litellm&lt;span class="o"&gt;==&lt;/span&gt;1.61.2
  FOUND  /home/dev/projects/llm-gateway/src/router.py:3:import litellm

── Phase 8: Persistence artifacts &lt;span class="o"&gt;(&lt;/span&gt;TeamPCP backdoor&lt;span class="o"&gt;)&lt;/span&gt; ──
  CLEAN  No persistence artifacts found

── Phase 9: Network IOCs &lt;span class="o"&gt;(&lt;/span&gt;C2 domains&lt;span class="o"&gt;)&lt;/span&gt; ──
  CLEAN  No network IOCs found

══════════════════════════════════════════
  SCAN SUMMARY
══════════════════════════════════════════
  pyenv:                    1 finding&lt;span class="o"&gt;(&lt;/span&gt;s&lt;span class="o"&gt;)&lt;/span&gt;
  virtualenvs:              1 finding&lt;span class="o"&gt;(&lt;/span&gt;s&lt;span class="o"&gt;)&lt;/span&gt;
  Source references:        2 finding&lt;span class="o"&gt;(&lt;/span&gt;s&lt;span class="o"&gt;)&lt;/span&gt;
  Persistence artifacts:    0 finding&lt;span class="o"&gt;(&lt;/span&gt;s&lt;span class="o"&gt;)&lt;/span&gt;
  Network IOCs:             0 finding&lt;span class="o"&gt;(&lt;/span&gt;s&lt;span class="o"&gt;)&lt;/span&gt;
  Kubernetes IOCs:          0 finding&lt;span class="o"&gt;(&lt;/span&gt;s&lt;span class="o"&gt;)&lt;/span&gt;
  ──────────────────────────────────────
  TOTAL: 4 finding&lt;span class="o"&gt;(&lt;/span&gt;s&lt;span class="o"&gt;)&lt;/span&gt;

── Removal ──

Note: 2 &lt;span class="nb"&gt;source &lt;/span&gt;code reference&lt;span class="o"&gt;(&lt;/span&gt;s&lt;span class="o"&gt;)&lt;/span&gt; found.
  These are reported only — you must edit/remove them manually.
  Files with references:
    /home/dev/projects/llm-gateway/requirements.txt
    /home/dev/projects/llm-gateway/src/router.py

Removable packages:
  &lt;span class="o"&gt;[&lt;/span&gt;1] &lt;span class="o"&gt;[&lt;/span&gt;pyenv] Python 3.12.4 — litellm 1.61.2
  &lt;span class="o"&gt;[&lt;/span&gt;2] &lt;span class="o"&gt;[&lt;/span&gt;venv] /home/dev/projects/llm-gateway/.venv — litellm 1.61.2

Options:
  a — Remove all packages
  1,3,5 — Remove specific items &lt;span class="o"&gt;(&lt;/span&gt;comma-separated&lt;span class="o"&gt;)&lt;/span&gt;
  s — Skip &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="k"&gt;do &lt;/span&gt;nothing&lt;span class="o"&gt;)&lt;/span&gt;

Choice: a

Removing: &lt;span class="o"&gt;[&lt;/span&gt;pyenv] Python 3.12.4 — litellm 1.61.2
  Action: &lt;span class="nv"&gt;PYENV_VERSION&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;3.12.4 pyenv &lt;span class="nb"&gt;exec &lt;/span&gt;pip uninstall &lt;span class="nt"&gt;-y&lt;/span&gt; litellm
  Confirm? &lt;span class="o"&gt;(&lt;/span&gt;y/n&lt;span class="o"&gt;)&lt;/span&gt; y
  Successfully uninstalled litellm-1.61.2
  Removed

Removing: &lt;span class="o"&gt;[&lt;/span&gt;venv] /home/dev/projects/llm-gateway/.venv — litellm 1.61.2
  Action: /home/dev/projects/llm-gateway/.venv/bin/pip uninstall &lt;span class="nt"&gt;-y&lt;/span&gt; litellm
  Confirm? &lt;span class="o"&gt;(&lt;/span&gt;y/n&lt;span class="o"&gt;)&lt;/span&gt; y
  Successfully uninstalled litellm-1.61.2
  Removed

── Done ──
Review &lt;span class="nb"&gt;source &lt;/span&gt;code references manually and remove litellm from your dependency files.
Report: litellm-sweep-report-2026-03-27.txt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Version 1.61.2 isn't compromised, so no critical alert. But given the library's supply chain was breached, you might want to remove it anyway and evaluate alternatives.&lt;/p&gt;

&lt;h3&gt;
  
  
  Example 3: Compromised Version Detected (The Bad Scenario)
&lt;/h3&gt;

&lt;p&gt;This is the output you do not want to see:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;./litellm-sweep.sh

litellm-sweep — scanning &lt;span class="k"&gt;for &lt;/span&gt;all traces of litellm
Report will be saved to: litellm-sweep-report-2026-03-27.txt
Mode: full scan &lt;span class="o"&gt;(&lt;/span&gt;home + common locations + environments&lt;span class="o"&gt;)&lt;/span&gt;

── Phase 1: pyenv ──
  CLEAN  Python 3.11.9

── Phase 2: virtualenvs ──
  &lt;span class="o"&gt;!!&lt;/span&gt;CRITICAL!!  /srv/apps/inference/.venv — litellm 1.82.8 &lt;span class="k"&gt;***&lt;/span&gt; COMPROMISED VERSION &lt;span class="k"&gt;***&lt;/span&gt;

── Phase 7: Source code references ──
  Scanning /home/deploy ...
  FOUND  /srv/apps/inference/requirements.txt:8:litellm&lt;span class="o"&gt;==&lt;/span&gt;1.82.8

── Phase 8: Persistence artifacts &lt;span class="o"&gt;(&lt;/span&gt;TeamPCP backdoor&lt;span class="o"&gt;)&lt;/span&gt; ──
  &lt;span class="o"&gt;!!&lt;/span&gt;CRITICAL!!  BACKDOOR ARTIFACT: /home/deploy/.config/sysmon/sysmon.py
  &lt;span class="o"&gt;!!&lt;/span&gt;CRITICAL!!  BACKDOOR ARTIFACT: /home/deploy/.config/systemd/user/sysmon.service
  &lt;span class="o"&gt;!!&lt;/span&gt;CRITICAL!!  sysmon.service is ACTIVELY RUNNING
  &lt;span class="o"&gt;!!&lt;/span&gt;CRITICAL!!  MALICIOUS .pth FILE: /srv/apps/inference/.venv/lib/python3.12/site-packages/litellm_init.pth
  CLEAN  /tmp/tpcp.tar.gz

── Phase 9: Network IOCs &lt;span class="o"&gt;(&lt;/span&gt;C2 domains&lt;span class="o"&gt;)&lt;/span&gt; ──
  FOUND  C2 domain resolves: models.litellm.cloud &lt;span class="o"&gt;(&lt;/span&gt;verify no active connections&lt;span class="o"&gt;)&lt;/span&gt;
  &lt;span class="o"&gt;!!&lt;/span&gt;CRITICAL!!  ACTIVE CONNECTION to C2 domain: models.litellm.cloud
  FOUND  C2 domain found &lt;span class="k"&gt;in &lt;/span&gt;shell &lt;span class="nb"&gt;history&lt;/span&gt;: /home/deploy/.bash_history → checkmarx.zone

── Phase 10: Kubernetes IOCs ──
  &lt;span class="o"&gt;!!&lt;/span&gt;CRITICAL!!  MALICIOUS POD &lt;span class="k"&gt;in &lt;/span&gt;kube-system: pod/node-setup-gke-prod-01
  &lt;span class="o"&gt;!!&lt;/span&gt;CRITICAL!!  MALICIOUS POD &lt;span class="k"&gt;in &lt;/span&gt;kube-system: pod/node-setup-gke-prod-02
  &lt;span class="o"&gt;!!&lt;/span&gt;CRITICAL!!  PRIVILEGED malicious pod: node-setup-gke-prod-01 &lt;span class="o"&gt;(&lt;/span&gt;privileged, image: alpine:latest&lt;span class="o"&gt;)&lt;/span&gt;

══════════════════════════════════════════
  SCAN SUMMARY
══════════════════════════════════════════
  virtualenvs:              1 finding&lt;span class="o"&gt;(&lt;/span&gt;s&lt;span class="o"&gt;)&lt;/span&gt;
  Source references:        1 finding&lt;span class="o"&gt;(&lt;/span&gt;s&lt;span class="o"&gt;)&lt;/span&gt;
  Persistence artifacts:    4 finding&lt;span class="o"&gt;(&lt;/span&gt;s&lt;span class="o"&gt;)&lt;/span&gt;
  Malicious .pth files:     1 finding&lt;span class="o"&gt;(&lt;/span&gt;s&lt;span class="o"&gt;)&lt;/span&gt;
  Network IOCs:             3 finding&lt;span class="o"&gt;(&lt;/span&gt;s&lt;span class="o"&gt;)&lt;/span&gt;
  Kubernetes IOCs:          3 finding&lt;span class="o"&gt;(&lt;/span&gt;s&lt;span class="o"&gt;)&lt;/span&gt;
  ──────────────────────────────────────
  TOTAL: 13 finding&lt;span class="o"&gt;(&lt;/span&gt;s&lt;span class="o"&gt;)&lt;/span&gt;

┌─────────────────────────────────────────────────────────────────┐
│  &lt;span class="o"&gt;!!&lt;/span&gt; COMPROMISED VERSION &lt;span class="o"&gt;(&lt;/span&gt;1.82.7 or 1.82.8&lt;span class="o"&gt;)&lt;/span&gt; DETECTED &lt;span class="o"&gt;!!&lt;/span&gt;         │
│                                                                 │
│  This system may have been backdoored by TeamPCP.               │
│  IMMEDIATE ACTIONS REQUIRED:                                    │
│                                                                 │
│  1. Rotate ALL credentials on this machine:                     │
│     - SSH keys &lt;span class="o"&gt;(&lt;/span&gt;~/.ssh/&lt;span class="k"&gt;*&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;                                       │
│     - AWS/GCP/Azure credentials                                 │
│     - Kubernetes tokens &amp;amp; configs                               │
│     - Docker registry credentials                               │
│     - API keys, database passwords                              │
│     - Cryptocurrency wallet keys                                │
│                                                                 │
│  2. Check &lt;span class="k"&gt;for &lt;/span&gt;persistence:                                      │
│     - ~/.config/sysmon/sysmon.py                                │
│     - ~/.config/systemd/user/sysmon.service                     │
│     - litellm_init.pth &lt;span class="k"&gt;in &lt;/span&gt;site-packages                         │
│                                                                 │
│  3. Audit cloud services &lt;span class="k"&gt;for &lt;/span&gt;unauthorized access                │
│  4. Check k8s &lt;span class="k"&gt;for &lt;/span&gt;node-setup-&lt;span class="k"&gt;*&lt;/span&gt; pods &lt;span class="k"&gt;in &lt;/span&gt;kube-system              │
│  5. Consider rebuilding from a clean environment                │
│                                                                 │
│  Ref: snyk.io/articles/poisoned-security-scanner-backdooring-litellm/
└─────────────────────────────────────────────────────────────────┘

── Removal ──

IOC artifacts found &lt;span class="o"&gt;(&lt;/span&gt;8&lt;span class="o"&gt;)&lt;/span&gt;:
  &lt;span class="o"&gt;[&lt;/span&gt;1] &lt;span class="o"&gt;[&lt;/span&gt;ioc-persist] BACKDOOR ARTIFACT: /home/deploy/.config/sysmon/sysmon.py
  &lt;span class="o"&gt;[&lt;/span&gt;2] &lt;span class="o"&gt;[&lt;/span&gt;ioc-persist] BACKDOOR ARTIFACT: /home/deploy/.config/systemd/user/sysmon.service
  &lt;span class="o"&gt;[&lt;/span&gt;3] &lt;span class="o"&gt;[&lt;/span&gt;ioc-persist] sysmon.service is ACTIVELY RUNNING
  &lt;span class="o"&gt;[&lt;/span&gt;4] &lt;span class="o"&gt;[&lt;/span&gt;ioc-pth] MALICIOUS .pth FILE: /srv/apps/inference/.venv/.../litellm_init.pth
  &lt;span class="o"&gt;[&lt;/span&gt;5] &lt;span class="o"&gt;[&lt;/span&gt;ioc-network] ACTIVE CONNECTION to C2 domain: models.litellm.cloud
  &lt;span class="o"&gt;[&lt;/span&gt;6] &lt;span class="o"&gt;[&lt;/span&gt;ioc-k8s] MALICIOUS POD &lt;span class="k"&gt;in &lt;/span&gt;kube-system: pod/node-setup-gke-prod-01
  &lt;span class="o"&gt;[&lt;/span&gt;7] &lt;span class="o"&gt;[&lt;/span&gt;ioc-k8s] MALICIOUS POD &lt;span class="k"&gt;in &lt;/span&gt;kube-system: pod/node-setup-gke-prod-02
  &lt;span class="o"&gt;[&lt;/span&gt;8] &lt;span class="o"&gt;[&lt;/span&gt;ioc-k8s] PRIVILEGED malicious pod: node-setup-gke-prod-01

Removable IOC artifacts:
  /home/deploy/.config/sysmon/sysmon.py
  /home/deploy/.config/systemd/user/sysmon.service
  sysmon.service &lt;span class="o"&gt;(&lt;/span&gt;systemd user service&lt;span class="o"&gt;)&lt;/span&gt;
  /srv/apps/inference/.venv/.../litellm_init.pth

Remove IOC artifacts? &lt;span class="o"&gt;(&lt;/span&gt;y/n&lt;span class="o"&gt;)&lt;/span&gt; y
  Removing: /home/deploy/.config/sysmon/sysmon.py
  Removed
  Stopping and disabling sysmon.service...
  Stopped + disabled + removed
  Removing: /srv/apps/inference/.venv/.../litellm_init.pth
  Removed
  &lt;span class="o"&gt;[&lt;/span&gt;ioc-network] ACTIVE CONNECTION to C2 domain — requires manual remediation
  &lt;span class="o"&gt;[&lt;/span&gt;ioc-k8s] MALICIOUS POD &lt;span class="k"&gt;in &lt;/span&gt;kube-system: pod/node-setup-gke-prod-01 — requires manual remediation
  &lt;span class="o"&gt;[&lt;/span&gt;ioc-k8s] MALICIOUS POD &lt;span class="k"&gt;in &lt;/span&gt;kube-system: pod/node-setup-gke-prod-02 — requires manual remediation

Removable packages:
  &lt;span class="o"&gt;[&lt;/span&gt;1] &lt;span class="o"&gt;[&lt;/span&gt;venv] /srv/apps/inference/.venv — litellm 1.82.8

Options:
  a — Remove all packages
  1,3,5 — Remove specific items &lt;span class="o"&gt;(&lt;/span&gt;comma-separated&lt;span class="o"&gt;)&lt;/span&gt;
  s — Skip &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="k"&gt;do &lt;/span&gt;nothing&lt;span class="o"&gt;)&lt;/span&gt;

Choice: a

Removing: &lt;span class="o"&gt;[&lt;/span&gt;venv] /srv/apps/inference/.venv — litellm 1.82.8
  Action: /srv/apps/inference/.venv/bin/pip uninstall &lt;span class="nt"&gt;-y&lt;/span&gt; litellm
  Confirm? &lt;span class="o"&gt;(&lt;/span&gt;y/n&lt;span class="o"&gt;)&lt;/span&gt; y
  Successfully uninstalled litellm-1.82.8
  Removed

── Done ──
Review &lt;span class="nb"&gt;source &lt;/span&gt;code references manually and remove litellm from your dependency files.

REMINDER: Compromised version was detected. Credential rotation is CRITICAL.
Do NOT just uninstall — assume all secrets on this machine are compromised.
Rebuild from a clean environment after rotating all credentials.
Report: litellm-sweep-report-2026-03-27.txt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notice how the Kubernetes IOCs and network connections are flagged but marked as "requires manual remediation." The script won't auto-delete pods or kill network connections because those actions need human judgment. You might be running the scan on a jump box, and blindly killing connections could make the situation worse.&lt;/p&gt;

&lt;h3&gt;
  
  
  Example 4: Scanning Only Specific Paths
&lt;/h3&gt;

&lt;p&gt;If you manage multiple services and just want to check one deployment directory:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;./litellm-sweep.sh &lt;span class="nt"&gt;--only&lt;/span&gt; /srv/apps/ml-service /srv/apps/inference

litellm-sweep — scanning &lt;span class="k"&gt;for &lt;/span&gt;all traces of litellm
Report will be saved to: litellm-sweep-report-2026-03-27.txt
Mode: &lt;span class="nt"&gt;--only&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;scanning specified paths only, skipping &lt;span class="nb"&gt;env &lt;/span&gt;scans&lt;span class="o"&gt;)&lt;/span&gt;
  Target: /srv/apps/ml-service
  Target: /srv/apps/inference

── Phase 7: Source code references ──
  Scanning /srv/apps/ml-service ...
  CLEAN  No &lt;span class="nb"&gt;source &lt;/span&gt;code references found
  Scanning /srv/apps/inference ...
  FOUND  /srv/apps/inference/requirements.txt:8:litellm&lt;span class="o"&gt;==&lt;/span&gt;1.82.6
  FOUND  /srv/apps/inference/src/llm_router.py:1:from litellm import completion

── Phase 8: Persistence artifacts &lt;span class="o"&gt;(&lt;/span&gt;TeamPCP backdoor&lt;span class="o"&gt;)&lt;/span&gt; ──
  CLEAN  No persistence artifacts found

── Phase 9: Network IOCs &lt;span class="o"&gt;(&lt;/span&gt;C2 domains&lt;span class="o"&gt;)&lt;/span&gt; ──
  CLEAN  No network IOCs found

── Phase 10: Kubernetes IOCs ──
  SKIP   kubectl not available &lt;span class="o"&gt;(&lt;/span&gt;skip k8s IOC check&lt;span class="o"&gt;)&lt;/span&gt;

══════════════════════════════════════════
  SCAN SUMMARY
══════════════════════════════════════════
  Source references:        2 finding&lt;span class="o"&gt;(&lt;/span&gt;s&lt;span class="o"&gt;)&lt;/span&gt;
  Persistence artifacts:    0 finding&lt;span class="o"&gt;(&lt;/span&gt;s&lt;span class="o"&gt;)&lt;/span&gt;
  Network IOCs:             0 finding&lt;span class="o"&gt;(&lt;/span&gt;s&lt;span class="o"&gt;)&lt;/span&gt;
  Kubernetes IOCs:          0 finding&lt;span class="o"&gt;(&lt;/span&gt;s&lt;span class="o"&gt;)&lt;/span&gt;
  ──────────────────────────────────────
  TOTAL: 2 finding&lt;span class="o"&gt;(&lt;/span&gt;s&lt;span class="o"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With &lt;code&gt;--only&lt;/code&gt;, environment scans (pyenv, venvs, conda, etc.) are completely skipped. Only the source code scan runs against your specified paths, plus the IOC phases, which always run. This is useful when you're sweeping a shared server and only care about specific deployment directories.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Bigger Picture
&lt;/h2&gt;

&lt;p&gt;This attack worked because of a chain of trust that most teams never audit:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fsttttqsz5ttd9hqly4ww.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fsttttqsz5ttd9hqly4ww.png" alt="Bigger-Picture" width="800" height="60"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Git tags are mutable. Anyone with write access to a repo can point a tag at a different commit. If your CI/CD references an action by tag (like &lt;code&gt;@v1&lt;/code&gt;), you're trusting that the tag still points to the code you reviewed. In this case, it didn't.&lt;/p&gt;

&lt;p&gt;Pin your GitHub Actions to full commit SHAs, not tags. It's one line of config that would have prevented this entire attack chain.&lt;/p&gt;

&lt;h2&gt;
  
  
  Get the Script
&lt;/h2&gt;

&lt;p&gt;The full script is on GitHub. Single file, no dependencies beyond bash and the standard tools already on your system:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-O&lt;/span&gt; https://gist.githubusercontent.com/RitvikDayal/18d35fe1d51b49ecf5b90c6f262a8c9d/raw/litellm-sweep.sh
&lt;span class="nb"&gt;chmod&lt;/span&gt; +x litellm-sweep.sh
./litellm-sweep.sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Full usage docs and IOC reference: &lt;a href="https://gist.github.com/RitvikDayal/18d35fe1d51b49ecf5b90c6f262a8c9d" rel="noopener noreferrer"&gt;&lt;strong&gt;litellm-sweep on GitHub Gist&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you're running Kubernetes workloads that might have pulled litellm, run it on those nodes too. The script checks for the k8s IOCs when kubectl is available.&lt;/p&gt;

&lt;p&gt;And if it finds a compromised version: &lt;strong&gt;do not just uninstall and move on&lt;/strong&gt;. Rotate every credential on that machine. Every single one. The exfiltration happens fast, and the data was encrypted before being sent, which means it was designed to be collected and processed later. Your credentials may not have been used yet, but assume they will be.&lt;/p&gt;

&lt;p&gt;Stay safe out there.&lt;/p&gt;

</description>
      <category>litellm</category>
      <category>security</category>
      <category>devops</category>
      <category>python</category>
    </item>
    <item>
      <title>Supabase is Blocked in India. Here's the Free Fix Using a Cloudflare Worker</title>
      <dc:creator>Ritvik Dayal</dc:creator>
      <pubDate>Sun, 01 Mar 2026 09:00:18 +0000</pubDate>
      <link>https://dev.to/ritvikdayal/supabase-is-blocked-in-india-heres-the-exact-fix-using-a-cloudflare-worker-2ejf</link>
      <guid>https://dev.to/ritvikdayal/supabase-is-blocked-in-india-heres-the-exact-fix-using-a-cloudflare-worker-2ejf</guid>
      <description>&lt;p&gt;If your app uses Supabase and suddenly stopped working for Indian users, you're not alone. India has DNS-blocked &lt;code&gt;*.supabase.co&lt;/code&gt; at the ISP level. This article explains what's happening, why it breaks only browser-side calls, and how to fix it permanently in under 30 minutes — for free — using a Cloudflare Worker as a transparent proxy.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Is Actually Happening
&lt;/h2&gt;

&lt;p&gt;This is not a Supabase outage. The service is running fine globally. What's happening is that Indian ISPs have blocked the DNS records for &lt;code&gt;*.supabase.co&lt;/code&gt;. When a browser in India tries to connect to your Supabase project, it asks the local DNS resolver for the IP address of &lt;code&gt;abcdefgh.supabase.co&lt;/code&gt;. The ISP intercepts that query and returns nothing — or an error — so the connection never starts.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F96zw7t2g5ugwhcji77hy.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F96zw7t2g5ugwhcji77hy.png" alt="How Supabase is blocked" width="800" height="389"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Your server-side code is completely unaffected. When Vercel (or any hosting provider outside India) runs your Next.js server components or API routes, it makes the DNS query from a US/EU data center, where Supabase is not blocked. Only the browser, running on the user's device in India, is impacted.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fgqvzz6e0ejdg2daelvmu.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fgqvzz6e0ejdg2daelvmu.png" alt="nextjs application fails to communicate to supabase" width="762" height="1362"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Why This Breaks More Than You Think
&lt;/h2&gt;

&lt;p&gt;The Supabase JS SDK runs in the browser. When you call&lt;br&gt;
&lt;code&gt;supabase.auth.signIn()&lt;/code&gt;, &lt;code&gt;supabase.from('table').select()&lt;/code&gt;, or&lt;br&gt;
subscribe to a Realtime channel, those are all browser-to-Supabase&lt;br&gt;
HTTP/WebSocket calls. Every single one fails for Indian users.&lt;/p&gt;

&lt;p&gt;Symptoms your Indian users see:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Login page spins forever, then shows a network error&lt;/li&gt;
&lt;li&gt;Dashboard loads (SSR works), but data tables are empty&lt;/li&gt;
&lt;li&gt;Real-time updates never arrive&lt;/li&gt;
&lt;li&gt;File uploads silently fail&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  The Fix: A Cloudflare Worker as a Transparent Proxy
&lt;/h2&gt;

&lt;p&gt;The solution is to put a proxy in the middle that lives on a domain&lt;br&gt;
that is NOT blocked in India. Cloudflare Workers run on&lt;br&gt;
&lt;code&gt;*.workers.dev&lt;/code&gt; — Cloudflare's own domain, fully accessible in India.&lt;/p&gt;

&lt;p&gt;The Worker receives every browser request meant for Supabase, strips&lt;br&gt;
nothing, adds nothing meaningful, and forwards it verbatim to the real&lt;br&gt;
Supabase project URL. From Supabase's perspective, it looks like a&lt;br&gt;
normal request. From the browser's perspective, it's talking to a&lt;br&gt;
&lt;code&gt;workers.dev&lt;/code&gt; domain that it can actually reach.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fut38l351b6hf6mnaa65l.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fut38l351b6hf6mnaa65l.png" alt="introduction of cloudflare worker" width="800" height="404"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;
  
  
  Why Cloudflare Workers specifically?
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Option&lt;/th&gt;
&lt;th&gt;WebSocket Support&lt;/th&gt;
&lt;th&gt;Free Tier&lt;/th&gt;
&lt;th&gt;No Domain Needed&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Cloudflare Worker&lt;/td&gt;
&lt;td&gt;✅ Yes&lt;/td&gt;
&lt;td&gt;100k req/day&lt;/td&gt;
&lt;td&gt;✅ &lt;code&gt;*.workers.dev&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Vercel Rewrites&lt;/td&gt;
&lt;td&gt;❌ No&lt;/td&gt;
&lt;td&gt;Unlimited&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Nginx reverse proxy&lt;/td&gt;
&lt;td&gt;✅ Yes&lt;/td&gt;
&lt;td&gt;Self-hosted cost&lt;/td&gt;
&lt;td&gt;❌ Needs a domain&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AWS Lambda&lt;/td&gt;
&lt;td&gt;✅ With ALB&lt;/td&gt;
&lt;td&gt;Limited&lt;/td&gt;
&lt;td&gt;❌ Needs a domain&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Vercel Rewrites seem like the obvious choice since you're already on Vercel — but they do not support WebSocket protocol upgrades. Supabase Realtime uses WebSockets. If you route through Vercel, Realtime silently breaks. Cloudflare Workers handle WebSocket proxying natively.&lt;/p&gt;
&lt;h2&gt;
  
  
  Architecture After the Fix
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F3hafu3q9dltux9vohr2a.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F3hafu3q9dltux9vohr2a.png" alt="after fix architecture" width="800" height="1106"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Note that server-side clients (&lt;code&gt;server.ts&lt;/code&gt;, &lt;code&gt;service.ts&lt;/code&gt;) continue to communicate directly with Supabase. Only &lt;code&gt;client.ts&lt;/code&gt; — the browser SDK — needs the proxy URL, and it picks that up automatically from &lt;code&gt;NEXT_PUBLIC_SUPABASE_URL&lt;/code&gt;.&lt;/p&gt;
&lt;h2&gt;
  
  
  Step-by-Step Implementation
&lt;/h2&gt;
&lt;h3&gt;
  
  
  Step 1 — Create the Cloudflare Worker
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Go to &lt;a href="https://dash.cloudflare.com" rel="noopener noreferrer"&gt;dash.cloudflare.com&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;In the left sidebar click &lt;strong&gt;Compute&lt;/strong&gt; → &lt;strong&gt;Workers &amp;amp; Pages&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;Create&lt;/strong&gt; → &lt;strong&gt;Create Worker&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Name it &lt;code&gt;supabase-proxy&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;Deploy&lt;/strong&gt; (this deploys a hello-world placeholder)&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;Edit code&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Delete all existing code and paste the following:
&lt;/li&gt;
&lt;/ol&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;SUPABASE_ORIGIN&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://YOUR_PROJECT_REF.supabase.co&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;URL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;targetUrl&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;SUPABASE_ORIGIN&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;pathname&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;search&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="c1"&gt;// Clone and rewrite the Host header so Supabase accepts the request&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;headers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Headers&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;host&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;URL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;SUPABASE_ORIGIN&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;host&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// WebSocket upgrade (Supabase Realtime) — pass through unchanged&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Upgrade&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;websocket&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;targetUrl&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;method&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;targetUrl&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;method&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;GET&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;HEAD&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;method&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;redirect&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;follow&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Replace &lt;code&gt;YOUR_PROJECT_REF&lt;/code&gt; with your actual Supabase project reference ID. Find it at: Supabase Dashboard → Project Settings → General → &lt;strong&gt;Reference ID&lt;/strong&gt; It looks like &lt;code&gt;abcdefghijklmnop&lt;/code&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Click &lt;strong&gt;Deploy&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Copy your Worker URL from the deployment screen: &lt;code&gt;https://supabase-proxy.&amp;lt;your-account&amp;gt;.workers.dev&lt;/code&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;
  
  
  Step 2 — Update Your Environment Variable
&lt;/h3&gt;

&lt;p&gt;This is the only application-level change required.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Before:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;NEXT_PUBLIC_SUPABASE_URL=https://abcdefghijklmnop.supabase.co
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;After:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;NEXT_PUBLIC_SUPABASE_URL=https://supabase-proxy.&amp;lt;your-account&amp;gt;.workers.dev
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;All other environment variables stay identical:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;NEXT_PUBLIC_SUPABASE_ANON_KEY=eyJ...          # unchanged
SUPABASE_SERVICE_ROLE_KEY=eyJ...              # unchanged
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you're on &lt;strong&gt;Vercel&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Dashboard → Project → Settings → Environment Variables&lt;/li&gt;
&lt;li&gt;Edit &lt;code&gt;NEXT_PUBLIC_SUPABASE_URL&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Save → Redeploy&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The Supabase JS SDK reads &lt;code&gt;NEXT_PUBLIC_SUPABASE_URL&lt;/code&gt; on initialisation. Every browser-side call now targets your Worker URL automatically, with zero code changes in your application.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 3 — Fix Your Content Security Policy
&lt;/h3&gt;

&lt;p&gt;If you have a Content Security Policy header with &lt;code&gt;connect-src&lt;/code&gt;, you need to ensure it allows your Worker URL and its WebSocket equivalent (&lt;code&gt;wss://&lt;/code&gt; instead of &lt;code&gt;https://&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;The naive approach is to hardcode both URLs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// ❌ Brittle — requires updating in two places when URL changes&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;cspConnectSrc&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;supabaseUrl&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; wss://*.supabase.co`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The correct approach derives the WebSocket URL dynamically from whatever &lt;code&gt;NEXT_PUBLIC_SUPABASE_URL&lt;/code&gt; is set to:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// ✅ Works for any URL — Supabase direct or Worker proxy&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;workerUrl&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;NEXT_PUBLIC_SUPABASE_URL&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;workerWss&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;workerUrl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/^https:&lt;/span&gt;&lt;span class="se"&gt;\/\/&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;wss://&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;cspConnectSrc&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;workerUrl&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;workerUrl&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;workerWss&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now, when you change the env var, the CSP updates automatically on the next build. No second place to update.&lt;/p&gt;

&lt;h2&gt;
  
  
  How the Worker Script Works
&lt;/h2&gt;

&lt;p&gt;Let's walk through the Worker script line by line.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;SUPABASE_ORIGIN&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://YOUR_PROJECT_REF.supabase.co&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The real destination. Only configured in one place in the Worker.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;URL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;targetUrl&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;SUPABASE_ORIGIN&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;pathname&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;search&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The browser calls &lt;code&gt;https://supabase-proxy.account.workers.dev/auth/v1/token&lt;/code&gt;.&lt;br&gt;
We strip the Worker's origin and reconstruct the target as&lt;br&gt;
&lt;code&gt;https://abcdefgh.supabase.co/auth/v1/token&lt;/code&gt;. Path and query string are&lt;br&gt;
preserved exactly.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;headers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Headers&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;host&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;URL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;SUPABASE_ORIGIN&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;host&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is the critical part. HTTP requests must include a &lt;code&gt;Host&lt;/code&gt; header matching the destination server. Without rewriting it, Supabase would receive &lt;code&gt;Host: supabase-proxy.account.workers.dev&lt;/code&gt; and reject the request. We rewrite it to &lt;code&gt;abcdefgh.supabase.co&lt;/code&gt;. &lt;br&gt;
All other headers — including &lt;code&gt;Authorization&lt;/code&gt;, &lt;code&gt;apikey&lt;/code&gt;, and &lt;code&gt;Content-Type&lt;/code&gt; — pass through untouched.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Upgrade&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;websocket&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;targetUrl&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;method&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Supabase Realtime connects via WebSocket. The browser sends an HTTP Upgrade request. We detect it and forward it directly — Cloudflare Workers handle the protocol upgrade natively.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;targetUrl&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;method&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;GET&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;HEAD&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;method&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;redirect&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;follow&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For all other requests: forward method, headers, and body. GET and HEAD cannot have a body per the HTTP spec — passing &lt;code&gt;undefined&lt;/code&gt; avoids a runtime error. &lt;code&gt;redirect: 'follow'&lt;/code&gt; handles any redirects Supabase issues transparently. &lt;/p&gt;

&lt;h2&gt;
  
  
  Verification
&lt;/h2&gt;

&lt;p&gt;After deploying:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Open your app from an Indian network, or use a VPN set to India&lt;/li&gt;
&lt;li&gt;Open DevTools → Network tab&lt;/li&gt;
&lt;li&gt;Look for Supabase-related requests&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Before the fix:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;❌ GET https://abcdefgh.supabase.co/rest/v1/transactions
   Status: (failed) net::ERR_NAME_NOT_RESOLVED
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;After the fix:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;✅ GET https://supabase-proxy.account.workers.dev/rest/v1/transactions
   Status: 200 OK
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Also verify in the Cloudflare dashboard:&lt;br&gt;
Workers &amp;amp; Pages → supabase-proxy → &lt;strong&gt;Metrics&lt;/strong&gt;&lt;br&gt;
You should see request counts climbing as your users hit the Worker. If you don't, check the Worker logs for any errors.&lt;/p&gt;
&lt;h2&gt;
  
  
  This Generalises to Any Blocked Service
&lt;/h2&gt;

&lt;p&gt;The Worker doesn't care that it's proxying Supabase. Change &lt;code&gt;SUPABASE_ORIGIN&lt;/code&gt; to any blocked backend URL:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Firebase&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;SUPABASE_ORIGIN&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://your-project.firebaseio.com&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// PlanetScale&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;SUPABASE_ORIGIN&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://your-db.us-east.psdb.cloud&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// Any REST API&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;SUPABASE_ORIGIN&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://api.your-service.com&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Same Worker code, different origin constant. The pattern is universal. You can use this to proxy any blocked service to any non-blocked domain.&lt;/p&gt;

&lt;h2&gt;
  
  
  Limitations
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;This is a workaround, not a root fix.&lt;/strong&gt; The correct fix is for Indian ISPs to unblock Supabase's DNS records, which is outside your control. This proxy adds one network hop (browser → Cloudflare edge → Supabase), which adds a small amount of latency. In practice, Cloudflare's global edge network means the hop is geographically close to both the user and Supabase, so the latency impact is minimal.&lt;/p&gt;

&lt;p&gt;The free tier gives you 100,000 requests per day. For most applications, this is more than sufficient. If you exceed it, Cloudflare's paid plan is $5/month for 10 million requests. &lt;/p&gt;

&lt;p&gt;The entire fix is:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;A 25-line Cloudflare Worker&lt;/li&gt;
&lt;li&gt;One environment variable change&lt;/li&gt;
&lt;li&gt;A two-line CSP update&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Your application code stays untouched.&lt;/p&gt;

</description>
      <category>tutorial</category>
      <category>devops</category>
      <category>webdev</category>
      <category>architecture</category>
    </item>
  </channel>
</rss>
