← ShipReady home · this is a real scan of the planted fixture, not a mockup

ShipReady audit

Production readiness report

/workspace/shipready/fixtures/broken-app
Scanned 2026-08-25T01:13:56Z · scanner v1.0.0 · 16 files

17 P0 — ship-blockers
3 P1 — will break in prod
1 P2 — hygiene
31.5 hours, unique issue classes

Recommended next step: 72h Production Pass $497

P0 issues mean the app can leak data or take fake payments. A Production Pass is the shortest path to a deploy you can actually charge for.

P0 rls

RLS policy uses USING (true) — every row is readable

supabase/migrations/20240801000000_init.sql:21 · ~4.0h to fix

-- PLANTED ISSUE: USING (true) makes RLS a no-op

Why it matters. ENABLE ROW LEVEL SECURITY with USING (true) is theatre. The anon key can SELECT/UPDATE/DELETE everything. AI generators do this to 'make the demo work'.

Fix. Replace USING (true) with USING (auth.uid() = user_id) (or the real owner column). Split SELECT/INSERT/UPDATE/DELETE policies. Test with the anon key, not the service role.

P0 secrets

Secrets file committed to the repo

.env:3 · ~3.0h to fix

VITE_SUPABASE_ANON_KEY=FAKE_eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.FAKE_payload

Why it matters. Anyone with the repo (or a leaked GitHub Action log) gets live keys. Lovable/Bolt/v0 projects often commit .env because the generator treats it as source.

Fix. Rotate every key in this file. Add .env to .gitignore. Move secrets to Vercel/host env vars. Keep only VITE_/NEXT_PUBLIC_ values that are meant for the browser in a checked-in example file, with fake placeholders.

P0 secrets

Secrets file committed to the repo

.env.local:2 · ~3.0h to fix

DATABASE_URL=postgres://postgres:FAKE_password@db.fakeproject.supabase.co:5432/postgres

Why it matters. Anyone with the repo (or a leaked GitHub Action log) gets live keys. Lovable/Bolt/v0 projects often commit .env because the generator treats it as source.

Fix. Rotate every key in this file. Add .env to .gitignore. Move secrets to Vercel/host env vars. Keep only VITE_/NEXT_PUBLIC_ values that are meant for the browser in a checked-in example file, with fake placeholders.

P0 secrets

window.env assignment looks like it dumps secrets into the page

public/window-env.js:1 · ~3.0h to fix

// PLANTED: another window.env dump

Why it matters. Anything on window is readable from DevTools.

Fix. Delete window.env. Use server env and a narrow public config object with anon/publishable keys only.

P0 secrets

Stripe secret key referenced in a client file

public/window-env.js:3 · ~2.0h to fix

STRIPE_SECRET_KEY: "sk_test_FAKE_from_public_folder",

Why it matters. sk_live / STRIPE_SECRET in the browser lets anyone issue refunds or create payouts as you.

Fix. Keep only the publishable key (pk_) in Vite/Next public env. Call Stripe from a server route. Rotate the secret if this file ever shipped.

P0 secrets

Private API key or secret referenced in a client file

public/window-env.js:4 · ~3.0h to fix

OPENAI_API_KEY: "sk-FAKE_openai_from_public",

Why it matters. Client bundles are downloadable. A secret in src/ is a secret on the internet.

Fix. Move the key to a server-only env var. Proxy the call through /api. Rotate the leaked key.

P0 secrets

Supabase service_role (or lookalike) in a client file

src/lib/config.js:3 · ~3.0h to fix

VITE_SUPABASE_SERVICE_ROLE: import.meta.env.VITE_SUPABASE_SERVICE_ROLE,

Why it matters. service_role bypasses Row Level Security. In a browser bundle it is public. This is the #1 way AI-generated Supabase apps leak every row.

Fix. Delete the key from any file under src/, app/, public/, or components/. Use the anon key plus RLS on the client. Put service_role only in a server route, Edge Function, or GitHub Action secret.

P0 secrets

VITE_/NEXT_PUBLIC_ variable that looks like a private secret

src/lib/config.js:3 · ~3.0h to fix

VITE_SUPABASE_SERVICE_ROLE: import.meta.env.VITE_SUPABASE_SERVICE_ROLE,

Why it matters. Vite inlines every VITE_ value into the browser bundle. Prefixing a service role or secret key with VITE_ does not hide it.

Fix. Rename and un-prefix. Only publishable keys (anon, pk_) belong in VITE_/NEXT_PUBLIC_. Everything else lives on the server.

P0 secrets

Stripe secret key referenced in a client file

src/lib/config.js:4 · ~2.0h to fix

VITE_STRIPE_SECRET_KEY: import.meta.env.VITE_STRIPE_SECRET_KEY,

Why it matters. sk_live / STRIPE_SECRET in the browser lets anyone issue refunds or create payouts as you.

Fix. Keep only the publishable key (pk_) in Vite/Next public env. Call Stripe from a server route. Rotate the secret if this file ever shipped.

P0 secrets

Private API key or secret referenced in a client file

src/lib/config.js:5 · ~3.0h to fix

