Audit snapshots, product attributes, and a document with two hot keys. Three cases from one Postgres project, one question for each: column or JSONB.
I answer it by looking at queries, not at data.
Audit snapshots. When an order changes, we store the whole previous state. Nobody queries inside it. It is read as a blob, whole, rarely, by a human during an incident. Perfect JSONB. Normalizing it would mean a dozen tables for data nobody joins.
Product attributes that users filter by. Started as JSONB, “because attributes are flexible”. Then came filtering by brand plus size plus color, with counts. A GIN index helps with containment, but the planner estimates JSONB predicates badly, and every query in the code is a string of ->>'...' with the key name repeated and no one checking the spelling. We pulled the three attributes people actually filter by into real columns with real indexes and real statistics. The rest stayed in JSONB.
The middle case: one or two hot keys inside a document. Before moving to columns, try an expression index, CREATE INDEX ON products ((attrs->>'brand')). Cheap, often enough. When you find yourself making the third one, that is the data telling you it wants to be a table.
Two things JSONB will not give you. Foreign keys, so any id stored inside a document is a promise nobody enforces. And cheap partial updates: Postgres rewrites the whole value, so a large document that changes often is write amplification you signed up for.
So the border for me: JSONB for data you store and read, columns for data you query and join. Write the five most important queries first. The storage form follows from them.
The “SQL vs NoSQL” argument is dead, and I do not miss it. I spent years in it, on the wrong side more than once.