I wanted dashboards over my app's production data without giving a BI tool any way to change that data. Metabase plus a carefully-scoped database role does exactly that.

The principle

Never point BI at your app's main database user. Create a dedicated role that can read and nothing else:

CREATE ROLE metabase_ro LOGIN PASSWORD '********';
GRANT USAGE ON SCHEMA public TO metabase_ro;
GRANT SELECT ON ALL TABLES IN SCHEMA public TO metabase_ro;
ALTER DEFAULT PRIVILEGES IN SCHEMA public
  GRANT SELECT ON TABLES TO metabase_ro;

The row-level-security wrinkle

My tables have row-level security policies written for the app's auth context, not a reporting role — so a plain read-only role would see almost nothing. Granting the role BYPASSRLS lets it read every row for analytics while still being unable to write. A deliberate trade: the BI role sees all data, but physically cannot mutate it.

Connecting

Metabase runs in its own container with its own metadata database and connects out as metabase_ro. Dashboards build against real data; a compromised Metabase can, at worst, read.