Real-time chat package for Laravel 11, 12 and 13.
Private and group conversations, message reactions, file attachments, message status (sent / delivered / seen), typing indicators, user blocking, group management, FCM and Web Push notifications, WebSocket broadcasting via Reverb or Pusher.
Zero manual steps. Everything is configured automatically on install and removed on uninstall.
| Dependency | Version |
|---|---|
| PHP | 8.2 or higher |
| Laravel | 11.x, 12.x, or 13.x |
| Laravel Sanctum | 4.x or 5.x |
All optional packages (Reverb, Pusher, Firebase, Web Push) are installed
automatically based on your choices during talkbridge:install.
composer require rahatulrabbi/talkbridge
php artisan talkbridge:installThe wizard asks two questions, then does everything else automatically:
[2] Select broadcasting driver
Which broadcasting driver do you want to use?
> reverb — Reverb (self-hosted WebSocket, recommended)
pusher — Pusher Channels (cloud, requires credentials)
ably — Ably (cloud, requires API key)
log — Log driver (testing / local only)
null — Null driver (broadcasting disabled)
[5] Select push notification provider
Which push notification provider do you want?
> none — Disabled (no push notifications)
fcm — Firebase Cloud Messaging (Android + iOS)
web — Browser Web Push via VAPID (desktop browsers)
both — FCM + Web Push (mobile and browser)
After answering, TalkBridge:
- Installs the broadcaster package (
composer require laravel/reverbetc.) - Installs the push notification package (
composer require kreait/laravel-firebaseetc.) - Runs
composer dump-autoloadafter each install - Publishes
config/talkbridge.php - Publishes all database migrations
- Writes all
.envvariables - Injects
HasTalkBridgeFeaturesinto yourApp\Models\User - Adds
last_seen_atto$fillable(if your model uses$fillable) - Registers middleware alias, scheduler, broadcast channels, API routes — all via ServiceProvider
# 1. Review and adjust user field mapping if your columns differ
# See: Configuration section below
# 2. Start WebSocket server (if using Reverb)
php artisan reverb:start --debug
# 3. Start queue worker
php artisan queue:work --queue=talkbridgeOpen config/talkbridge.php. The most important section is user_fields.
TalkBridge needs to know your column names. Change the values to match your database:
'user_fields' => [
'id' => 'id',
'name' => 'name', // single column — most common
'avatar' => 'avatar_path', // your avatar column name
'last_seen' => 'last_seen_at', // your last_seen column name
'is_active' => null, // set to 'is_active' if you have this column
],If your users table stores first and last name separately:
'user_fields' => [
'name' => ['first_name', 'last_name'],
// or three parts:
'name' => ['f_name', 'm_name', 'l_name'],
],TalkBridge will automatically join them with a space everywhere — system messages, presence channel payloads, reaction lists, conversation resources.
If you want to define routes yourself:
'routing' => [
'enabled' => false,
],Copy stubs/talkbridge/ to your routes folder and register manually in bootstrap/app.php.
'routing' => [
'enabled' => true,
'prefix' => 'api/v2',
'middleware' => ['api', 'auth:sanctum', 'talkbridge.last-seen'],
],'uploads' => [
'disk' => 's3',
'message_path' => 'chat/messages',
'group_avatar_path'=> 'chat/groups/avatars',
],Control what settings new groups get:
'group_defaults' => [
'allow_members_to_send_messages' => true,
'allow_members_to_add_remove_participants' => false,
'allow_members_to_change_group_info' => false,
'admins_must_approve_new_members' => false,
'allow_invite_users_via_link' => true,
],Every feature is available as a service method. Inject ChatService anywhere
in your application.
use RahatulRabbi\TalkBridge\Services\ChatService;
class YourController extends Controller
{
public function __construct(protected ChatService $chat) {}
}Or use the Facade:
use RahatulRabbi\TalkBridge\Facades\TalkBridge;// List conversations (paginated, supports search)
$conversations = $chat->listConversations($user, perPage: 30, query: 'search term');
// Start or get a private conversation
$conversation = $chat->startConversation($user, receiverId: 5);
// Create a group
$group = $chat->createGroup($user, [
'name' => 'Project Team',
'participants' => [2, 3, 4],
'group' => ['description' => 'Our team', 'type' => 'private'],
]);
// Remove conversation from user's list (soft delete for that user only)
$chat->deleteConversationForUser($user->id, $conversationId);
// Media library for a conversation (images, video, audio, files, links)
$media = $chat->mediaLibrary($user, $conversationId, perPage: 30);// Send a text message
$message = $chat->sendMessage($user, [
'conversation_id' => 15,
'message' => 'Hello team!',
'message_type' => 'text',
]);
// Send with a file attachment
$message = $chat->sendMessage($user, [
'conversation_id' => 15,
'message_type' => 'image',
'attachments' => [['path' => $request->file('image')]],
]);
// Reply to a message
$message = $chat->sendMessage($user, [
'conversation_id' => 15,
'message' => 'I agree',
'reply_to_message_id' => 42,
]);
// Forward a message to multiple conversations
$chat->sendMessage($user, [
'conversation_id' => 20,
'message' => $original->message,
'message_type' => $original->message_type,
'forward_to_message_id' => $original->id,
]);
// Edit a message
$updated = $chat->updateMessage($user, ['message' => 'Corrected text'], $message);
// Get messages (paginated, supports search)
$messages = $chat->getMessages($user, $conversationId, query: null, perPage: 20);
// Get pinned messages
$pinned = $chat->pinnedMessages($user, $conversationId);
// Pin or unpin
$chat->pinToggleMessage($user, $message);
// Delete for current user only
$chat->deleteForMe($user, ['message_ids' => [10, 11, 12]]);
// Unsend for everyone
$chat->deleteForEveryone($user, ['message_ids' => [10]]);
// Mark all messages as seen when opening a conversation
$chat->markConversationAsRead($user, $conversationId);
// Mark specific messages as seen (when conversation is already open)
$chat->markMessagesAsRead($user, [
'conversation_id' => 15,
'message_ids' => [40, 41, 42],
]);
// Mark as delivered
$chat->markDelivered($user, $conversationId);// Toggle a reaction (adds if not present, removes if already reacted with same emoji)
$reactions = $chat->toggleReaction($user, $messageId, '❤️');
// List all reactions grouped by emoji
$reactions = $chat->listReactions($messageId);
// Returns:
// [
// 'total_reactions' => 5,
// 'grouped' => [
// '❤️' => ['count' => 3, 'users' => [...]],
// '👍' => ['count' => 2, 'users' => [...]],
// ]
// ]// Add members
$chat->addMembers($user, $groupId, memberIds: [5, 6, 7]);
// Remove members
$chat->removeMember($user, $groupId, memberIds: [6]);
// Promote to admin
$chat->addGroupAdmins($user, $groupId, userIds: [5]);
// Demote admin
$chat->removeGroupAdmins($user, $groupId, userIds: [5]);
// Get all members
$members = $chat->getMembers($user, $groupId);
// Mute a group
$chat->muteGroup($user, $groupId, minutes: 60); // mute for 60 minutes
$chat->muteGroup($user, $groupId, minutes: -1); // mute forever
$chat->muteGroup($user, $groupId, minutes: 0); // unmute
// Leave
$chat->leaveGroup($user, $groupId);
// Delete (super_admin only)
$chat->deleteGroup($user, $groupId);
// Update group name, description, avatar, settings
$chat->updateGroupInfo($user, $groupId, [
'name' => 'New Name',
'group' => [
'description' => 'Updated description',
'avatar' => $request->file('avatar'),
'allow_members_to_send_messages' => false,
],
]);
// Generate or regenerate invite link
$result = $chat->regenerateInvite($user, [
'expires_at' => now()->addDays(7),
'max_uses' => 50,
], $groupId);
// $result['invite_link'] = 'https://your-app.com/api/v1/accept-invite/abc123'
// Accept invite
$chat->acceptInvite($user, $token);// Block / unblock (toggles)
$isBlocked = $chat->toggleBlock($user, $targetUserId);
// Restrict / unrestrict (toggles)
$isRestricted = $chat->toggleRestrict($user, $targetUserId);Publish the stubs:
php artisan talkbridge:publish --tag=stubsThen create your own controller that extends the package controller:
namespace App\Http\Controllers\Chat;
use RahatulRabbi\TalkBridge\Http\Controllers\Api\V1\Chat\MessageController as BaseController;
use RahatulRabbi\TalkBridge\Http\Requests\Chat\SendMessageRequest;
use Illuminate\Support\Facades\Auth;
class MessageController extends BaseController
{
public function store(SendMessageRequest $request)
{
// Your custom pre-send logic
$data = $request->validated();
$data['message'] = strip_tags($data['message'] ?? '');
$message = $this->chatService->sendMessage(Auth::user(), $data);
// Your custom post-send logic (e.g. custom notification)
// MyNotificationService::notify($message);
return $this->success($message, 'Message sent.', 201);
}
}Disable the built-in routes and register yours:
// config/talkbridge.php
'routing' => ['enabled' => false],// routes/api.php
Route::prefix('api/v1')->middleware(['api', 'auth:sanctum', 'talkbridge.last-seen'])->group(function () {
Route::apiResource('messages', \App\Http\Controllers\Chat\MessageController::class)
->only(['store', 'show', 'update']);
// ... rest of routes from stubs/talkbridge/
});namespace App\Services;
use RahatulRabbi\TalkBridge\Services\ChatService;
class AppChatService extends ChatService
{
public function sendMessage($user, array $data)
{
// Custom validation
if (strlen($data['message'] ?? '') > 5000) {
throw new \InvalidArgumentException('Message too long.');
}
$message = parent::sendMessage($user, $data);
// Log to your own analytics
// Analytics::track('message_sent', ['user_id' => $user->id]);
return $message;
}
}Bind it in your AppServiceProvider:
$this->app->bind(
\RahatulRabbi\TalkBridge\Services\ChatService::class,
\App\Services\AppChatService::class
);// In your EventServiceProvider or AppServiceProvider
use RahatulRabbi\TalkBridge\Events\MessageEvent;
use RahatulRabbi\TalkBridge\Events\ConversationEvent;
Event::listen(MessageEvent::class, function (MessageEvent $event) {
if ($event->type === 'sent') {
// e.g. send email digest, update analytics
}
});
Event::listen(ConversationEvent::class, function (ConversationEvent $event) {
if ($event->action === 'member_added') {
// e.g. send welcome message
}
});Publish the resource stubs and override:
php artisan talkbridge:publish --tag=stubsExtend the resource:
namespace App\Http\Resources\Chat;
use RahatulRabbi\TalkBridge\Http\Resources\Chat\MessageResource as BaseResource;
class MessageResource extends BaseResource
{
public function toArray($request): array
{
$base = parent::toArray($request);
// Add your custom fields
$base['is_bookmarked'] = $request->user()?->bookmarks()->where('message_id', $this->id)->exists();
$base['custom_meta'] = $this->custom_field ?? null;
return $base;
}
}Bind in AppServiceProvider:
$this->app->bind(
\RahatulRabbi\TalkBridge\Http\Resources\Chat\MessageResource::class,
\App\Http\Resources\Chat\MessageResource::class
);// Upload a file to the configured disk
$path = talkbridge_upload_file($file, 'uploads/custom');
// Delete a file
talkbridge_delete_file($path);
// Delete multiple files
talkbridge_delete_files([$path1, $path2]);
// Detect file type from extension
$type = talkbridge_file_type('photo.jpg'); // 'image'
// Get user display name (respects composite name config)
$name = talkbridge_user_name($user);
// Get user avatar
$avatar = talkbridge_user_avatar($user);After install, your User model has these methods available:
// Check if user is online
$user->isOnline(); // bool
// Get display name (handles composite columns)
$user->getChatDisplayName(); // string
// Get avatar URL
$user->getChatAvatar(); // string|null
// Get last seen as human diff
$user->getChatLastSeen(); // '2 minutes ago'
// Blocking
$user->hasBlocked($otherUser); // bool
$user->isBlockedBy($otherUser); // bool
$user->blockedUsers(); // BelongsToMany
$user->blockedByUsers(); // BelongsToMany
// Restricting
$user->hasRestricted($otherUser); // bool
$user->restrictedUsers(); // BelongsToMany
$user->restrictedByUsers(); // BelongsToMany
// Device tokens
$user->deviceTokens(); // HasManyimport Echo from 'laravel-echo';
import Pusher from 'pusher-js';
import axios from 'axios';
window.Pusher = Pusher;
window.axios = axios;
axios.defaults.withCredentials = true;
window.Echo = new Echo({
broadcaster: 'reverb',
key: import.meta.env.VITE_REVERB_APP_KEY,
wsHost: import.meta.env.VITE_REVERB_HOST ?? window.location.hostname,
wsPort: Number(import.meta.env.VITE_REVERB_PORT ?? 8080),
wssPort: Number(import.meta.env.VITE_REVERB_PORT ?? 8080),
forceTLS: (import.meta.env.VITE_REVERB_SCHEME ?? 'http') === 'https',
enabledTransports: ['ws', 'wss'],
});// Global online presence
window.Echo.join('online')
.here(users => { onlineUsers.value = users; })
.joining(user => { onlineUsers.value.push(user); })
.leaving(user => { onlineUsers.value = onlineUsers.value.filter(u => u.id !== user.id); });
// Personal notifications
window.Echo.private(`user.${authUser.id}`)
.listen('.ConversationEvent', event => {
if (event.action === 'added') { addToConversationList(event.conversation); }
if (event.action === 'removed') { removeFromConversationList(event.conversation.id); }
if (event.action === 'blocked') { markAsBlocked(event.conversation.id); }
});
// Conversation channel
const channel = window.Echo.join(`conversation.${conversationId}`)
.listen('.MessageEvent', event => {
if (event.type === 'sent') { addMessage(event.payload); }
if (event.type === 'updated') { updateMessage(event.payload); }
if (event.type === 'deleted_for_everyone'){ markAsUnsent(event.payload); }
if (event.type === 'reaction') { updateReactions(event.payload); }
if (event.type === 'seen') { updateReadStatus(event.payload); }
if (event.type === 'delivered') { updateDeliveryStatus(event.payload); }
if (event.type === 'pinned') { markPinned(event.payload); }
})
.listen('.ConversationEvent', event => {
if (event.action === 'member_added') { refreshMemberList(); }
if (event.action === 'updated') { refreshGroupInfo(); }
})
.listenForWhisper('typing', ({ name, isTyping }) => {
typingUser.value = isTyping ? name : null;
});
// Typing indicator
channel.whisper('typing', { userId: authUser.id, name: authUser.name, isTyping: true });For Flutter and React Native integration, see docs/mobile/README.md.
All under /api/v1 with Sanctum auth (Authorization: Bearer {token}).
| Method | Endpoint | Description |
|---|---|---|
| GET | /conversations |
List all (paginated, ?q=search) |
| POST | /conversations |
Create group |
| POST | /conversations/private |
Start or get private conversation |
| DELETE | /conversations/{id} |
Remove for current user only |
| GET | /conversations/{id}/media |
Media library |
| Method | Endpoint | Body / Notes |
|---|---|---|
| POST | /messages |
{conversation_id, message, message_type} |
| GET | /messages/{conversation} |
Paginated, ?q=search |
| PUT | /messages/{message} |
{message} |
| DELETE | /messages/delete-for-me |
{message_ids: [1,2]} |
| DELETE | /messages/delete-for-everyone |
{message_ids: [1]} |
| GET | /messages/seen/{conversation} |
Mark all seen (on open) |
| POST | /messages/mark-seen |
{conversation_id, message_ids:[...]} |
| GET | /messages/delivered/{conversation} |
Mark as delivered |
| POST | /messages/{message}/forward |
{conversation_ids: [2, 3]} |
| POST | /messages/{message}/toggle-pin |
Pin or unpin |
| GET | /messages/{conversation}/pinned-messages |
All pinned |
| Method | Endpoint | Body |
|---|---|---|
| POST | /messages/{message}/reaction |
{"reaction":"❤️"} |
| GET | /messages/{message}/reaction |
Returns grouped reactions |
| Method | Endpoint | Body / Notes |
|---|---|---|
| POST | /group/{id}/update |
{name, group:{description, avatar, type, ...settings}} |
| POST | /group/{id}/members/add |
{member_ids: [5,6]} |
| POST | /group/{id}/members/remove |
{member_ids: [5]} |
| GET | /group/{id}/members |
All members |
| POST | /group/{id}/admins/add |
{member_ids: [5]} |
| POST | /group/{id}/admins/remove |
{member_ids: [5]} |
| POST | /group/{id}/mute |
{minutes: 60} / -1=forever / 0=unmute |
| POST | /group/{id}/leave |
Leave the group |
| DELETE | /group/{id}/delete-group |
Super admin only |
| POST | /group/{id}/regenerate-invite |
{expires_at?, max_uses?} |
| GET | /accept-invite/{token} |
Join via invite link |
| Method | Endpoint | Notes |
|---|---|---|
| GET | /available-users?search=name |
Search users |
| GET | /online-users |
Currently online |
| POST | /users/{user}/block-toggle |
Block or unblock |
| POST | /users/{user}/restrict-toggle |
Restrict or unrestrict |
| Action | When |
|---|---|
added |
User added to / created a conversation |
removed |
Removed from group |
left |
Left group |
updated |
Group name / avatar / settings changed |
deleted |
Group deleted |
blocked / unblocked |
Block status changed |
unmuted |
Auto-unmuted by scheduler |
member_added / member_left |
Group membership changed |
admin_added / admin_removed |
Role changed |
| Type | When |
|---|---|
sent |
New message |
updated |
Message edited |
deleted_for_everyone |
Message unsent |
deleted_permanent |
Hard deleted |
reaction |
Reaction toggled |
delivered / seen |
Status update |
pinned / unpinned |
Pin toggled |
| Command | Description |
|---|---|
php artisan talkbridge:install |
Install wizard |
php artisan talkbridge:install --broadcaster=pusher --push=fcm |
Non-interactive install |
php artisan talkbridge:uninstall |
Remove everything |
php artisan talkbridge:uninstall --keep-data |
Remove code, keep database tables |
php artisan talkbridge:uninstall --keep-packages |
Remove code, keep Composer packages |
php artisan talkbridge:uninstall --force |
Skip all confirmation prompts |
php artisan talkbridge:publish --tag=config |
Re-publish config only |
php artisan talkbridge:publish --tag=migrations |
Re-publish migrations only |
php artisan talkbridge:publish --tag=stubs |
Publish stubs for customization |
php artisan talkbridge:version |
Show installed version and recent changelog |
php artisan talkbridge:version --check |
Check Packagist for a newer version |
php artisan talkbridge:update |
Update to latest version — runs composer update, migrations, cache clear |
php artisan talkbridge:update --version=1.0.1 |
Install a specific version |
php artisan talkbridge:update --force |
Update and overwrite published config and stubs |
php artisan talkbridge:auto-unmute |
Process expired mutes (auto-run by scheduler) |
php artisan talkbridge:uninstallTalkBridge reads TALKBRIDGE_INSTALLED_BROADCASTER and TALKBRIDGE_INSTALLED_PUSH
from .env to know exactly which packages it installed, so it only removes those —
it never touches packages that were already in your project.
After uninstall:
composer remove rahatulrabbi/talkbridgeSee CHANGELOG.md.
MIT — see LICENSE.
Author: MD. RAHATUL RABBI — github.com/learnwithfair