<?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: Aravind Govindhasamy</title>
    <description>The latest articles on DEV Community by Aravind Govindhasamy (@aravind-govindhasamy).</description>
    <link>https://dev.to/aravind-govindhasamy</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.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4200811%2Fe645e166-74a2-46f0-8d3d-22a6f604c53a.jpg</url>
      <title>DEV Community: Aravind Govindhasamy</title>
      <link>https://dev.to/aravind-govindhasamy</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/aravind-govindhasamy"/>
    <language>en</language>
    <item>
      <title>Deploy FastAPI and a React app on a VPS with nginx, systemd and uv</title>
      <dc:creator>Aravind Govindhasamy</dc:creator>
      <pubDate>Sun, 25 Oct 2026 00:00:00 +0000</pubDate>
      <link>https://dev.to/aravind-govindhasamy/deploy-fastapi-and-a-react-app-on-a-vps-with-nginx-systemd-and-uv-3ek0</link>
      <guid>https://dev.to/aravind-govindhasamy/deploy-fastapi-and-a-react-app-on-a-vps-with-nginx-systemd-and-uv-3ek0</guid>
      <description>&lt;p&gt;A single small VPS (or a home server) runs a FastAPI + React app comfortably. No containers needed: the frontend is static files, the API is one Python process under systemd, and nginx sits in front of both.&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%2Fstarter.budeglobal.in%2Fblog%2Fdeploy-architecture.svg" 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%2Fstarter.budeglobal.in%2Fblog%2Fdeploy-architecture.svg" alt="Browsers reach nginx, which serves the frontend's static files and proxies API requests to uvicorn; uvicorn calls Supabase" width="960" height="380"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What you need
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;An Ubuntu server (22.04 or newer) you can SSH into&lt;/li&gt;
&lt;li&gt;A domain with two DNS records pointing at the server: &lt;code&gt;example.com&lt;/code&gt; (the site) and &lt;code&gt;api.example.com&lt;/code&gt; (the API)&lt;/li&gt;
&lt;li&gt;Your app's code, with the backend managed by &lt;a href="https://docs.astral.sh/uv/" rel="noopener noreferrer"&gt;uv&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  1. Prepare the server
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;apt update &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;sudo &lt;/span&gt;apt &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; nginx
curl &lt;span class="nt"&gt;-LsSf&lt;/span&gt; https://astral.sh/uv/install.sh | sh &lt;span class="c"&gt;# installs uv for your user&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Open only what you need with the firewall:&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="nb"&gt;sudo &lt;/span&gt;ufw allow OpenSSH &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;sudo &lt;/span&gt;ufw allow &lt;span class="s1"&gt;'Nginx Full'&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;sudo &lt;/span&gt;ufw &lt;span class="nb"&gt;enable&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  2. Build the frontend where you develop
&lt;/h2&gt;

&lt;p&gt;Build on your own machine (or in CI), not on the server: the server doesn't need Node, and a build failure never takes the site down.&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="nb"&gt;cd &lt;/span&gt;frontend
&lt;span class="nv"&gt;API_URL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;https://api.example.com &lt;span class="nv"&gt;SITE_URL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;https://example.com npm run build

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Copy the output to the server:&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="nb"&gt;tar&lt;/span&gt; &lt;span class="nt"&gt;-czf&lt;/span&gt; - &lt;span class="nt"&gt;-C&lt;/span&gt; dist &lt;span class="nb"&gt;.&lt;/span&gt; | ssh user@server &lt;span class="s1"&gt;'sudo mkdir -p /var/www/example &amp;amp;&amp;amp; sudo tar -xzf - -C /var/www/example'&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;(If your build writes to a subfolder, such as &lt;code&gt;dist/client&lt;/code&gt; for a pre-rendered TanStack Start site, copy that folder instead.)&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Install the API
&lt;/h2&gt;

&lt;p&gt;Copy the backend to the server (for example with &lt;code&gt;git archive&lt;/code&gt;), then:&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="nb"&gt;cd&lt;/span&gt; ~/app/backend
uv &lt;span class="nb"&gt;sync&lt;/span&gt; &lt;span class="nt"&gt;--frozen&lt;/span&gt; &lt;span class="nt"&gt;--no-dev&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Put secrets in &lt;code&gt;~/app/.env&lt;/code&gt; with permissions &lt;code&gt;600&lt;/code&gt;. Then create &lt;code&gt;/etc/systemd/system/example-api.service&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight systemd"&gt;&lt;code&gt;&lt;span class="k"&gt;[Unit]&lt;/span&gt;
&lt;span class="nt"&gt;Description&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;Example API
&lt;span class="nt"&gt;After&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;network-online.target

&lt;span class="k"&gt;[Service]&lt;/span&gt;
&lt;span class="nt"&gt;User&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;deploy
&lt;span class="nt"&gt;WorkingDirectory&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;/home/deploy/app/backend
&lt;span class="nt"&gt;EnvironmentFile&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;/home/deploy/app/.env
&lt;span class="nt"&gt;ExecStart&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;/home/deploy/app/backend/.venv/bin/uvicorn app.main:app --host 127.0.0.1 --port 8000 --workers 2 --proxy-headers --forwarded-allow-ips 127.0.0.1
&lt;span class="nt"&gt;Restart&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;always

