Read on for the technical stuff regarding limited visibility content.
The actual nuts-and-bolts of this system were actually fairly straightforward. Each chat room contains a member list, and when messages are sent in each chat room, we simply address and federate the note out directly to those users.
When posts come in, they go through a different logic path from public posts. Instead of a new reply/topic, they get added to a chat room, or if the visibility scope is narrowed (addressed to fewer people), then a new chat room is created.
There were multiple iterations in my attempts to get this to work.
Post Visibility
The first attempt involved each post maintaining its own list of recipients. Soon afterwards, it was determined that this would not scale well, and cause pagination issues similar to how NodeBB's soft-deleted posts are handled.
Topic visibility
The second attempt was to limit the visibility of posts within a topic. This made the most sense to start with as public objects as sent and received via ActivityPub are parsed as posts (and groups together into topics).
I quickly ran into database limitations because NodeBB is built on a NoSQL database abstraction. What would be straightforward (i.e. a join against a visibility table) became very very complicated as a lot of that optimized query work is not available with free-form NoSQL structures.
The reality would be that utilising the existing organizational structure (categories, topics, and posts) would end up causing so much overhead and data duplication that the fragility of the system made it an ultimate no-go.
Chats
Once I realized that we could potentially use the chats system for limited visibility, I set about building that proof-of-concept. Having built the chat system some 8ish years ago, I remembered that each user maintained their own collection of message ids per room — perfect! Each chat room's users could simply maintain their own set of messages they were allowed to view.
Alas, the chat system had actually been refactored for performance and simplicity by @baris, and this was no longer the case. Messages were more simply retrieved by timestamp from their chat room join date, which meant I needed to take an alternative approach.
Thankfully, I was able to sidestep that particular problem and altered the behaviour so that if incoming messages narrowed visibility, then a new chat room was created.