<?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: ShipSafeStack</title>
    <description>The latest articles on DEV Community by ShipSafeStack (@shipsafestack).</description>
    <link>https://dev.to/shipsafestack</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%2F4228861%2F1249db63-10af-4139-80a7-0a47918f370b.png</url>
      <title>DEV Community: ShipSafeStack</title>
      <link>https://dev.to/shipsafestack</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/shipsafestack"/>
    <language>en</language>
    <item>
      <title>Is the Supabase anon key safe to expose?</title>
      <dc:creator>ShipSafeStack</dc:creator>
      <pubDate>Fri, 06 Nov 2026 15:10:01 +0000</pubDate>
      <link>https://dev.to/shipsafestack/is-the-supabase-anon-key-safe-to-expose-10ko</link>
      <guid>https://dev.to/shipsafestack/is-the-supabase-anon-key-safe-to-expose-10ko</guid>
      <description>&lt;p&gt;&lt;em&gt;Practical checklist from &lt;a href="https://shipsafestack.com" rel="noopener noreferrer"&gt;ShipSafeStack&lt;/a&gt;. Not a penetration test, compliance audit, score, or proof that a project is safe. Informational only — no guarantees.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Short answer:&lt;/strong&gt; Supabase’s docs say the anon key, and the newer publishable key, are meant to be in the browser. They are not a secret. That only holds when row level security is on and the grants are tight. The &lt;code&gt;service_role&lt;/code&gt; key and the newer secret key (&lt;code&gt;sb_secret_…&lt;/code&gt;) bypass RLS and do not belong in the client.&lt;/p&gt;

&lt;p&gt;If RLS is off, or grants are wide open, the anon key is a public handle on a public table. Setup steps: &lt;a href="https://shipsafestack.com/guides/supabase-row-level-security-for-an-ai-built-app" rel="noopener noreferrer"&gt;how to set up Supabase RLS for an AI-built app&lt;/a&gt;. After policies exist: &lt;a href="https://shipsafestack.com/guides/supabase-row-level-security" rel="noopener noreferrer"&gt;RLS checklist&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Which key is which
&lt;/h2&gt;

&lt;p&gt;Supabase’s API key docs describe four keys — two for code you ship, two that are elevated:&lt;/p&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;Format&lt;/th&gt;
&lt;th&gt;What Supabase says&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&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;Low privilege. OK in a web page or mobile app. Only reaches what RLS allows.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Anon key&lt;/td&gt;
&lt;td&gt;Long JWT&lt;/td&gt;
&lt;td&gt;Legacy publishable key. Same idea: identifies the project; not a secret.&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;Elevated. Bypasses RLS. Server / Edge Function / backend you control.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Service role key&lt;/td&gt;
&lt;td&gt;Long JWT&lt;/td&gt;
&lt;td&gt;Legacy secret key. Same bypass. Prefer a secret key where you can.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A publishable key with no signed-in user acts as the &lt;code&gt;anon&lt;/code&gt; Postgres role. With a signed-in user it acts as &lt;code&gt;authenticated&lt;/code&gt;. A secret/service_role key bypasses RLS — policies for &lt;code&gt;anon&lt;/code&gt; / &lt;code&gt;authenticated&lt;/code&gt; do not apply to it.&lt;/p&gt;

&lt;p&gt;Creating a publishable or secret key does not revoke the legacy keys until you disable them under Settings → API Keys.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to tell an anon JWT from a service_role JWT
&lt;/h2&gt;

&lt;p&gt;Both legacy keys are JWTs, so both can start with &lt;code&gt;eyJ&lt;/code&gt;. The prefix is not the difference — the payload is.&lt;/p&gt;

&lt;p&gt;A JWT is three base64url segments. The middle segment is the payload (encoded, not encrypted). Decode that segment &lt;strong&gt;on your own machine&lt;/strong&gt; and read the &lt;code&gt;role&lt;/code&gt; field:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;role&lt;/code&gt; of &lt;code&gt;anon&lt;/code&gt; → anon key (expected in the client when RLS is correct)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;role&lt;/code&gt; of &lt;code&gt;service_role&lt;/code&gt; → service role key (never in the client, &lt;code&gt;VITE_&lt;/code&gt; / &lt;code&gt;NEXT_PUBLIC_&lt;/code&gt;, or a public repo)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;sb_publishable_…&lt;/code&gt; → newer public key; &lt;code&gt;sb_secret_…&lt;/code&gt; → newer secret key&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Do not paste live keys into a random website to decode them.&lt;/p&gt;

&lt;p&gt;ShipSafeStack’s &lt;a href="https://shipsafestack.com/scan" rel="noopener noreferrer"&gt;free passive check&lt;/a&gt; reports an anon JWT in public scripts as information; it does not treat that alone as a failure. A &lt;code&gt;service_role&lt;/code&gt; JWT in public scripts is a different problem.&lt;/p&gt;

&lt;h2&gt;
  
  
  When the anon key is the wrong thing to worry about
&lt;/h2&gt;

