trinity/messages
Trinity Messages is a lightweight messaging layer for Laravel applications, providing models, relationships, and helpers for conversations, threads, and user-to-user messaging so you can add inbox-style features without building the basics from scratch.
Treat this as a small, unmaintained building block, not a batteries-included inbox. Assume Laravel 5.5-era code, no auto-discovery, and no Packagist presence — plan to pin it via a VCS repository until you've forked it.
composer config repositories.trinity-messages vcs https://github.com/modpreneur/trinity-messages
composer require trinity/messages:dev-master
Then wire it up manually:
config/app.php under providers (auto-discovery almost certainly isn't present at this age):
Trinity\Messages\MessagesServiceProvider::class,
php artisan vendor:publish --provider="Trinity\Messages\MessagesServiceProvider"
ls database/migrations | grep -i message
cat config/messages.php # if present — model/table overrides usually live here
messages table plus a conversations/threads table and a participants pivot.User:
use Trinity\Messages\Traits\HasMessages;
class User extends Authenticatable
{
use HasMessages;
}
Verify the exact trait/namespace with grep -rn "trait " vendor/trinity/messages/src — at this vintage, naming is often inconsistent across README and code.First use case: one-to-one messaging between two users, rendered as a thread page.
$conversation = $sender->conversations()->firstOrCreateWith($recipient);
$conversation->messages()->create([
'user_id' => $sender->id,
'body' => $request->input('body'),
]);
If the package doesn't ship a firstOrCreateWith-style helper, write that yourself in a thin service class immediately (see below) rather than scattering Conversation::create() + attach() in controllers.
Wrap the package in one application-level service. The single most valuable thing you can do with an abandoned package is reduce your call sites to a handful of methods:
class Messenger
{
public function send(User $from, Conversation $to, string $body): Message
{
$message = $to->messages()->create(['user_id' => $from->id, 'body' => $body]);
$to->participants()->syncWithoutDetaching([$from->id]);
$to->touch(); // keep threads sorted by last activity
event(new MessageSent($message));
return $message;
}
}
Thread list with unread counts. Don't loop and query per conversation — eager load and aggregate:
$conversations = $user->conversations()
->with(['participants:id,name', 'latestMessage'])
->withCount(['messages as unread_count' => function ($q) use ($user) {
$q->where('user_id', '!=', $user->id)
->whereNull('read_at')
->where('created_at', '>', $user->pivot->last_read_at ?? '1970-01-01');
}])
->latest('updated_at')
->paginate(25);
Read tracking belongs on the pivot, not on the message. A read_at on messages is one-recipient-only; the moment a thread has three people it lies. Add last_read_at to the participants pivot in your own migration and mark threads read on open.
Authorization as a policy, always. The package will happily return any conversation by ID. Gate every route:
public function view(User $user, Conversation $conversation): bool
{
return $conversation->participants()->whereKey($user->id)->exists();
}
API surface. Return a ConversationResource / MessageResource rather than raw Eloquent models — the package's serialization predates API Resources and will leak user_id, pivot columns, and timestamps you don't want.
Events for the inbox UI. Emit a domain event in your wrapper, then fan out to broadcasting (PrivateChannel("user.{$id}")) and to your notification system. Keep the package unaware of both.
ReturnTypeWillChange, missing $dates, and possibly Illuminate\Support\Str API drift. Fork it, bump composer.json, and add return types before adopting.packages/ with a path repository so you control it:
"repositories": [{"type": "path", "url": "packages/trinity-messages"}]
users table. Rename them with 2017_01_01_000001_-style padding, or the FK creation fails on a fresh migrate.$conversation->messages returns everything. Always chain ->paginate() / ->latest()->limit()—long threads will OOM your worker.id ordering if the package uses UUIDs; sort explicitly on created_at, id.conversation_user.deleted_at pivot; deleting the row nukes it for everyone. Build this before launch, not after.DB::listen() on the thread page is faster than reading the source. If the thread list fires 50 queries, the latestMessage relation is the usual culprit — add a proper hasOne ... ofMany or a subquery select.config/messages.php for model bindings — swapping Trinity\Messages\Models\Message for your own subclass (adding casts, scopes) is usually cleaner than forking models.RefreshDatabase and assert your wrapper's contract, not the package internals. When (not if) you replace the package, only the Messenger tests should need to change.user_id from request input, and that conversation_id from the URL is always policy-checked — IDOR is the classic failure mode in hand-rolled messaging layers.How can I help you explore Laravel packages today?