Postgres Security Checklist for App Developers (2026)
Postgres security for an app comes down to a short list: encrypt every connection, give each client the least privilege it needs, turn on Row Level Security for anything reachable through an API, lock down functions and views that can bypass it, keep secrets out of client code, and watch for mistakes. Most real breaches of app databases are not clever exploits. They are a table left open, a key shipped in a mobile app, or a view that skips the rules.
Use this checklist when you start a project and again before launch. Each item has the check and the fix.
#1. Connections
Require TLS on every connection. Unencrypted Postgres traffic exposes passwords and data on the network.
- Check: connect with
sslmode=disable; it should fail. - Fix (self-hosted):
hostssllines only inpg_hba.conf,ssl = on.
On StitchDB, Postgres connections must use TLS 1.2 or 1.3 and unencrypted connections are refused. A client without TLS gets FATAL: no authentication method is found.
Do not expose Postgres to the whole internet if you can avoid it.
- Fix: firewall or allow-list your app servers' IPs. On StitchDB, Postgres itself is never exposed directly; connections go through the pooler, and from the Pro plan you can add network restrictions with up to 50 IPv4 addresses or CIDR ranges.
Use SCRAM, not MD5, for passwords. password_encryption = scram-sha-256 is the default in modern Postgres. Check old roles that still have MD5 hashes.
Use long, random database passwords and store them in a secret manager, not in the repository.
#2. Roles and privileges
Do not run the app as a superuser. Superusers bypass every permission and every RLS policy.
Give each client its own role. A reporting tool, a background worker and the web app should not share credentials. If one leaks, you revoke one.
create role reporting login password '...';
grant usage on schema public to reporting;
grant select on public.orders, public.customers to reporting;On StitchDB, create roles under Database › Roles: each gets its own password, read-only or read-write rights on the schemas you pick, its own connection limit and an optional expiry date. See Schemas and roles.
Revoke what you did not mean to grant. Older Postgres versions grant create on public to everyone:
revoke create on schema public from public;Set expiry on temporary access. alter role contractor valid until '2026-12-31';
#3. Row Level Security
If browsers or mobile apps reach your database through an API (PostgREST, StitchDB, Supabase, Hasura), RLS is your main defense.
Enable RLS on every table in exposed schemas.
select schemaname, tablename
from pg_tables
where schemaname = 'public' and not rowsecurity;Every row returned is a table anyone with the public key may be able to read and change. Fix:
alter table public.invoices enable row level security;On StitchDB, RLS Guard turns RLS on for every new table in public, however it is created, and is on by default for new projects. Studio shows a warning for any public table without RLS.
Write with check on insert and update policies, so users cannot write rows they could not read.
Name the role in every policy (to authenticated, to anon). A policy without to applies to everyone.
Never authorize on user-editable claims. On backends of the Supabase-style stack, user_metadata is editable by the user; use app_metadata or a table lookup.
Index the columns your policies filter on, so security does not become a performance problem.
For depth, see Postgres Row Level Security: A Practical Guide.
#4. Views, functions and other bypasses
RLS protects tables. These can route around it.
Views run as their owner by default. On Postgres 15+, make them run as the caller:
alter view public.order_totals set (security_invoker = on);Materialized views have no RLS. Do not expose them through an API if they contain per-user data.
security definer functions run as their owner. Review every one that is callable by anon or authenticated. Pin the search path so a malicious object in another schema cannot be picked up:
alter function public.count_open_todos() set search_path = '';Revoke execute where it is not needed. Functions are executable by public by default:
revoke execute on function public.admin_reset(uuid) from public, anon, authenticated;StitchDB's Advisors flag views that bypass RLS, materialized views in the API, functions with a mutable search_path, anonymous write policies and columns with names like password, token or secret that the API can read. The Exposure check runs a live test as an anonymous visitor and a random signed-in user.
#5. API keys and secrets
Know which key goes where.
| Key | Where it can live |
|---|---|
| Public (anon) key | Browsers and mobile apps; safe only with RLS |
| Service role key | Servers only; bypasses RLS |
| JWT secret | Servers that verify tokens |
| Database password | Servers, secret managers |
Never put the service role key, JWT secret or database password in client code or a public repository. Search your repo and your built JavaScript bundles for them.
Store third-party secrets encrypted, not in plain columns. If SQL needs an API key (for example to call a webhook), use a vault. On StitchDB, Vault encrypts secrets with AES-256 using a per-project key that is not readable from SQL. Never grant vault.decrypted_secrets to anon or authenticated.
Rotate after any leak. Plan how you would rotate each key before you need to.
#6. Authentication settings
If your backend includes Auth:
Keep access tokens short-lived. One hour is a common default. Signing out revokes refresh tokens, but issued access tokens stay valid until they expire.
Rotate refresh tokens on each use.
Offer MFA and require it for sensitive tables with a restrictive policy:
SQLcreate policy "MFA required" on public.payouts as restrictive for all to authenticated using ((auth.jwt() ->> 'aal') = 'aal2');Rate-limit sign-ins and e-mails. StitchDB applies per-plan limits and lets you lower them; see Auth limits.
Restrict redirect URLs to your own domains to prevent open redirects in magic links and OAuth.
More in JWT Authentication Explained.
#7. Query safety
Use parameterized queries everywhere. Never build SQL with string concatenation from user input.
// good
await client.query('select * from orders where id = $1', [id])
// bad
await client.query(`select * from orders where id = ${id}`)Set statement timeouts so a heavy query cannot hold resources forever:
alter role web_app set statement_timeout = '15s';Managed platforms often set one for API requests. StitchDB's API query timeout is 8 seconds on Free, 30 on Pro and 60 on Scale and Enterprise.
Limit result sizes in APIs. Unbounded select * on a large table is both a performance and a data-exposure problem.
#8. Extensions
Install only what you use. Each extension adds code running inside the database.
select extname, extversion from pg_extension order by 1;Be careful with extensions that reach the network or file system (dblink, postgres_fdw, pg_net, untrusted languages). On StitchDB, extensions that could cross the project boundary are blocked, and HTTP requests from SQL cannot reach private or internal addresses.
#9. Monitoring and audit
- Log connections and failed logins if you self-host (
log_connections,log_disconnections). - Keep an audit trail of schema and permission changes. Migrations in version control are the easiest form. StitchDB also keeps an audit log of team actions.
- Watch for unusual reads: a sudden spike in rows returned from a sensitive table is often the first sign of a leak.
- Protect dashboard accounts with two-factor authentication. StitchDB lets organization owners require it for every member.
#10. Backups and recovery
Security includes being able to recover. Ransomware and credential theft often target backups too.
- Point-in-time recovery for human error.
- An independent, encrypted copy outside the main account.
- Restore drills.
See Postgres Backup Best Practices.
#11. Keep Postgres updated
Apply minor releases promptly; they contain security fixes. Plan major upgrades before your version reaches end of life. Managed hosts handle minor updates for you; check their policy for major versions.
#The short version of the Postgres security checklist
- TLS required, network restricted.
- One role per client, least privilege, no superuser for apps.
- RLS on every exposed table, tested.
- Views as
security_invoker,security definerfunctions reviewed, search path pinned. - Secrets on servers and in a vault, never in clients.
- Short tokens, MFA for sensitive data, rate limits.
- Parameterized queries and timeouts.
- Minimal extensions.
- Audit logs, 2FA on the dashboard, alerts.
- Tested backups.
#FAQ
#Is the anon key a secret?
No. It is designed to be public. It is safe only because RLS restricts what it can do, so every exposed table needs policies.
#Do I need RLS if only my server talks to the database?
It is not strictly required, but it is a strong second line of defense in multi-tenant apps.
#How do I find tables without RLS?
Query pg_tables where rowsecurity is false, or use your platform's advisor. On StitchDB, Studio shows them on the Advisors page.
#What is the most common Postgres security mistake in apps?
A table reachable through an API with RLS off or with a policy that allows everyone.
Read how StitchDB isolates projects on the security page, or create a project with RLS Guard on by default.