&lt;p&gt;Worry first about:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;RLS disabled or missing on tables the client can reach&lt;/li&gt;
&lt;li&gt;Policies that only check “signed in” without scoping rows to that user&lt;/li&gt;
&lt;li&gt;Default grants that are wider than your policies&lt;/li&gt;
&lt;li&gt;A service role / secret key that also shipped in the browser, repo, or chat&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  If the service role key leaked
&lt;/h2&gt;

&lt;p&gt;Treat it as compromised. Remove it from the client first, put a new secret only on the server, rotate at Supabase, and review logs/data. Full steps: &lt;a href="https://shipsafestack.com/guides/supabase-service-role-key-exposed" rel="noopener noreferrer"&gt;Supabase service role key exposed: what to do now&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ (condensed)
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Can the anon key sit in a public GitHub repo?&lt;/strong&gt; Supabase treats it as non-secret; the risk is still your RLS and grants, plus accidentally committing the service role beside it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Both &lt;code&gt;eyJ&lt;/code&gt; keys shipped?&lt;/strong&gt; Decode payloads locally; rotate and remove the &lt;code&gt;service_role&lt;/code&gt; one immediately; keep anon only if RLS is correct.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Does enabling RLS protect tables the service role reads?&lt;/strong&gt; No — service role bypasses RLS.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Lovable / Bolt:&lt;/strong&gt; public Supabase URL + anon/publishable values often go in client env (&lt;code&gt;VITE_…&lt;/code&gt;); service role belongs in server Secrets / Edge Functions, not in the bundle.&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;strong&gt;Check a public URL (passive, free):&lt;/strong&gt; &lt;a href="https://shipsafestack.com/scan" rel="noopener noreferrer"&gt;shipsafestack.com/scan&lt;/a&gt; — ordinary GETs only; nothing stored; informational only, not an audit.&lt;/p&gt;

&lt;p&gt;Original: &lt;a href="https://shipsafestack.com/guides/is-the-supabase-anon-key-safe-to-expose" rel="noopener noreferrer"&gt;Is the Supabase anon key safe to expose?&lt;/a&gt;&lt;/p&gt;

</description>
      <category>supabase</category>
      <category>security</category>
      <category>webdev</category>
      <category>javascript</category>
    </item>
    <item>
      <title>Pre-launch security checklist for vibe-coded apps</title>
      <dc:creator>ShipSafeStack</dc:creator>
      <pubDate>Thu, 05 Nov 2026 15:20:41 +0000</pubDate>
      <link>https://dev.to/shipsafestack/pre-launch-security-checklist-for-vibe-coded-apps-331b</link>
      <guid>https://dev.to/shipsafestack/pre-launch-security-checklist-for-vibe-coded-apps-331b</guid>
      <description>&lt;h1&gt;
  
  
  Pre-launch security checklist for vibe-coded apps
&lt;/h1&gt;

&lt;p&gt;&lt;em&gt;Practical checklist from &lt;a href="https://shipsafestack.com" rel="noopener noreferrer"&gt;ShipSafeStack&lt;/a&gt;. Not a penetration test, compliance audit, score, or proof that an app is safe to launch. Informational only — no guarantees.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Run this on the project you are about to share. AI tools often repeat the same shortcuts: a key in the client, a table with no access rule, a price decided in the browser.&lt;/p&gt;

&lt;p&gt;Builders this list covers include &lt;a href="https://shipsafestack.com/tools/lovable" rel="noopener noreferrer"&gt;Lovable&lt;/a&gt;, &lt;a href="https://shipsafestack.com/tools/bolt" rel="noopener noreferrer"&gt;Bolt.new&lt;/a&gt;, &lt;a href="https://shipsafestack.com/tools/cursor" rel="noopener noreferrer"&gt;Cursor&lt;/a&gt;, &lt;a href="https://shipsafestack.com/tools/v0" rel="noopener noreferrer"&gt;v0&lt;/a&gt;, and &lt;a href="https://shipsafestack.com/tools/replit" rel="noopener noreferrer"&gt;Replit&lt;/a&gt;. Shorter per-builder lists live under &lt;a href="https://shipsafestack.com/builders" rel="noopener noreferrer"&gt;Ship guides&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Before anyone else has the URL
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Decide which URL is production and which URLs are previews. Preview links are often public. Treat a preview that has real data as production.&lt;/li&gt;
&lt;li&gt;Read the environment variable list in the host dashboard. Anything the framework ships to the browser is public (for example &lt;code&gt;VITE_&lt;/code&gt; on Lovable, &lt;code&gt;NEXT_PUBLIC_&lt;/code&gt; on Next.js).&lt;/li&gt;
&lt;li&gt;Search the repo and the built JavaScript for shapes like &lt;code&gt;sk_live&lt;/code&gt;, &lt;code&gt;service_role&lt;/code&gt;, &lt;code&gt;sb_secret_&lt;/code&gt;, &lt;code&gt;sk-&lt;/code&gt;, &lt;code&gt;AKIA&lt;/code&gt;, and private key blocks. If you find a real secret, rotate it at the vendor, then remove it. Rotating without removing it leaks the next one.&lt;/li&gt;
&lt;li&gt;Confirm the app is served over HTTPS and that you are not shipping a debug page, a stack trace, or a seed script with a known password.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What the model tends to ship
&lt;/h2&gt;

