<?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: Víctor Pérez Cano</title>
    <description>The latest articles on DEV Community by Víctor Pérez Cano (@vpcano).</description>
    <link>https://dev.to/vpcano</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%2F3741369%2F7e7f52b6-e356-4c71-80d1-e7bdf44855aa.jpeg</url>
      <title>DEV Community: Víctor Pérez Cano</title>
      <link>https://dev.to/vpcano</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/vpcano"/>
    <language>en</language>
    <item>
      <title>How to Configure Asymmetric JWTs on Self-Hosted Supabase</title>
      <dc:creator>Víctor Pérez Cano</dc:creator>
      <pubDate>Fri, 30 Jan 2026 19:19:41 +0000</pubDate>
      <link>https://dev.to/vpcano/how-to-configure-asymmetric-jwts-on-self-hosted-supabase-53ed</link>
      <guid>https://dev.to/vpcano/how-to-configure-asymmetric-jwts-on-self-hosted-supabase-53ed</guid>
      <description>&lt;p&gt;I personally find self-hosting Supabase a great option for running my appplications on production. Unfortunately, Supabase docs are tailored to the official hosted version, and most of the newest features are not available on the open source one, or at least, well-documented enough. This is the case of &lt;strong&gt;asymmetric signing keys for JWTs&lt;/strong&gt;, which were &lt;a href="https://supabase.com/blog/jwt-signing-keys" rel="noopener noreferrer"&gt;introduced last year&lt;/a&gt;. In this guide I'll explain step by step how I've managed to set them up on my self-hosted Supabase instance.&lt;/p&gt;

&lt;h2&gt;
  
  
  🔐 Why asymmetric signing keys?
&lt;/h2&gt;

&lt;p&gt;Beyond security, using asymmetric cryptography algorithms such as RSA or ECC for signing JWTs brings an important performance improvement. The legacy &lt;code&gt;JWT_SECRET&lt;/code&gt; uses symmetric HS256 algorithm for signing and verifying JWTs. This means that on each request made by a user, the JWT must be verified against this secret on your Supabase instance. However, with asymmetric keys the JWT is signed with a private key that is stored on your Supabase instance, but can be verified just with the corresponding public key. This one is typically available at &lt;code&gt;/auth/v1/.well-known/jwks.json&lt;/code&gt; which is often cached. This allows your application to verify the JWT of the user without making additional requests to Supabase, by calling &lt;a href="https://supabase.com/docs/reference/javascript/auth-getclaims" rel="noopener noreferrer"&gt;&lt;code&gt;getClaims&lt;/code&gt;&lt;/a&gt; instead of &lt;code&gt;getUser&lt;/code&gt;. This is particularly important if you store custom claims on the user's JWT, such as user preferences, roles or subscription details...&lt;/p&gt;

&lt;h2&gt;
  
  
  🚀 How to set them up
&lt;/h2&gt;

