--- description: Security guardrails for AI-built web apps (Supabase RLS and grants, secret keys, provider keys, redirects, storage, input validation, dependencies). Always applied. alwaysApply: true --- # Security guardrails These rules guide the agent; they are not an enforcement layer. Review every change yourself. ## Always - Show proposed database migrations and code diffs before applying them. Wait for explicit approval for anything that changes the schema, policies, grants, auth settings, storage policies, environment variables or dependencies. - Say when you're unsure. Never invent package names, dashboard menu labels or API options. ## Database (Supabase / Postgres) - Never disable Row Level Security on any table (including `storage.objects`), not even temporarily or to fix an error. - When creating a table in `public`, put these in the same migration, in this order: `GRANT` only the privileges the app uses, `ALTER TABLE ... ENABLE ROW LEVEL SECURITY`, then policies. - Don't grant `anon` access unless the data is meant to be public. - INSERT policies check ownership in `WITH CHECK` (INSERT takes no `USING`). UPDATE policies on owned rows use `USING` and `WITH CHECK` both set to `(select auth.uid()) = user_id`, so `user_id` can't be changed to someone else's. Add a different `WITH CHECK` only when the new-row rule must differ from `USING`. - Never use `USING (true)` or `WITH CHECK (true)` on private data. `WITH CHECK (true)` loosens the new-row check. - Don't base authorization on `user_metadata` / `raw_user_meta_data`; users can edit it. - Create views with `security_invoker = true` so RLS applies to them. - Treat `42501 permission denied` as a missing grant. Fix it with a narrow `GRANT`, never by disabling RLS or switching to a secret key. ## Keys and secrets - Never use a Supabase secret key (`sb_secret_...`) or legacy `service_role` key in client-side code. - Never put secrets in browser-exposed env vars (`VITE_`, `NEXT_PUBLIC_` or similar). Only the Supabase URL and publishable key may be public. - Call AI and payment providers only from server code. Read each key from one server-only env var so rotating it is a config change. - Never hard-code keys, and never print them in logs or error messages. Keep `.env` files out of git. ## Auth and redirects - Accept only same-site relative paths for `next` / `redirect` / `returnTo` parameters, or parse the URL and check scheme, host and port against an allow-list. Never redirect to an unchecked user-supplied URL. - Enforce sign-in and roles on the server or with RLS. Hiding UI is not access control. Never trust a role or user ID sent by the client. ## Storage - Keep user files in private buckets and serve them with short-lived signed URLs. - Restrict `storage.objects` policies to the user's own folder. Set file size and type limits on buckets. ## Input and abuse - Validate all input on the server with a schema (allow-list what's accepted), with length and size limits. - Add rate limits or per-user caps to endpoints that cost money or send messages: AI calls, email/SMS, sign-up, password reset, uploads, exports. ## Dependencies - Before adding a package, confirm it exists in the official registry and is the one intended (check the name, maintainer and repository). Flag look-alike names. - Keep the lockfile committed. Use `npm ci` in CI. - Don't approve dependency install scripts or use `--dangerously-allow-all-scripts` without explaining why. ## References - BuiltSafely guides: /guides/supabase-rls/, /guides/supabase-oct30-grants/, /fixes/secret-key-in-frontend-bundle/, /fixes/ai-provider-key-leaked/ - Supabase RLS: https://supabase.com/docs/guides/database/postgres/row-level-security - Supabase API keys: https://supabase.com/docs/guides/api/api-keys - Postgres CREATE POLICY (USING vs WITH CHECK): https://www.postgresql.org/docs/current/sql-createpolicy.html