Think in four layers, because each one covers a different failure.
1. Limit what a query can cost you. Set a statement timeout on every role so a runaway or crafted query gets killed automatically instead of pinning your CPU. Add indexes on the columns you filter, sort, group and join by - that is a security control, not just a performance one, because it means even a hostile query resolves fast instead of tying up connections. And size your connection pool with a max and an acquire timeout, so requests fail fast rather than queueing forever when connections run out. Detail here.
2. Limit what the connection can do. A read-only role for anything serving reads, and least-privilege table grants rather than blanket access. New tables inherit default grants that are usually too permissive, so re-run your grant audit after every schema change.
3. Limit what a row can expose. Row-level security on every table, especially if a platform like Supabase publishes a REST API over your database with a key that lives in frontend code. But enabling RLS is not the same as being safe - the policy shape matters enormously.
4. Audit the thing that bypasses all of the above. SECURITY DEFINER functions run with the owner's privileges and ignore RLS entirely. One of them accepting a user-supplied identity is complete identity spoofing that no policy can stop.
Full item-by-item checklist with SQL: https://www.tigzig.com/security/database. For a database exposed to an AI agent specifically: https://www.tigzig.com/agents-faq/how-to-secure-a-database-for-ai-agents-and-mcp. DuckDB is a different threat model entirely - see here.
← All Agents FAQ