&lt;span class="k"&gt;[Install]&lt;/span&gt;
&lt;span class="nt"&gt;WantedBy&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;multi-user.target


&lt;span class="err"&gt;sudo&lt;/span&gt; &lt;span class="err"&gt;systemctl&lt;/span&gt; &lt;span class="err"&gt;daemon-reload&lt;/span&gt; &lt;span class="err"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="err"&gt;sudo&lt;/span&gt; &lt;span class="err"&gt;systemctl&lt;/span&gt; &lt;span class="err"&gt;enable&lt;/span&gt; &lt;span class="err"&gt;--now&lt;/span&gt; &lt;span class="err"&gt;example-api&lt;/span&gt;
&lt;span class="err"&gt;curl&lt;/span&gt; &lt;span class="err"&gt;http://127.0.0.1:8000/health&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;--proxy-headers&lt;/code&gt; with &lt;code&gt;--forwarded-allow-ips 127.0.0.1&lt;/code&gt; makes uvicorn trust nginx's &lt;code&gt;X-Forwarded-For&lt;/code&gt;, so your logs and rate limits see real client IPs, not &lt;code&gt;127.0.0.1&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Configure nginx
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;/etc/nginx/sites-available/example&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight nginx"&gt;&lt;code&gt;&lt;span class="k"&gt;server&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kn"&gt;server_name&lt;/span&gt; &lt;span class="s"&gt;example.com&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kn"&gt;root&lt;/span&gt; &lt;span class="n"&gt;/var/www/example&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="kn"&gt;location&lt;/span&gt; &lt;span class="n"&gt;/assets/&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kn"&gt;expires&lt;/span&gt; &lt;span class="s"&gt;1y&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kn"&gt;add_header&lt;/span&gt; &lt;span class="s"&gt;Cache-Control&lt;/span&gt; &lt;span class="s"&gt;"public,&lt;/span&gt; &lt;span class="s"&gt;immutable"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kn"&gt;try_files&lt;/span&gt; &lt;span class="nv"&gt;$uri&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;404&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="kn"&gt;location&lt;/span&gt; &lt;span class="n"&gt;/&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kn"&gt;add_header&lt;/span&gt; &lt;span class="s"&gt;Cache-Control&lt;/span&gt; &lt;span class="s"&gt;"no-cache"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kn"&gt;try_files&lt;/span&gt; &lt;span class="nv"&gt;$uri&lt;/span&gt; &lt;span class="nv"&gt;$uri&lt;/span&gt;&lt;span class="n"&gt;/index.html&lt;/span&gt; &lt;span class="n"&gt;/index.html&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="k"&gt;server&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kn"&gt;server_name&lt;/span&gt; &lt;span class="s"&gt;api.example.com&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="kn"&gt;location&lt;/span&gt; &lt;span class="n"&gt;/&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kn"&gt;proxy_pass&lt;/span&gt; &lt;span class="s"&gt;http://127.0.0.1:8000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kn"&gt;proxy_set_header&lt;/span&gt; &lt;span class="s"&gt;Host&lt;/span&gt; &lt;span class="nv"&gt;$host&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kn"&gt;proxy_set_header&lt;/span&gt; &lt;span class="s"&gt;X-Forwarded-For&lt;/span&gt; &lt;span class="nv"&gt;$proxy_add_x_forwarded_for&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kn"&gt;proxy_set_header&lt;/span&gt; &lt;span class="s"&gt;X-Forwarded-Proto&lt;/span&gt; &lt;span class="nv"&gt;$scheme&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;p&gt;The &lt;code&gt;try_files&lt;/code&gt; line serves a page's own HTML when it exists (pre-rendered pages) and falls back to the single-page app for every other path. Use your SPA's shell file in place of &lt;code&gt;/index.html&lt;/code&gt; if it has a different name.&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="nb"&gt;sudo ln&lt;/span&gt; &lt;span class="nt"&gt;-s&lt;/span&gt; /etc/nginx/sites-available/example /etc/nginx/sites-enabled/
&lt;span class="nb"&gt;sudo &lt;/span&gt;nginx &lt;span class="nt"&gt;-t&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl reload nginx

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  5. HTTPS
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;apt &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; certbot python3-certbot-nginx
&lt;span class="nb"&gt;sudo &lt;/span&gt;certbot &lt;span class="nt"&gt;--nginx&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt; example.com &lt;span class="nt"&gt;-d&lt;/span&gt; api.example.com

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Certbot edits the nginx config and renews certificates automatically. Behind Cloudflare, or with no public IP at all, use &lt;a href="https://starter.budeglobal.in/blog/cloudflare-tunnel-self-host" rel="noopener noreferrer"&gt;Cloudflare Tunnel&lt;/a&gt; instead and skip this step.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Redeploy
&lt;/h2&gt;