&lt;p&gt;I assume you already have a running self-hosted Supabase instance, with a &lt;code&gt;docker-compose.yml&lt;/code&gt; set up. If not, there are several guides on how to set it up on a VPS, but I'd recommend you to use &lt;a href="https://dokploy.com/" rel="noopener noreferrer"&gt;Dokploy&lt;/a&gt; with &lt;a href="https://docs.dokploy.com/docs/templates/supabase" rel="noopener noreferrer"&gt;the following template&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;To keep things simple, I wrote a &lt;a href="https://gist.github.com/vpcano/28e93b8af3cb36ba3ecd9a397ccf0ab7" rel="noopener noreferrer"&gt;small script&lt;/a&gt; that generates the keys using OpenSSL and converts them to the format supabase expects. In a nutshell, the script does the following:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Generates a pair of public/private keys using the algorithm of your choice, either RS256 (RSA) or ES256 (Elliptic Curve).&lt;/li&gt;
&lt;li&gt;Prompts you for an optional Key ID, or generates a random one.&lt;/li&gt;
&lt;li&gt;Writes both keys in JSON Web Key (JWK) format.&lt;/li&gt;
&lt;li&gt;Prompts you for both your previous &lt;code&gt;ANON_KEY&lt;/code&gt; and &lt;code&gt;SERVICE_ROLE_KEY&lt;/code&gt; in order to re-sign them with the new keys.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;You can run the script with the following command:&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;-sL&lt;/span&gt; https://gist.githubusercontent.com/vpcano/28e93b8af3cb36ba3ecd9a397ccf0ab7/raw/33a2769ac9d2ff0b8fa953566ee1fd5a773ec159/supabase-keygen.sh | bash
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The script will output the following environment variables, which you should then set in the corresponding supabase service under the &lt;code&gt;docker-compose.yml&lt;/code&gt; file:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;JWT_SIGNING_KEYS&lt;/code&gt;: this contains both the private and public keys, and must be set to the auth service &lt;code&gt;GOTRUE_JWT_KEYS&lt;/code&gt; env var.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;JWT_METHODS&lt;/code&gt;: this indicates to the auth service that you are now using asymmetric signing algorithms. It must be set to the &lt;code&gt;GOTRUE_JWT_VALID_METHODS&lt;/code&gt; env var of the auth service.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;JWT_JWKS&lt;/code&gt;: this contains the public key in JWK format. It must be set to the following services:

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;rest&lt;/code&gt; -&amp;gt; &lt;code&gt;PGRST_JWT_SECRET&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;rest&lt;/code&gt; -&amp;gt; &lt;code&gt;PGRST_APP_SETTINGS_JWT_SECRET&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;realtime&lt;/code&gt; -&amp;gt; &lt;code&gt;API_JWT_JWKS&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;storage&lt;/code&gt; -&amp;gt; &lt;code&gt;JWT_JWKS&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;
&lt;code&gt;ANON_KEY&lt;/code&gt; and &lt;code&gt;SERVICE_ROLE_KEY&lt;/code&gt;: these should replace your previous ones on all your Supabase services and your client applications.&lt;/li&gt;

&lt;/ul&gt;

&lt;p&gt;The final step is to make the &lt;code&gt;/auth/v1/.well-known/jwks.json&lt;/code&gt; endpoint publicly available. This is done on the kong service config file &lt;code&gt;kong.yml&lt;/code&gt;, which is typically mounted on a volume in the kong service. On Dokploy, this can be found on the "Advanced" tab at the end of the "Volumes" section. I added the following on the &lt;code&gt;services&lt;/code&gt; section under &lt;code&gt;## Open Auth routes&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="c1"&gt;## Open Auth routes&lt;/span&gt;
  &lt;span class="c1"&gt;# ...&lt;/span&gt;

  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;auth-v1-open-jwks&lt;/span&gt;
    &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;http://auth:9999/.well-known/jwks.json&lt;/span&gt;
    &lt;span class="na"&gt;routes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;auth-v1-open-jwks&lt;/span&gt;
        &lt;span class="na"&gt;strip_path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
        &lt;span class="na"&gt;paths&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;/auth/v1/.well-known/jwks.json&lt;/span&gt;
    &lt;span class="na"&gt;plugins&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;cors&lt;/span&gt;

  &lt;span class="c1"&gt;## Secure Auth routes  &lt;/span&gt;
  &lt;span class="c1"&gt;# ...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Finally, restart all Supabase services.&lt;/p&gt;

&lt;h2&gt;
  
  
  🧐 Checking if everything worked fine
&lt;/h2&gt;

&lt;p&gt;First, navigate to &lt;code&gt;https://&amp;lt;your.supabase.url&amp;gt;/auth/v1/.well-known/jwks.json&lt;/code&gt;. You should see something similar to this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{"keys":[{"alg":"ES256","crv":"P-256","key_ops":["verify"],"kid":"...","kty":"EC","use":"sig","x":"...","y":"..."}]}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is your public key, used to verify JWTs.&lt;/p&gt;

&lt;p&gt;To check your users JWTs are being signed with the new key, you can extract a JWT from an HTTP header or a cookie, and decode it in &lt;a href="//jwt.io"&gt;jwt.io&lt;/a&gt;. The header of the JWT should look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json-doc"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"alg"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ES256"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="c1"&gt;// or RS256&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"typ"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"JWT"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"kid"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"..."&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can also check that the signature can be correctly verified by pasting the above public key on the "JWT Signature Verification" at the bottom.&lt;/p&gt;




&lt;p&gt;And that's it! I truly hope this guide was helpful to anyone struggling with self-hosted Supabase. By the way, this is my first time posting on dev.to, so I'd love to hear your thoughts. Thanks for reading! 👋&lt;/p&gt;

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