NewWe open-sourced 50+ Laravel packages
Custom AI apps, agents and automation — Roundly ConsultingRoundly
All packages
Messages for Laravel

Direct messages

The most common case is a single call — Messages::direct() finds the DM between two models or creates it:

use RoundlyConsulting\Messages\Facades\Messages;

$thread = Messages::direct($alice, $bob);   // find-or-create the DM
Messages::send($thread, $alice, 'Hi Bob!');

Messages::unreadCount($bob);          // 1
Messages::markRead($thread, $bob);
Messages::unreadCount($bob);          // 0

It always returns the same direct thread for the same two participants, so you never end up with duplicate DMs. It matches a direct thread whose participants are exactly those two and looks it up oldest first, so every lookup returns the same thread.

Safe under concurrent first contact

Not even when both sides make first contact at the same moment: the DM carries a unique pair key (direct_key), the database refuses the second insert, and the loser gets the winner’s thread. A DM that was deleted, or that someone left, hands the key to the fresh one.

From the model

The HasMessaging trait offers the same flow as shorthand — conversationWith() calls Messages::direct() under the hood:

$thread = $alice->conversationWith($bob); // find-or-create the DM
$alice->sendMessageTo($thread, 'Hi Bob!');

$bob->unreadCount();          // 1
$bob->markThreadRead($thread);
$bob->unreadCount();          // 0

How direct threads differ

  • Always private — is_public and everyone_can_join are forced to false, whatever the publicity config says.
  • Roleless — participants carry no role, role enforcement is skipped, and setRole() or transferOwnership() throws ParticipationException::directThreadHasNoRoles().
  • Unnamed — a direct thread is created without a name.
  • Closed — once it exists, a direct thread takes no new participant from any caller, trusted ones included, and not even someone who left: participants()->add() throws ParticipationException::directThreadIsClosed(), so nobody can bring a third person into the pair’s history. Adding someone already in the DM still returns their row; to bring someone in, start a group thread.

Querying direct threads

use RoundlyConsulting\Messages\Models\Thread;

Thread::query()->direct()->get();                 // every 1:1 thread
Thread::query()->between($alice, $bob)->first();  // the DM of exactly these two, or null

$thread->is_direct;   // true
$thread->is_public;   // always false for a direct thread
$thread->name;        // null
$thread->direct_key;  // the unique pair key — Thread::directKeyFor($alice, $bob)

A DM made with the builder — Messages::start()->direct()->withParticipants([...])->create() — gets the same pair key when it has one or two distinct participants (one is a note-to-self), so a second DM for a pair that already has one is refused by the database with a UniqueConstraintViolationException instead of becoming a duplicate. More than two participants throw ParticipationException::directThreadTakesTwo(); a builder DM with no participants stays unkeyed. To find-or-create, use Messages::direct() or conversationWith() — they also take over the key of a DM that no longer qualifies, which the builder does not.

Show your open-source love

This package is free and MIT-licensed. If it saves you time, a one-off donation or a Patreon membership keeps it maintained, tested and documented.

More ways to support, including crypto

By donating, you agree to our donation terms.

Want this built into your product?

We integrate our packages into custom Laravel and AI builds. Tell us what you're working on and we'll reply within 48 hours.