// starters/security/security-posture

security posture

Security defaults that should travel with any vibe-coded project. Secret handling, RLS philosophy, auth check placement, webhook signatures, validation parity — the seven things that account for most of the failures we see.

last fed 28 may 20261170 words · 6 min read
// when to use this starter

Fork this into a new project's CLAUDE.md or into a dedicated SECURITY.md sourdough starter. Use it whenever the AI is about to scaffold something that touches auth, secrets, database access, or external integrations.

securityrlsauthsecretswebhookssupabaseposture

Security posture

A sourdough starter for security defaults. The seven things that account for most of what bites vibe-coded apps in their first six months. Fork into your project's CLAUDE.md or keep as a separate SECURITY.md your AI re-reads on every session.


1. Secrets

Rule: No secret in any file that touches git. Ever.

  • .env.local is gitignored — use it for development.
  • Production secrets live in a secrets manager: Supabase Vault if you're on Supabase, Doppler / Infisical / AWS Secrets Manager / Vercel env vars otherwise. Not committed files.
  • Never paste a production secret into a chat with an AI. Treat the AI as you'd treat a public log: if it went in, assume it's been recorded somewhere.
  • NEXT_PUBLIC_ prefix exposes the variable to the browser. Don't put a service-role key, an admin token, or any server-only credential behind NEXT_PUBLIC_. The pattern misleads the AI; the AI follows the pattern; the key is now on the client.
  • Rotate when in doubt. If a secret might have leaked (chat, screenshot, log dump), rotate immediately. Faster than auditing whether it actually leaked.

Tool: secret_leak_finder scans 29 distinct credential patterns. Run before every deploy.


2. Row-Level Security (Supabase)

Rule: RLS enabled on every table. Every policy scoped to auth.uid().

  • Default-deny: turn RLS on, then write explicit allow policies.
  • USING (true) is not a policy. It's the absence of a policy with extra steps. Catches everyone, lets everyone through.
  • auth.role() = 'authenticated' is not the same as auth.uid() = user_id. The first lets any logged-in user through; only the second scopes to the owner. The AI confuses these.
  • WITH CHECK on INSERT / UPDATE. USING only covers SELECT-shaped reads. Without WITH CHECK, a user can update a row to reassign it to another user — the row-reassignment bug.
  • SECURITY DEFINER functions bypass RLS. Use sparingly; when you use one, guard inside the function (check auth.uid() explicitly).
  • System tables (spatial_ref_sys, pg_*) can't have RLS toggled — that's normal. Don't fight it.

Tool: rls_checker scans for the 8 canonical RLS misconfigurations. Run before every migration.


3. Auth check placement

Rule: Every protected route has a detectable auth check. Centralised if possible.

  • Middleware is the cleanest place — one auth check covers a whole path prefix (/dashboard/*, /api/admin/*). Audit surface is one file.
  • Per-route checks are fine but get audited per-route, which means missed-on-new-route is a likely bug class.
  • Routes the AI tends to leave unprotected: /admin, /api/admin, /api/internal, anything under /dashboard that was added after the middleware was written.
  • Webhooks need signature verification. Stripe (stripe.webhooks.constructEvent), Svix, custom HMAC — pick one, use it on every webhook handler. Without verification, anyone can POST to your endpoint claiming to be Stripe.

Tool: auth_flow_trace flags protected-shaped routes without detectable auth gating, plus webhooks without signature verification.


4. Input validation parity

Rule: The server validates everything the client validates, plus everything the client doesn't.

  • Client validation is UX. Server validation is security. Never mistake one for the other.
  • <input required> is a client hint, not a check. The server still has to validate.
  • Schema library on both sides (Zod / Yup / Valibot etc.) — use the same library and ideally share the schema object so they can't drift.
  • The "client validates, server trusts" bug is the most common shape: form looks right, behaves right, then someone curls the endpoint with garbage and the database accepts it.

Tool: form_validation_audit flags client/server validation mismatches.


5. CORS

Rule: No wildcard origin in production. Ever.

  • Wildcard origin (Access-Control-Allow-Origin: *) lets any site's JavaScript hit your API.
  • Wildcard origin + credentials: true — the browser blocks the request, but the intent reveals an architectural flaw. Don't ship the combination even though it doesn't "work."
  • Reflective origin echoing (returning whatever the request's Origin header was) is wildcard with extra steps.
  • Allowlist specific domains. If you need many, build the allowlist explicitly; don't write a regex that drifts.

Tool: cors_audit covers the 6 CORS patterns.


6. Webhook signature verification

Rule: Every webhook handler verifies the signature before doing anything.

  • Stripe: stripe.webhooks.constructEvent(rawBody, sig, webhookSecret). Raw body, not parsed JSON — parsing changes the bytes and the signature fails.
  • Supabase Auth webhooks: HMAC SHA-256 of the body with the webhook secret.
  • Svix / Hookdeck / similar: provider-specific verification SDK.
  • Idempotency on every handler. Webhook providers can replay, can fire out of order, can deliver duplicates. Every handler needs to be safe to receive the same event twice.

The pattern: verify, then dispatch. If verification fails, return 401 silently — don't echo why.


7. Prompt injection (if you ship anything AI-shaped)

Rule: Treat fetched content as adversarial.

  • Anything your app fetches and forwards into a prompt (email body, scraped page, tool response, user-submitted document) can contain injection payloads — instructions to ignore the system prompt, role-breakout attempts, capability exploits.
  • Severity scales with where the content came from. A user-typed message is one thing; a scraped third-party page is much worse.
  • Don't put untrusted content into a system prompt position. If you need to summarise an email, put the email inside a <email_content> tag (or similar) and instruct the model that anything inside that tag is data, not instructions.
  • The model won't always follow that instruction. Defense-in-depth: don't give the model tools it shouldn't use on untrusted content.

Tool: prompt_injection_scan covers 6 injection categories with content-type-aware severity.


How to feed this starter

Add entries when:

  • A new failure mode shows up in your project. ("Our webhook handler trusted the body before verifying — fixed by reordering, lesson: verify-then-dispatch.")
  • A new tool / library introduces its own security pattern. ("Clerk webhooks use Svix signature verification, not the Stripe pattern.")
  • The AI suggests a workaround that would have shipped a hole. ("AI suggested disabling RLS to fix a 'permission denied' error — actually fix is the WITH CHECK policy. Recorded so it doesn't suggest it again.")

Remove entries when:

  • The underlying stack moves and the gotcha is no longer real.
  • A platform fixes the default (e.g. if Supabase ever defaults RLS to on, the "always enable RLS" rule becomes redundant).

Companion tools (Fizzgig)

The _audit tools that map to each section above:

Section Tool
1. Secrets fizzgig__secret_leak_finder (29 credential patterns) + fizzgig__env_auditor (.env file structure)
2. RLS fizzgig__rls_checker (8 misconfiguration patterns)
3. Auth + webhooks fizzgig__auth_flow_trace
4. Validation fizzgig__form_validation_audit
5. CORS fizzgig__cors_audit
7. Prompt injection fizzgig__prompt_injection_scan

All of these collapse into a single fizzgig__audit MCP tool at the June 2026 Audit Suite launch — one MCP tool, all checks, one verdict.


This is a sourdough starter — fed as the project evolves and as the security landscape moves. Last meaningful update: 2026-05-28.