Dev.to Security ๐Ÿ” Cybersecurity ๐Ÿ‘ 0 ๐Ÿ“– 4 min read

I ran my Supabase + Stripe security scanner on two popular starters. It was wrong 7 times.

I've been building a small static checker for Supabase + Stripe apps, the kind people ship fast with Lovable, Bolt, Cursor or Claude Code. It's a personal project, MIT licensed: vibe-preflight. It runs as a plain Node sc

I've been building a small static checker for Supabase + Stripe apps, the kind people ship fast with Lovable, Bolt, Cursor or Claude Code. It's a personal project, MIT licensed: vibe-preflight. It runs as a plain Node script or as an Agent Skill, and it looks for six things I keep seeing in rushed apps:

  • tables in public with Row Level Security never turned on
  • policies like using (true) that let anyone read or write
  • the service_role key or a Stripe secret used in browser code
  • secrets behind NEXT_PUBLIC_ / VITE_ / EXPO_PUBLIC_
  • Stripe webhook handlers that never call constructEvent
  • .env files that git tracks

Fixtures and unit tests only tell you so much, so I pointed it at two public MIT starters to see what it does on code I didn't write.

Starter 1: vercel/nextjs-subscription-payments

vibe-preflight 1.0.0 ยท 56 source files, 3 SQL files ยท 6 checks

warning schema.sql:66
        Policy "Allow public read-only access." on `public.products` (select) lets anyone (signed in or not) read every row. [rls-permissive-policy]
warning schema.sql:99
        Policy "Allow public read-only access." on `public.prices` (select) lets anyone (signed in or not) read every row. [rls-permissive-policy]
warning supabase/migrations/20230530034630_init.sql:66
        (same policy, migration copy)
warning supabase/migrations/20230530034630_init.sql:99
        (same policy, migration copy)

0 errors, 4 warnings

That's the result I wanted. A pricing page has to show products and prices to people who aren't logged in, so a public select policy on those two tables is on purpose. That's why a read policy only gets a warning. A using (true) on insert, update or delete is an error.

Starter 2: makerkit/next-supabase-saas-kit-lite

vibe-preflight 1.0.0 ยท 291 source files, 2 SQL files ยท 6 checks

error   apps/web/.env:1
        `apps/web/.env` is committed to git. [env-file-committed]
error   apps/web/.env.development:1
        `apps/web/.env.development` is committed to git. [env-file-committed]
error   apps/web/.env.test:1
        `apps/web/.env.test` is committed to git. [env-file-committed]
error   apps/web/config/auth.config.ts:29
        `NEXT_PUBLIC_AUTH_PASSWORD` looks like a secret and is exposed to the browser. [public-env-secret]
error   packages/features/auth/src/schemas/password.schema.ts:11
        `NEXT_PUBLIC_PASSWORD_REQUIRE_SPECIAL_CHARS` looks like a secret and is exposed to the browser. [public-env-secret]
error   packages/features/auth/src/schemas/password.schema.ts:12
        `NEXT_PUBLIC_PASSWORD_REQUIRE_NUMBERS` looks like a secret and is exposed to the browser. [public-env-secret]
error   packages/features/auth/src/schemas/password.schema.ts:13
        `NEXT_PUBLIC_PASSWORD_REQUIRE_UPPERCASE` looks like a secret and is exposed to the browser. [public-env-secret]

7 errors, 0 warnings

Seven errors looks alarming. I opened every file, and all seven were false positives.

The "password" variables are on/off switches. The code reads them like this:

password: process.env.NEXT_PUBLIC_AUTH_PASSWORD === 'true',
numbers: process.env.NEXT_PUBLIC_PASSWORD_REQUIRE_NUMBERS === 'true',

My rule saw PASSWORD in the name and stopped there. It never checked how the value was used.

The committed env files hold no real secrets. .env only has NEXT_PUBLIC_* settings like the site URL, theme colour and AUTH_PASSWORD=true. Those values end up in the browser bundle anyway. .env.development and .env.test do contain a SUPABASE_SERVICE_ROLE_KEY, but it's the key the Supabase CLI prints for every local stack, pointing at 127.0.0.1. If you decode the JWT, the issuer is supabase-demo. Anyone who runs supabase start has the same key, and this starter commits it on purpose so a fresh clone runs straight away.

What I changed in 1.1.0

  1. Boolean flags are skipped. A secret-sounding NEXT_PUBLIC_ name passes when every value the project gives it is a boolean and every place the code reads it compares it to 'true' or 'false'. If one file does const mode = process.env.NEXT_PUBLIC_SECRET_MODE, it gets flagged again.
  2. Committed env files get a severity based on what's inside. If the only keys are JWTs with issuer supabase-demo and nothing looks like a real credential (sk_live_, whsec_, sb_secret_, other JWTsโ€ฆ), it's a warning that says why. Same for a file that only sets public variables. Anything else stays an error. A test puts a real-format Stripe key next to a demo key and checks that it's still an error.

Same repo after the change:

warning apps/web/.env:1
        `apps/web/.env` is committed to git. It only sets browser-exposed variables, which ship in the bundle anyway; keep it that way, or real keys added later get committed too. [env-file-committed]
warning apps/web/.env.development:1
        `apps/web/.env.development` is committed to git. Its keys are the public Supabase local-dev defaults, which is fine; make sure real project keys never go in this file. [env-file-committed]
warning apps/web/.env.test:1
        (same, for .env.test)

0 errors, 3 warnings

I kept those as warnings because the habit is still risky. Once someone pastes the production service key into .env.development "just to test something", it's in git history for good.

What I took from it

  • A security check that cries wolf gets turned off. Seven fake errors on a well-maintained starter would make me add --skip and forget about it, and then the real problem goes unnoticed too.
  • Name matching is only a starting point. The useful signal was in how the value is used (=== 'true') and in what the value is (the JWT issuer).
  • Run your tool on code you didn't write before you trust it. My own fixtures happened to avoid both cases.

It's still a static check. A clean run doesn't mean an app is secure: storage policies, auth flows and anything created only in the Supabase dashboard are out of its reach.

If you have a public Supabase + Stripe repo where it gets something wrong, I'd like to see the output.

Disclosure: vibe-preflight is my personal project. I used AI tools (Claude Code) to help write the code and this post, and I checked the scanner output and the files above myself.

๐Ÿ“ฐ Read the original article on Dev.to Security

Originally published by Dev.to Security. Aggregated on AIWithGhost for educational purposes โ€” full credit and traffic to the original publisher.