&lt;p&gt;These are patterns vendors document — not a statistic about how often they appear.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;A key next to a &lt;code&gt;fetch&lt;/code&gt; in the client&lt;/strong&gt; → confirm it is a publishable value, or move it to a server secret.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A new table with no access policy&lt;/strong&gt; → row level security (or equivalent rules) exists, and a second user fails to read the first user’s rows.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A rule that only checks “is signed in”&lt;/strong&gt; → the rule also checks which rows that person owns.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A price, plan, or “paid” flag set in the browser&lt;/strong&gt; → the server records the result from the payment provider’s signed event, not from the success page.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;An admin or service credential in client code&lt;/strong&gt; → that credential lives only in a server function.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Secrets the browser can see
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Sort publishable keys from secrets. Publishable (when the vendor says so): Stripe &lt;code&gt;pk_&lt;/code&gt;, Supabase anon/publishable with RLS on, public analytics IDs.&lt;/li&gt;
&lt;li&gt;Never ship in the client: Stripe &lt;code&gt;sk_&lt;/code&gt;, Supabase &lt;code&gt;service_role&lt;/code&gt; / &lt;code&gt;sb_secret_&lt;/code&gt;, model keys, email provider keys, database URLs with passwords, webhook signing secrets.&lt;/li&gt;
&lt;li&gt;Search the deployed site’s JavaScript (network panel → &lt;code&gt;.js&lt;/code&gt; responses), not only the source tree.&lt;/li&gt;
&lt;li&gt;Check host env screens: a client-prefixed variable is public even if the dashboard looks private.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;More detail: &lt;a href="https://shipsafestack.com/guides/api-keys-in-the-frontend" rel="noopener noreferrer"&gt;API keys leaking into the frontend&lt;/a&gt; and &lt;a href="https://shipsafestack.com/guides/keep-api-keys-out-of-ai-generated-code" rel="noopener noreferrer"&gt;How to keep API keys out of AI-generated code&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Data access
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;For Supabase: enable RLS, fix default grants, and add per-user policies before a client with the anon key talks to the database. See &lt;a href="https://shipsafestack.com/guides/supabase-row-level-security-for-an-ai-built-app" rel="noopener noreferrer"&gt;Supabase RLS for an AI-built app&lt;/a&gt; and the &lt;a href="https://shipsafestack.com/guides/supabase-row-level-security" rel="noopener noreferrer"&gt;RLS checklist&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;Prove a second account cannot read the first account’s rows.&lt;/li&gt;
&lt;li&gt;The anon key in the browser is expected when RLS is correct; a &lt;code&gt;service_role&lt;/code&gt; key in the browser is not. See &lt;a href="https://shipsafestack.com/guides/is-the-supabase-anon-key-safe-to-expose" rel="noopener noreferrer"&gt;Is the Supabase anon key safe to expose?&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Accounts, money, email, abuse
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Confirm sessions are checked on the server for protected routes.&lt;/li&gt;
&lt;li&gt;Record payments from the provider’s signed webhook, not from a browser “success” page — &lt;a href="https://shipsafestack.com/guides/stripe-webhook-verification" rel="noopener noreferrer"&gt;Stripe webhook verification checklist&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;Keep email/API secrets on the server.&lt;/li&gt;
&lt;li&gt;Put rate limits on public forms and auth routes on the server, not only by disabling a button — &lt;a href="https://shipsafestack.com/guides/rate-limiting" rel="noopener noreferrer"&gt;Rate limiting checklist&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  After you have read the code
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Re-check the production URL after deploy (headers, HTTPS, no debug pages).&lt;/li&gt;
&lt;li&gt;Rotate anything that appeared in chat transcripts, screenshots, or old preview deployments.&lt;/li&gt;
&lt;li&gt;If a Supabase service role key leaked: &lt;a href="https://shipsafestack.com/guides/supabase-service-role-key-exposed" rel="noopener noreferrer"&gt;what to do now&lt;/a&gt;. If a GitHub token leaked: &lt;a href="https://shipsafestack.com/guides/leaked-github-token" rel="noopener noreferrer"&gt;next 15 minutes&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Soft reminder
&lt;/h2&gt;

&lt;p&gt;A green scan from a builder is not the same as finishing this list. Builder scans help; they are not a substitute for checking secrets, RLS, payments, and abuse controls yourself.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Check a public URL (passive, free):&lt;/strong&gt; &lt;a href="https://shipsafestack.com/scan" rel="noopener noreferrer"&gt;shipsafestack.com/scan&lt;/a&gt; — ordinary GETs only; nothing stored; informational only, not an audit.&lt;/p&gt;

&lt;p&gt;Original: &lt;a href="https://shipsafestack.com/guides/pre-launch-security-checklist" rel="noopener noreferrer"&gt;Pre-launch security checklist for vibe-coded apps&lt;/a&gt;&lt;/p&gt;

</description>
      <category>security</category>
      <category>webdev</category>
      <category>ai</category>
      <category>devops</category>
    </item>
  </channel>
</rss>
