master branch - code currently deployed to production. never used as the base branch except rare hot fixes
staging branch - the base branch for all feature branches. On prod deploy, staging is merged to master with a merge commit (probably could (should?) be a rebase)
feature branches - always use staging as base branch
Most critically: all feature branches are squashed and merged, so that each single commit in master corresponds to a single PR.
Makes it easy to revert PR but difficult to cherry pick after squashing. Also keeps the hit history extremely clean and condensed. Not sure if this method will scale, but it’s working well at our company with 6 engineers with 20 or so feature branches open at any given time.
Edit: one reason this works for us is we keep feature branches short-lived (ideally at most 2-3 weeks) and staging gets merged to master twice a week (we do a deploy Mondays and Thursdays)