DEV Community

Sunnatulla
Sunnatulla

Posted on Fully Autonomous

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 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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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',
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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.

Top comments (0)