sevenspan/laravel-chat
sevenspan/laravel-chat is a Laravel package for adding user-to-user chat to your application. It includes models, migrations, and controllers for conversations and messages, helping you integrate private messaging quickly with minimal setup.
The package is small enough that setup is quick, but also young enough (18 stars, no dependents) that you should treat it as a starting point rather than a finished product.
composer require sevenspan/laravel-chat, then publish the config and migrations (php artisan vendor:publish --tag=laravel-chat-config and the migrations tag — check config/chat.php after publishing to confirm the exact tag names). Run php artisan migrate.User model. The package ships a trait (typically HasChats or similar) that gives your model chats(), messages(), and helper methods for creating and enumerating conversations. Add it to App\Models\User before doing anything else — most of the public surface hangs off this trait.config/chat.php (local vs s3). The S3 path expects FILESYSTEM_DISK=s3 plus the usual AWS_* envs; if you're not on S3, leave it as public and make sure php artisan storage:link has been run.src/Models, src/Traits, and src/Contracts — that's where the actual behavior lives. The README is a rough starting point, not a reference.unreadCount() reflects correctly. Once that loop works end-to-end, the rest of the package's surface is variations on the same theme.Chat creation — keep it behind a service. Both one-to-one and group chats are usually created through a dedicated method on the trait. Wrap that in a ChatService so you can enforce "don't create duplicate DMs between the same pair," add participants in bulk, and attach metadata (name, avatar for groups) that the package itself doesn't care about. The raw package method will happily create two chats between the same users if you call it twice.
Polymorphic participants vs. user IDs. Depending on the version, participants may be stored as user IDs or through a pivot. If you need role-based membership (owner/admin/member for groups) or non-user participants (admins, support bots), add a chat_participants table of your own and treat the package's membership as the "core" set. Don't extend the package's migrations directly — you'll lose them on the next upgrade.
Attachments. Document sharing is the headline feature, so treat uploads as a first-class concern. The pattern that ages well: accept the upload in your own controller, store via Storage::disk(config('chat.storage_disk'))->putFile(...), then pass the resulting path/metadata into the package's message-create call. That way, when you need virus scanning, signed URLs, or size limits, you control the boundary. If you hand the raw UploadedFile straight to the package you lose those hooks.
Read/unread counts. The package tracks read state per participant. Two patterns are common:
chat_read_state (user_id, chat_id, last_read_message_id, unread_count) table updated on send and on read, and refresh it via a queued job. Let the package's counts be a fallback, not the primary read path.Broadcasting. If the package dispatches events (check src/Events), wire them into your BroadcastServiceProvider and expose them on private channels keyed by chat ID. Don't broadcast on public channels — read receipts and message previews leak content. If it doesn't ship events, that's the first thing to add yourself: after Message::create, dispatch MessageSent, and let a listener broadcast.
Notifications. Pair the package with Laravel's notification system rather than relying on it for delivery. Send a NewChatMessage notification (mail + database + broadcast) from a listener on the send path, so the chat package stays focused on storage and the "notify" policy stays in your app.
composer.json and be prepared to fork. Read every migration before running it in production — collision with existing chats/messages tables is a real risk if you've prototyped your own chat previously.ChatPolicy::view, ::send) yourself and use Gate::authorize in controllers; don't rely on the package to reject writes.get() behavior on a long chat will load the entire history. Always use the package's paginated query or ->paginate() on the relation, and load messages in reverse chronological order for the UI.unreadCount query is scoped to the right user, and (c) any broadcast listener is authenticated on the private channel. Ninety percent of "chat doesn't work" reports are one of those three.MessageObserver (for search indexing, moderation, notifications), a ChatPolicy (for group authorization), and a custom ChatRepository wrapping the package's queries if you need to swap in read-replica connections later. Keep all three in app/, never in vendor/.How can I help you explore Laravel packages today?