Separate schemas, not a shared table with a filter
Each tenant's records live in their own Postgres schema, and a bot connects with its search_path scoped to that schema. The isolation is a property of the connection rather than of every query being written correctly. A query that forgets a tenant filter finds nothing instead of finding somebody else's row.
