fof/byobu
Byobu is a Flarum extension that adds private discussions, letting users create and manage discussions visible only to selected users or groups. It includes permissions, recipient management, and integration with Flarum's discussion list for controlled, private conversations.
Byobu is a private discussions layer for Flarum: it adds recipients to discussions, a is_private flag on the discussion, permission gates around who can create/invite/leave, and visibility scoping so non-recipients can't see the thread through the API, search, or the discussion list.
Minimal setup:
composer require fof/byobu
php flarum migrate
php flarum cache:clear
Then enable it in Admin → Extensions → Private Discussions (Byobu) and grant the private-discussion permissions on the Groups page — by default nobody can start one. The extension's own settings page controls the softer behaviours (whether recipients may add/remove others, whether they may leave, whether they can edit their own posts, and the default "who can be invited" rules).
Read these in order before writing any code:
src/Discussion/Recipients.php — the value object you will interact with most. It wraps the discussion↔user / discussion↔group relationships and is the only sane way to read or mutate recipients.migrations/ — the pivot table backing recipients. You need to know its columns (added_by, removed_at/removed_by, timestamps) before you write any reporting or cleanup query.src/Listeners/ — the lifecycle hooks (SaveDiscussion, StartDiscussion, ShowDiscussion, API attribute injection). This is where Byobu enforces its invariants; your code should sit next to them, not in front of them.js/src/forum/ — the composer flow, the recipients modal, and the Discussion model extensions (isPrivate(), recipient relationships). That's your template for any custom UI.First use case: gate a custom feature on privacy. If you're adding a "share this thread externally" button, an analytics export, or a webhook, the first check is always the same — is this discussion private, and is the acting user actually a recipient?
use Flarum\Discussion\Discussion;
if ($discussion->is_private && ! $discussion->recipientUsers->contains($actor)) {
abort(403);
}
Treat Recipients as the API, not the pivot table. Every read/write of recipients should go through FoF\Byobu\Discussion\Recipients (obtained from the discussion). It knows about added-by, removed-at semantics, and group recipients; hand-rolled attach()/detach() calls will desync the flags Byobu's UI relies on. If you're building an invitation endpoint or a "grant access to team" action, load the discussion, get its recipients, add, save.
Creating private discussions programmatically. For an integration that opens a thread on a user's behalf (support ticket, moderation note), construct the discussion normally, set is_private before save, and attach recipients inside a Discussion\Saving or Discussion\Started listener so Byobu's own listeners see a consistent state. Don't set is_private = true with zero recipients and expect anyone but the author to see it — decide explicitly who the fallback audience is.
Query scoping. The extension registers visibility scoping on the Discussion model, so Discussion::whereVisibleTo($actor) is the correct filter and is what you should use in any custom controller or relation. For "show me only threads I'm a recipient of":
$discussions = Discussion::whereVisibleTo($actor)
->where('is_private', true)
->whereHas('recipientUsers', fn ($q) => $q->where('users.id', $actor->id))
->get();
whereHas('recipientUsers', ...) is also the join you want for list building; a raw join on the pivot table will work but will break when the schema changes and won't respect group recipients.
API surface. Byobu serializes privacy onto the discussion resource — an isPrivate attribute plus recipientUsers / recipientGroups relationships. On the frontend, extend Flarum's Discussion model (Discussion.prototype.isPrivate = function () { return this.attribute('isPrivate'); }) and read recipients the same way, rather than re-fetching from your own endpoint. If you need extra data (recipient count, "you were added by X"), inject it through an ApiController extender on the discussion show/index endpoints and keep the shape consistent with Byobu's.
Permission gating in depth. Byobu adds private-discussion permissions to the Groups page; check them with $actor->can('discussion.startPrivateDiscussion') (verify exact permission keys in the extension's extend.php / permission listener, they're namespaced like core discussion permissions). For anything that changes the audience — a "forward to moderator" action, an escalation flow — check the recipient-editing permission and whether the actor is still a recipient with rights (a user who was removed from a thread must not be able to re-add themselves).
Notifications. Byobu hooks recipient changes into Flarum's notification pipeline. If you add a new recipient-manipulating action, dispatch through the normal Discussion events so Byobu's listeners can queue the added to private discussion notification; firing raw DB inserts silently loses that.
Discussion::find($id) does not respect privacy. Any new endpoint must go through whereVisibleTo() or an explicit $actor->can('view', $discussion) check. This is the number one way people leak private content with this extension installed.is_private and "has recipients" are two different things. A discussion can be flagged private with an empty recipient set (author-only), and a recipient list can exist on a non-private discussion after a "make public" action. Always check both, and pick a deliberate fallback.removed_at / removed_by for audit and for the "you were removed" UI. Queries that filter on the relation automatically exclude removed recipients; queries against the raw table do not — you must filter removed_at IS NULL yourself.null on added_by / removed_by) or your audit views will throw on eager-loaded relations.php flarum cache:clear after changing group permissions or extension settings, or you'll debug a "permission not honoured" ghost. Test visibility with a fresh non-admin session.js/src/forum/ component class names and prop shapes are the least stable part of the package across minor releases. Hook at the Discussion model / API attribute layer if you need something durable.is_private flag on discussions. If you're importing from a forum that used core's version, backfill recipient rows explicitly; a discussion that is is_private with no Byobu recipients will render as a broken private thread.Recipients value object, Byobu's lifecycle listeners (Saving/Started), the discussion API attributes/relations, and Flarum's standard permission and notification extenders. Prefer composing around those over forking the extension — at ~59 stars and a single maintainer, upstream drift is the main long-term risk, so keep your customisations in your own extension's listeners.How can I help you explore Laravel packages today?