Strong post — RLS is the right answer for exactly the reason you give. Three production gotchas worth adding: (1) RLS doesn't apply to table owners or superusers, so the app role must not own the tables, and watch out for BYPASSRLS; (2) the tenant context (e.g. SET app.current_tenant) must be reset per checkout when you're behind PgBouncer — a leaked tenant setting is a data leak; (3) policy expressions run on every row, so keep them sargable and index-friendly on large tables or RLS becomes a performance tax. Miss any of the three and the enforcement story quietly breaks.
Good to see a write up that covers the parts that usually bite later, such as connection pooling and setting the tenant context per transaction rather than per session.
One practice I would add: make cross tenant access tests part of the definition of done for every feature that touches tenant data. A small suite that logs in as tenant A and tries to read, update and export tenant B's records catches the cases where a new view or a background job quietly bypasses the policy. Those tests are cheap to write and they turn row level security from a configuration you trust into a property you verify on each deploy.
It also makes security reviews with customers much easier, because you can show the tests rather than describe the intent. Do you run background jobs under a separate role with its own policies, or do they set the tenant context the same way requests do?