Supabase Core Principles 1. Supabase changes frequently — verify against current docs before implementing. Do not rely on training data for Supabase features. Function signatures, config.toml settings, and API conventions change between versions. Before implementing, look up the relevant topic using the documentation access methods below. 2. Verify your work. After implementing any fix, run a test query to confirm the change works. A fix without verification is incomplete. 3. Recover from errors, don't loop. If an approach fails after 2-3 attempts, stop and reconsider. Try a different method, check documentation, inspect the error more carefully, and review relevant logs when available. Supabase issues are not always solved by retrying the same command, and the answer is not always in the logs, but logs are often worth checking before proceeding. 4. RLS by default in exposed schemas. Enable RLS on every table in any exposed schema, especially public . This is critical in Supabase because tables in exposed schemas can be reachable through the Data API. For private schemas, prefer RLS as defense in depth. After enabling RLS, create policies that match the actual access model rather than defaulting every table to the same auth.uid() pattern. 5. Security checklist. When working on any Supabase task that touches auth, RLS, views, storage, or user data, run through this checklist. These are Supabase-specific security traps that silently create vulnerabilities: Auth and session security Never use user_metadata claims in JWT-based authorization decisions. In Supabase, raw_user_meta_data is user-editable and can appear in auth.jwt() , so it is unsafe for RLS policies or any other authorization logic. Store authorization data in raw_app_meta_data / app_metadata instead. Deleting a user does not invalidate existing access tokens. Sign out or revoke sessions first, keep JWT expiry short for sensitive apps, and for strict guarantees validate session_id against auth.sessions on sensitive operations. If you use app_metadata or auth.jwt() for authorization, remember JWT claims are not always fresh until the user's token is refreshed. API key and client exposure Never expose the service_role or secret key in public clients. Prefer publishable keys for frontend code. Legacy anon keys are only for compatibility. In Next.js, any NEXT_PUBLIC_ env var is sent to the browser. RLS, views, and privileged database code Views bypass RLS by default. In Postgres 15 and above, use CREATE VIEW ... WITH (security_invoker = true) . In older versions of Postgres, protect your views by revoking access from the anon and authenticated roles, or by putting them in an unexposed schema. UPDATE requires a SELECT policy. In Postgres RLS, an UPDATE needs to first SELECT the row. Without a SELECT policy, updates silently return 0 rows — no error, just no change. Do not put security definer functions in an exposed schema. Keep them in a private or otherwise unexposed schema. Storage access control Storage upsert requires INSERT + SELECT + UPDATE. Granting only INSERT allows new uploads but file replacement (upsert) silently fails. You need all three. For any security concern not covered above, fetch the Supabase product security index: https://supabase.com/docs/guides/security/product-security.md Supabase CLI Always discover commands via --help — never guess. The CLI structure changes between versions. supabase --help
All top-level commands
supabase < group
--help
Subcommands (e.g., supabase db --help)
supabase < group
< command
--help
Flags for a specific command
Supabase CLI Known gotchas:
supabase db query
requires
CLI v2.79.0+
→ use MCP
execute_sql
or
psql
as fallback
supabase db advisors
requires
CLI v2.81.3+
→ use MCP
get_advisors
as fallback
When you need a new migration SQL file,
always
create it with
supabase migration new