OPENAI_API_KEY: import.meta.env.VITE_OPENAI_API_KEY,

Why it matters. Client bundles are downloadable. A secret in src/ is a secret on the internet.

Fix. Move the key to a server-only env var. Proxy the call through /api. Rotate the leaked key.

P0 secrets

Stripe secret key referenced in a client file

src/lib/stripe.js:2 · ~2.0h to fix

const STRIPE_SECRET_KEY = "sk_test_FAKE_51ClientSideSecret";

Why it matters. sk_live / STRIPE_SECRET in the browser lets anyone issue refunds or create payouts as you.

Fix. Keep only the publishable key (pk_) in Vite/Next public env. Call Stripe from a server route. Rotate the secret if this file ever shipped.

P0 secrets

Supabase service_role (or lookalike) in a client file

src/lib/supabase.js:3 · ~3.0h to fix

// PLANTED ISSUE: service_role used from the browser. FAKE key.

Why it matters. service_role bypasses Row Level Security. In a browser bundle it is public. This is the #1 way AI-generated Supabase apps leak every row.

Fix. Delete the key from any file under src/, app/, public/, or components/. Use the anon key plus RLS on the client. Put service_role only in a server route, Edge Function, or GitHub Action secret.

P0 secrets

Literal key material in source

src/lib/supabase.js:4 · ~3.0h to fix

const SUPABASE_SERVICE_ROLE = "FAKE_service_role_eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.FAKE_sr_payload";

Why it matters. Hard-coded sk_ / JWT / AWS key material survives into git history even after you move to env vars.

Fix. Rotate the key. Replace the literal with an env lookup. Purge git history if this repo is public.

P0 secrets

window.env assignment looks like it dumps secrets into the page

src/lib/supabase.js:11 · ~3.0h to fix

// PLANTED ISSUE: window.env dumps a private-looking secret into the page

Why it matters. Anything on window is readable from DevTools.

Fix. Delete window.env. Use server env and a narrow public config object with anon/publishable keys only.

P0 secrets

Private API key or secret referenced in a client file

src/lib/supabase.js:13 · ~3.0h to fix

OPENAI_API_KEY: import.meta.env.VITE_OPENAI_API_KEY,

Why it matters. Client bundles are downloadable. A secret in src/ is a secret on the internet.

Fix. Move the key to a server-only env var. Proxy the call through /api. Rotate the leaked key.

P0 secrets

VITE_/NEXT_PUBLIC_ variable that looks like a private secret

src/lib/supabase.js:13 · ~3.0h to fix

OPENAI_API_KEY: import.meta.env.VITE_OPENAI_API_KEY,

Why it matters. Vite inlines every VITE_ value into the browser bundle. Prefixing a service role or secret key with VITE_ does not hide it.

Fix. Rename and un-prefix. Only publishable keys (anon, pk_) belong in VITE_/NEXT_PUBLIC_. Everything else lives on the server.

P0 stripe

Stripe webhook handler does not verify the signature

api/webhook.js:1 · ~2.5h to fix

// Intentionally broken Stripe webhook: trusts req.body, no constructEvent.

Why it matters. Without stripe.webhooks.constructEvent, anyone can POST a fake invoice.paid and mark themselves as paid. Lovable/Bolt apps often fetch('/api/webhook') from the client and trust the body.

Fix. Read the raw request body. Call stripe.webhooks.constructEvent(rawBody, sig, process.env.STRIPE_WEBHOOK_SECRET). Return 400 on failure. Never fetch the webhook from the browser.

P1 oauth

Auth site URL / redirect is localhost

.env:10 · ~1.0h to fix

SITE_URL=http://localhost:5173

Why it matters. Production auth will bounce users back to your laptop.

Fix. Set SITE_URL and redirect allow-list to the production domain in the host's env, not in source.

P1 oauth

OAuth redirect URI still points at localhost

src/lib/auth.js:4 · ~1.5h to fix

// PLANTED ISSUE: OAuth redirect stays on localhost

Why it matters. Google/GitHub/Supabase will refuse (or silently bounce) production logins if the only registered redirect is http://localhost:5173. This is the default the generator writes.

Fix. Add https://YOURDOMAIN/auth/callback (and the Vercel URL) to the provider and to Supabase Auth redirect URLs. Keep localhost for dev only. Never commit a production client secret next to it.

P1 stripe

Client code calls the Stripe webhook URL

src/lib/stripe.js:6 · ~2.0h to fix

await fetch("/api/webhook", {

Why it matters. Webhooks are for Stripe's servers, not your React app. A client fetch skips signature checks and can be replayed.

Fix. Handle success in the checkout.session.completed webhook on the server. On the client, poll a server route that reads the order row.

P2 observability

No Sentry (or other error tracker) found

(project):0 · ~1.5h to fix

No match for Sentry.init, @sentry/*, or sentry.io DSN in scanned files.

Why it matters. Vercel white screens and silent webhook failures are invisible without a client + server tracer. You will hear about them from Stripe or from a user, not from a log.

Fix. Add @sentry/browser (or @sentry/nextjs) with a DSN in server env. Capture unhandled rejections. Set tracesSampleRate low (0.1) until traffic is real.