&lt;p&gt;A redeploy is the same three moves: build and copy the frontend, copy the backend and &lt;code&gt;uv sync&lt;/code&gt;, restart the service.&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="nb"&gt;sudo &lt;/span&gt;systemctl restart example-api &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; curl &lt;span class="nt"&gt;-fsS&lt;/span&gt; https://api.example.com/health

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Checklist
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;.env&lt;/code&gt; is readable only by the service user, and never committed.&lt;/li&gt;
&lt;li&gt;The API listens on &lt;code&gt;127.0.0.1&lt;/code&gt; only; nginx is the only public entry.&lt;/li&gt;
&lt;li&gt;An uptime monitor watches &lt;code&gt;/health&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Logs: &lt;code&gt;journalctl -u example-api -f&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Using it with FastAPI Supabase Starter
&lt;/h2&gt;

&lt;p&gt;This is how the starter's own demo runs. Build with &lt;code&gt;API_URL&lt;/code&gt; and &lt;code&gt;SITE_URL&lt;/code&gt;, copy &lt;code&gt;frontend/dist/client&lt;/code&gt;, and use &lt;code&gt;/_shell.html&lt;/code&gt; as the fallback in &lt;code&gt;try_files&lt;/code&gt;. The API needs no database password: it talks to Supabase over HTTPS. Webhook and hook URLs to register afterwards are listed in &lt;a href="https://starter.budeglobal.in/help/deploying" rel="noopener noreferrer"&gt;Deploy to production&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>fastapi</category>
      <category>react</category>
      <category>nginx</category>
      <category>devops</category>
    </item>
    <item>
      <title>Cloudflare Tunnel: publish a home server or VPS without opening ports</title>
      <dc:creator>Aravind Govindhasamy</dc:creator>
      <pubDate>Thu, 22 Oct 2026 15:52:06 +0000</pubDate>
      <link>https://dev.to/aravind-govindhasamy/cloudflare-tunnel-publish-a-home-server-or-vps-without-opening-ports-4cbc</link>
      <guid>https://dev.to/aravind-govindhasamy/cloudflare-tunnel-publish-a-home-server-or-vps-without-opening-ports-4cbc</guid>
      <description>&lt;p&gt;Cloudflare Tunnel lets a machine that can't accept incoming connections (a home server behind your router, a laptop, a VPS with every port closed) serve websites on your domain. A small program, &lt;code&gt;cloudflared&lt;/code&gt;, opens an outbound connection to Cloudflare, and Cloudflare sends your visitors' requests down it. You get HTTPS, DDoS protection and no open ports, for free.&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%2Fstarter.budeglobal.in%2Fblog%2Fcloudflare-tunnel.svg" 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%2Fstarter.budeglobal.in%2Fblog%2Fcloudflare-tunnel.svg" alt="Visitors connect to Cloudflare over HTTPS; cloudflared on your server keeps an outbound tunnel to Cloudflare and forwards each hostname to a local port" width="960" height="320"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What you need
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;A domain using Cloudflare's nameservers (add it to Cloudflare and change the nameservers at your registrar)&lt;/li&gt;
&lt;li&gt;A Linux machine running your app on local ports, for example a website on &lt;code&gt;localhost:8080&lt;/code&gt; and an API on &lt;code&gt;localhost:8000&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  1. Create the tunnel
&lt;/h2&gt;

&lt;p&gt;In the Cloudflare dashboard, go to &lt;strong&gt;Networking → Tunnels&lt;/strong&gt; (in the Zero Trust dashboard it's &lt;strong&gt;Networks → Tunnels&lt;/strong&gt; ) and click &lt;strong&gt;Create a tunnel&lt;/strong&gt;. Choose &lt;strong&gt;cloudflared&lt;/strong&gt; , give it a name such as &lt;code&gt;home-server&lt;/code&gt;, and save.&lt;/p&gt;

&lt;p&gt;Cloudflare shows install commands for your operating system. On Debian or Ubuntu they add Cloudflare's package repository, install &lt;code&gt;cloudflared&lt;/code&gt;, and register it as a service with your tunnel's token:&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="nb"&gt;sudo &lt;/span&gt;cloudflared service &lt;span class="nb"&gt;install&lt;/span&gt; &amp;lt;TOKEN&amp;gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Within a few seconds the dashboard shows the connector as &lt;strong&gt;Connected&lt;/strong&gt;. The token is a credential: anyone with it can run your tunnel, so don't paste it in public.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Route a hostname to a local port
&lt;/h2&gt;

&lt;p&gt;Open the tunnel, go to &lt;strong&gt;Routes&lt;/strong&gt; , click &lt;strong&gt;Add route&lt;/strong&gt; and choose &lt;strong&gt;Published application&lt;/strong&gt; (older dashboards call this &lt;strong&gt;Public hostnames → Add a public hostname&lt;/strong&gt; ):&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;Example&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Subdomain&lt;/td&gt;
&lt;td&gt;&lt;code&gt;app&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Domain&lt;/td&gt;
&lt;td&gt;&lt;code&gt;example.com&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Path&lt;/td&gt;
&lt;td&gt;(empty)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Service URL&lt;/td&gt;
&lt;td&gt;&lt;code&gt;http://localhost:8080&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Save. Cloudflare creates the DNS record for &lt;code&gt;app.example.com&lt;/code&gt; itself (a proxied CNAME to your tunnel), and the site is live over HTTPS. No restart needed.&lt;/p&gt;

&lt;p&gt;Add as many hostnames as you like to one tunnel, for example &lt;code&gt;api.example.com → http://localhost:8000&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. See real visitor IPs
&lt;/h2&gt;

&lt;p&gt;Requests reach your app from &lt;code&gt;cloudflared&lt;/code&gt; on the same machine, so your app sees &lt;code&gt;127.0.0.1&lt;/code&gt; as every visitor's address. Cloudflare passes the real one in &lt;code&gt;X-Forwarded-For&lt;/code&gt; and &lt;code&gt;CF-Connecting-IP&lt;/code&gt;. Tell your server to trust them from localhost:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;uvicorn (FastAPI):&lt;/strong&gt; &lt;code&gt;--proxy-headers --forwarded-allow-ips 127.0.0.1&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;nginx:&lt;/strong&gt; &lt;code&gt;set_real_ip_from 127.0.0.1; real_ip_header CF-Connecting-IP;&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Without this, per-IP rate limits treat all your visitors as one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Gotchas
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Dashboard tunnels ignore local ingress rules.&lt;/strong&gt; If you created the tunnel in the dashboard, editing &lt;code&gt;/etc/cloudflared/config.yml&lt;/code&gt; changes nothing: the routes come from the dashboard. Manage hostnames there.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Restarting &lt;code&gt;cloudflared&lt;/code&gt; drops every hostname on the tunnel&lt;/strong&gt; for a few seconds. You don't need to restart it to add or change routes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Keep your firewall closed.&lt;/strong&gt; The tunnel is outbound, so &lt;code&gt;ufw default deny incoming&lt;/code&gt; stays as it is.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Webhooks work.&lt;/strong&gt; Stripe, Razorpay and Supabase hooks reach a tunneled API like any HTTPS URL, which also makes a tunnel handy for testing webhooks from your laptop.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cloudflare settings apply.&lt;/strong&gt; Caching rules, Bot Fight Mode and Cloudflare's managed &lt;code&gt;robots.txt&lt;/code&gt; affect tunneled sites like any other. If search engines or AI crawlers can't see your site, check those first.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Protect private tools.&lt;/strong&gt; Put admin hostnames behind &lt;strong&gt;Cloudflare Access&lt;/strong&gt; so only your email can reach them.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Check it
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-I&lt;/span&gt; https://app.example.com &lt;span class="c"&gt;# should show server: cloudflare&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl status cloudflared &lt;span class="c"&gt;# connector running&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Using it with FastAPI Supabase Starter
&lt;/h2&gt;

&lt;p&gt;Serve the built frontend (for example with nginx on a local port) and run the API on &lt;code&gt;127.0.0.1:8000&lt;/code&gt;, then add two routes: your site's hostname to the frontend's port and &lt;code&gt;api.&lt;/code&gt; to port 8000. Set &lt;code&gt;API_URL&lt;/code&gt;, &lt;code&gt;FRONTEND_URL&lt;/code&gt; and &lt;code&gt;SITE_URL&lt;/code&gt; to the public HTTPS addresses, and keep &lt;code&gt;FORWARDED_ALLOW_IPS&lt;/code&gt; at its localhost default so the request log and rate limit see real IPs. The full server setup is in &lt;a href="https://dev.to/aravind_govindhasamy/deploy-fastapi-and-a-react-app-on-a-vps-with-nginx-systemd-and-uv-100e-temp-slug-7151020"&gt;Deploy FastAPI and React on a VPS&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>python</category>
      <category>fastapi</category>
      <category>supabase</category>
      <category>security</category>
    </item>
    <item>
      <title>Send Supabase Auth emails through Resend SMTP (with Cloudflare DNS)</title>
      <dc:creator>Aravind Govindhasamy</dc:creator>
      <pubDate>Thu, 22 Oct 2026 00:00:00 +0000</pubDate>
      <link>https://dev.to/aravind-govindhasamy/send-supabase-auth-emails-through-resend-smtp-with-cloudflare-dns-56bh</link>
      <guid>https://dev.to/aravind-govindhasamy/send-supabase-auth-emails-through-resend-smtp-with-cloudflare-dns-56bh</guid>
      <description>&lt;p&gt;Out of the box, Supabase Auth sends confirmation, magic-link and password-reset emails through a shared service meant for testing. It only delivers to members of your Supabase team, and only about 2 messages an hour. The first real user who signs up won't get their email. The fix is custom SMTP, and &lt;a href="https://resend.com" rel="noopener noreferrer"&gt;Resend&lt;/a&gt; is a quick, developer-friendly option.&lt;/p&gt;

&lt;h2&gt;
  
  
  What you need
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;A Resend account&lt;/li&gt;
&lt;li&gt;A domain you control, with DNS on Cloudflare (other DNS hosts work the same way)&lt;/li&gt;
&lt;li&gt;Your Supabase project&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  1. Pick a sending domain
&lt;/h2&gt;

&lt;p&gt;Send from a subdomain, such as &lt;code&gt;mail.example.com&lt;/code&gt; or &lt;code&gt;auth.example.com&lt;/code&gt;, rather than your root domain. It keeps your sending reputation separate from your main domain's mail, and Supabase's own guidance recommends a separate domain for auth emails.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Watch out:&lt;/strong&gt; the address you send from must be on the domain you verify. If you verify &lt;code&gt;mail.example.com&lt;/code&gt;, send from &lt;code&gt;no-reply@mail.example.com&lt;/code&gt;. Sending from &lt;code&gt;no-reply@example.com&lt;/code&gt; fails with a "domain is not verified" error.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Add the domain in Resend
&lt;/h2&gt;

&lt;p&gt;In Resend, go to &lt;strong&gt;Domains → Add domain&lt;/strong&gt; and enter your sending domain. Resend shows the DNS records to add.&lt;/p&gt;

&lt;p&gt;On Cloudflare, Resend's &lt;strong&gt;Sign in to Cloudflare&lt;/strong&gt; button can add them for you. To add them by hand, open your zone's &lt;strong&gt;DNS → Records&lt;/strong&gt; and create:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Type&lt;/th&gt;
&lt;th&gt;Name&lt;/th&gt;
&lt;th&gt;Content&lt;/th&gt;
&lt;th&gt;Notes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;MX&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;send&lt;/code&gt; (or &lt;code&gt;send.mail&lt;/code&gt; for a subdomain)&lt;/td&gt;
&lt;td&gt;the &lt;code&gt;feedback-smtp...amazonses.com&lt;/code&gt; value Resend shows&lt;/td&gt;
&lt;td&gt;Priority 10&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;TXT&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;send&lt;/code&gt; (or &lt;code&gt;send.mail&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;v=spf1 include:amazonses.com ~all&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;SPF&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;TXT&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;resend._domainkey&lt;/code&gt; (or &lt;code&gt;resend._domainkey.mail&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;the long &lt;code&gt;p=...&lt;/code&gt; value Resend shows&lt;/td&gt;
&lt;td&gt;DKIM, &lt;strong&gt;DNS only&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Copy the exact names Resend gives you: for a subdomain they include it. In the Name field, enter only the part before your zone's domain; Cloudflare adds the rest.&lt;/p&gt;

&lt;p&gt;Then click &lt;strong&gt;Verify&lt;/strong&gt; in Resend. Records usually propagate in minutes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Recommended:&lt;/strong&gt; add a DMARC record too, for example a TXT record named &lt;code&gt;_dmarc&lt;/code&gt; with &lt;code&gt;v=DMARC1; p=none;&lt;/code&gt; to start, then tighten it once mail flows cleanly.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Create an API key
&lt;/h2&gt;

&lt;p&gt;In Resend, go to &lt;strong&gt;API Keys → Create API key&lt;/strong&gt;. For SMTP, a key with &lt;strong&gt;Sending access&lt;/strong&gt; is enough. Copy it now; it's shown once.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Turn on custom SMTP in Supabase
&lt;/h2&gt;

&lt;p&gt;In the Supabase dashboard, open &lt;strong&gt;Authentication → Emails → SMTP Settings&lt;/strong&gt; and enable custom SMTP:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Sender email&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;no-reply@mail.example.com&lt;/code&gt; (on your verified domain)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sender name&lt;/td&gt;
&lt;td&gt;Your product name&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Host&lt;/td&gt;
&lt;td&gt;&lt;code&gt;smtp.resend.com&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Port&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;587&lt;/code&gt; (STARTTLS) or &lt;code&gt;465&lt;/code&gt; (SSL)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Username&lt;/td&gt;
&lt;td&gt;&lt;code&gt;resend&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Password&lt;/td&gt;
&lt;td&gt;your Resend API key (&lt;code&gt;re_...&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Save. Supabase now sends to any address, starting with a limit of 30 emails an hour; raise it under &lt;strong&gt;Authentication → Rate Limits&lt;/strong&gt; once you're sure the setup is right.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Send a test
&lt;/h2&gt;

&lt;p&gt;Sign up with an address outside your team, or trigger a password reset. The email should arrive within seconds, and it shows up in Resend's &lt;strong&gt;Emails&lt;/strong&gt; list with its delivery status. If it doesn't:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;"Domain is not verified":&lt;/strong&gt; the sender address isn't on the verified domain (see step 1).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Nothing in Resend's list:&lt;/strong&gt; the SMTP details in Supabase are wrong; re-enter the password.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Lands in spam:&lt;/strong&gt; check SPF and DKIM pass in the message headers, and add DMARC.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  6. Send your app's own emails
&lt;/h2&gt;

&lt;p&gt;Invitations and notifications from your API can use the same SMTP details. In FastAPI with the standard library:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;smtplib&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;email.message&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;EmailMessage&lt;/span&gt;

&lt;span class="n"&gt;msg&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;EmailMessage&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;From&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;no-reply@mail.example.com&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;To&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;user@example.com&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Subject&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;You&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;re invited&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set_content&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Join the team: https://example.com/invite/abc&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;smtplib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;SMTP&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;smtp.resend.com&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;587&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;smtp&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;smtp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;starttls&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;smtp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;login&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;resend&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;RESEND_API_KEY&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;smtp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send_message&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run it in a thread (&lt;code&gt;run_in_threadpool&lt;/code&gt;) or a background task so a slow mail server doesn't block requests.&lt;/p&gt;

&lt;h2&gt;
  
  
  Using it with FastAPI Supabase Starter
&lt;/h2&gt;

&lt;p&gt;Steps 1 to 5 are the same. For the app's own emails (organization invitations and templates from &lt;strong&gt;Admin › Messaging&lt;/strong&gt; ), put the same values in &lt;code&gt;.env&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;SMTP_HOST=smtp.resend.com
SMTP_PORT=587
SMTP_USER=resend
SMTP_PASSWORD=re_xxx
EMAILS_FROM_EMAIL=no-reply@mail.example.com

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every message is recorded with its status in the delivery log under &lt;strong&gt;Admin › Messaging&lt;/strong&gt;. More in &lt;a href="https://starter.budeglobal.in/help/email-and-messaging" rel="noopener noreferrer"&gt;Email, SMS and WhatsApp&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>payments</category>
      <category>razorpay</category>
      <category>fastapi</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Supabase API keys explained: publishable, secret and JWT signing keys for FastAPI</title>
      <dc:creator>Aravind Govindhasamy</dc:creator>
      <pubDate>Thu, 22 Oct 2026 00:00:00 +0000</pubDate>
      <link>https://dev.to/aravind-govindhasamy/supabase-api-keys-explained-publishable-secret-and-jwt-signing-keys-for-fastapi-49lm</link>
      <guid>https://dev.to/aravind-govindhasamy/supabase-api-keys-explained-publishable-secret-and-jwt-signing-keys-for-fastapi-49lm</guid>
      <description>&lt;p&gt;Supabase gives every project a URL and a handful of keys, and mixing them up is the most common way to leak data. This guide covers what each one is, where it belongs, and how a FastAPI backend should use them.&lt;/p&gt;

&lt;h2&gt;
  
  
  The keys at a glance
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Key&lt;/th&gt;
&lt;th&gt;Looks like&lt;/th&gt;
&lt;th&gt;Where it goes&lt;/th&gt;
&lt;th&gt;Row-level security&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Project URL&lt;/td&gt;
&lt;td&gt;&lt;code&gt;https://&amp;lt;project-ref&amp;gt;.supabase.co&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Browser and server&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Publishable key&lt;/td&gt;
&lt;td&gt;&lt;code&gt;sb_publishable_...&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Browser, mobile apps, and your server&lt;/td&gt;
&lt;td&gt;Applies&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Secret key&lt;/td&gt;
&lt;td&gt;&lt;code&gt;sb_secret_...&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Server only, never a browser&lt;/td&gt;
&lt;td&gt;Bypassed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Legacy &lt;code&gt;anon&lt;/code&gt; key&lt;/td&gt;
&lt;td&gt;A long JWT starting &lt;code&gt;eyJ&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Same as publishable&lt;/td&gt;
&lt;td&gt;Applies&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Legacy &lt;code&gt;service_role&lt;/code&gt; key&lt;/td&gt;
&lt;td&gt;A long JWT starting &lt;code&gt;eyJ&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Same as secret&lt;/td&gt;
&lt;td&gt;Bypassed&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The legacy &lt;code&gt;anon&lt;/code&gt; and &lt;code&gt;service_role&lt;/code&gt; keys still work, but Supabase is retiring them by the end of 2026. Use the publishable and secret keys in new code.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Find your URL and keys
&lt;/h2&gt;

&lt;p&gt;In the Supabase dashboard, open your project and go to &lt;strong&gt;Project Settings → API Keys&lt;/strong&gt;. The &lt;strong&gt;Connect&lt;/strong&gt; button at the top of the project also shows the URL and publishable key.&lt;/p&gt;

&lt;p&gt;Put them in your backend's environment:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;SUPABASE_URL=https://&amp;lt;project-ref&amp;gt;.supabase.co
SUPABASE_PUBLISHABLE_KEY=sb_publishable_...
SUPABASE_SECRET_KEY=sb_secret_...

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The frontend only ever gets the URL and the publishable key.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Know what the secret key can do
&lt;/h2&gt;

&lt;p&gt;The secret key skips row-level security: any query made with it can read and change every row. Supabase refuses it from a browser (it answers 401), but that check isn't protection on its own. Keep it in server environment variables only, and use it only for work no user owns, such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;recording a payment after a verified webhook&lt;/li&gt;
&lt;li&gt;looking up an API key's owner&lt;/li&gt;
&lt;li&gt;sending a message on the system's behalf&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For everything a user asks for, call Supabase &lt;strong&gt;as that user&lt;/strong&gt; (step 4).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tip:&lt;/strong&gt; create a separate secret key for each backend component (API, workers, scripts). If one leaks, you rotate that one: create a new key under &lt;strong&gt;API Keys&lt;/strong&gt; , deploy it, confirm it works, then delete the old one.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Verify the user's token in FastAPI
&lt;/h2&gt;

&lt;p&gt;When a user signs in, Supabase Auth gives the browser an access token (a JWT). The browser sends it to your API as &lt;code&gt;Authorization: Bearer &amp;lt;token&amp;gt;&lt;/code&gt;. Your API must check it before trusting it.&lt;/p&gt;

&lt;p&gt;Supabase signs tokens with &lt;strong&gt;JWT signing keys&lt;/strong&gt;. With asymmetric keys (ES256 or RS256), the public keys are published at a JWKS endpoint, so your API can verify tokens without any secret:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;https://&amp;lt;project-ref&amp;gt;.supabase.co/auth/v1/.well-known/jwks.json

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With PyJWT:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;jwt&lt;/span&gt;

&lt;span class="n"&gt;jwks&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;jwt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;PyJWKClient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;SUPABASE_URL&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/auth/v1/.well-known/jwks.json&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cache_keys&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;verify_token&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;alg&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;jwt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_unverified_header&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;token&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;alg&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;alg&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ES256&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;RS256&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="n"&gt;jwt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;InvalidAlgorithmError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;alg&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;jwks&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_signing_key_from_jwt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;jwt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;decode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;algorithms&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;alg&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;audience&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;authenticated&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                      &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;require&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;exp&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sub&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]})&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Older projects may still sign with the legacy shared &lt;strong&gt;JWT secret&lt;/strong&gt; (HS256). If yours does, verify with that secret instead, or migrate to signing keys on the &lt;strong&gt;JWT Keys&lt;/strong&gt; page of your project settings, where Supabase moves keys through standby, current and revoked states so you can rotate in stages. Always pin the algorithm: never accept whatever the token's header claims.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Call Supabase as the user
&lt;/h2&gt;

&lt;p&gt;The safest pattern is to pass the user's own token through to Supabase. Postgres then sees who is asking, and your row-level security policies decide what they can read or write:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;postgrest&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;SyncPostgrestClient&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;as_user&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;SyncPostgrestClient&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;SyncPostgrestClient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;SUPABASE_URL&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/rest/v1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;apikey&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;SUPABASE_PUBLISHABLE_KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Authorization&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Bearer &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;items&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;as_user&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;from_&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;items&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;select&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;*&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="c1"&gt;# only this user's rows
&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Compare that with connecting to Postgres directly or using the secret key for everything: then every route has to remember to filter by owner, and one forgotten &lt;code&gt;where user_id = ...&lt;/code&gt; leaks other people's data. &lt;a href="https://dev.to/aravind_govindhasamy/why-our-fastapi-backend-never-connects-to-postgres-directly-3hhh-temp-slug-2831773"&gt;Why we never connect to Postgres directly&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Checklist
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;The browser has the URL and publishable key, nothing else.&lt;/li&gt;
&lt;li&gt;The secret key lives in server environment variables and is used only for system work.&lt;/li&gt;
&lt;li&gt;Your API verifies every token with the JWKS endpoint and a pinned algorithm.&lt;/li&gt;
&lt;li&gt;User requests reach Supabase with the user's own token, so RLS applies.&lt;/li&gt;
&lt;li&gt;Every table has RLS enabled, with policies written for &lt;code&gt;auth.uid()&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Using it with FastAPI Supabase Starter
&lt;/h2&gt;

&lt;p&gt;The starter is built this way. Copy the three values above into &lt;code&gt;.env&lt;/code&gt; and you're done: &lt;code&gt;verify_token()&lt;/code&gt; checks tokens against your project's JWKS (with the legacy secret as a fallback), every route queries through &lt;code&gt;as_user(token)&lt;/code&gt;, and the secret key is used only for verified webhooks, payments and API-key lookups. API keys and MCP clients get a real Supabase session for their owner, so the same RLS applies to them. Setup steps are in the &lt;a href="https://starter.budeglobal.in/help/quick-start" rel="noopener noreferrer"&gt;quick start&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>supabase</category>
      <category>fastapi</category>
      <category>python</category>
      <category>security</category>
    </item>
    <item>
      <title>Stop bot sign-ups on Supabase with Cloudflare Turnstile</title>
      <dc:creator>Aravind Govindhasamy</dc:creator>
      <pubDate>Thu, 22 Oct 2026 00:00:00 +0000</pubDate>
      <link>https://dev.to/aravind-govindhasamy/stop-bot-sign-ups-on-supabase-with-cloudflare-turnstile-5cpk</link>
      <guid>https://dev.to/aravind-govindhasamy/stop-bot-sign-ups-on-supabase-with-cloudflare-turnstile-5cpk</guid>
      <description>&lt;p&gt;Bots find sign-up forms within days of launch. They create junk accounts and, worse, use your password-reset and magic-link forms to send email to people who never asked for it, which hurts your sending reputation. Supabase Auth can require a CAPTCHA on every sign-up, sign-in and reset, and Cloudflare Turnstile is free and usually invisible to real users.&lt;/p&gt;

&lt;h2&gt;
  
  
  How it works
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;The Turnstile widget runs in your page and produces a short-lived token.&lt;/li&gt;
&lt;li&gt;Your frontend passes that token to Supabase with the auth call.&lt;/li&gt;
&lt;li&gt;Supabase checks the token with Cloudflare using your secret key, and refuses the request if it's missing or invalid.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Your backend doesn't need any changes.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Create a Turnstile widget
&lt;/h2&gt;

&lt;p&gt;In the Cloudflare dashboard, open &lt;strong&gt;Turnstile → Add widget&lt;/strong&gt; :&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Name:&lt;/strong&gt; your app's name&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hostnames:&lt;/strong&gt; your domain (&lt;code&gt;example.com&lt;/code&gt;) and &lt;code&gt;localhost&lt;/code&gt; for development&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Widget mode:&lt;/strong&gt;  &lt;strong&gt;Managed&lt;/strong&gt; is a good default; most visitors never see a challenge&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Copy the &lt;strong&gt;Site key&lt;/strong&gt; (public, goes in your frontend) and the &lt;strong&gt;Secret key&lt;/strong&gt; (goes in Supabase).&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Add the widget to your forms
&lt;/h2&gt;

&lt;p&gt;With React, the &lt;code&gt;@marsidev/react-turnstile&lt;/code&gt; package is the simplest route:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="nx"&gt;npm&lt;/span&gt; &lt;span class="nx"&gt;install&lt;/span&gt; &lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;marsidev&lt;/span&gt;&lt;span class="sr"&gt;/react-turnstil&lt;/span&gt;&lt;span class="err"&gt;e
&lt;/span&gt;

&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Turnstile&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@marsidev/react-turnstile&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;captchaToken&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setCaptchaToken&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;useState&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Turnstile&lt;/span&gt; &lt;span class="nx"&gt;siteKey&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;TURNSTILE_SITE_KEY&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="nx"&gt;onSuccess&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;setCaptchaToken&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then pass the token to every auth call Supabase protects:&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;await&lt;/span&gt; &lt;span class="nx"&gt;supabase&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;signUp&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;password&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;options&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;captchaToken&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;supabase&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;signInWithPassword&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;password&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;options&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;captchaToken&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;supabase&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;signInWithOtp&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;options&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;captchaToken&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;supabase&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;resetPasswordForEmail&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;captchaToken&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A token works once. After a failed attempt, reset the widget to get a fresh one.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Deploy the frontend first
&lt;/h2&gt;

&lt;p&gt;This order matters. Once Supabase requires a CAPTCHA, every sign-in without a token is refused, including from users still running your previous build. So:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Deploy the frontend with the widget and &lt;code&gt;captchaToken&lt;/code&gt; in place.&lt;/li&gt;
&lt;li&gt;Check that sign-in still works (Supabase ignores the token until step 4).&lt;/li&gt;
&lt;li&gt;Then turn on enforcement.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  4. Turn on CAPTCHA protection in Supabase
&lt;/h2&gt;

&lt;p&gt;In the Supabase dashboard, open &lt;strong&gt;Authentication → Attack Protection&lt;/strong&gt; (some dashboard versions call it &lt;strong&gt;Bot and Abuse Protection&lt;/strong&gt; ). Turn on &lt;strong&gt;Enable CAPTCHA protection&lt;/strong&gt; , choose &lt;strong&gt;Cloudflare Turnstile&lt;/strong&gt; , paste the &lt;strong&gt;Secret key&lt;/strong&gt; and save.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Test
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Sign up in a normal browser: it should work, usually without a visible challenge.&lt;/li&gt;
&lt;li&gt;Call the auth API without a token (for example with &lt;code&gt;curl&lt;/code&gt;): Supabase should refuse it with a CAPTCHA error.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Troubleshooting
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Symptom&lt;/th&gt;
&lt;th&gt;Cause&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;"captcha verification process failed" for everyone&lt;/td&gt;
&lt;td&gt;The secret in Supabase doesn't match the site key's widget, or the hostname isn't on the widget's list.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Works in production, fails on localhost&lt;/td&gt;
&lt;td&gt;Add &lt;code&gt;localhost&lt;/code&gt; to the widget's hostnames.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Fails on the second try&lt;/td&gt;
&lt;td&gt;Tokens are single-use; reset the widget after each attempt.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Using it with FastAPI Supabase Starter
&lt;/h2&gt;

&lt;p&gt;The widget and the &lt;code&gt;captchaToken&lt;/code&gt; wiring are already in the sign-in page. Put the site key in &lt;code&gt;.env&lt;/code&gt;, rebuild and deploy the frontend, then add the secret key in Supabase:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;TURNSTILE_SITE_KEY=0x4AAAAAAA...

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Keep that variable set in every future build, or sign-in breaks once Supabase requires the CAPTCHA. See &lt;a href="https://starter.budeglobal.in/help/sign-in-methods" rel="noopener noreferrer"&gt;Sign-in methods&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>supabase</category>
      <category>cloudflare</category>
      <category>security</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
