Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Filament Team Chat Laravel Package

qalainau/filament-team-chat

Self-hosted Slack-like team chat for Filament v5. Channels, DMs, threads, reactions, mentions, file sharing, search, and unread tracking work out of the box. No external services needed—just Livewire polling and your existing auth and users.

View on GitHub
Deep Wiki
Context7

Getting Started

The package is self-contained — its own tc_ prefixed tables, its own Livewire components, its own polling layer. That means the fastest way to build confidence is to prove the whole install pipeline works for two real users before you touch a single config value.

Install and migrate:

composer require qalainau/filament-team-chat

php artisan vendor:publish --tag=team-chat-migrations
php artisan migrate

# only needed for @mention / DM database notifications
php artisan make:notifications-table && php artisan migrate

Register the plugin in the panel provider where chat should live:

use Filament\TeamChat\FilamentTeamChatPlugin;

public function panel(Panel $panel): Panel
{
    return $panel
        // ...
        ->plugin(FilamentTeamChatPlugin::make());
}

Add the trait to User. This is not optional — DM helpers, mention resolution, unread counts and presence all hang off it:

use Filament\TeamChat\Concerns\HasTeamChat;

class User extends Authenticatable
{
    use HasTeamChat;
}

Compile the Tailwind sources. This is the number one first-run failure. The package ships utility classes in its own views; Filament v5 only compiles what your theme's @source globs include. If you have no custom theme yet, run php artisan filament:theme first, then:

/* resources/css/filament/admin/theme.css */
@source '../../../../vendor/qalainau/filament-team-chat/resources/views/**/*';

Then npm run build.

Publish the config immediately, even if you change nothing. Having config/team-chat.php in your repo makes future upgrades diffable instead of a guessing game about defaults:

php artisan vendor:publish --tag=team-chat-config

First use case to validate: two browsers, two users, open /admin/team-chat, click a public channel (clicking auto-joins), send a message. It should appear in the second window within ~3 seconds. If it doesn't, suspect Livewire polling or your theme build — not the chat layer.

Where to look first when something is off:

  1. config/team-chat.php — polling intervals, uploads, tenancy, feature toggles.
  2. The tc_ tables — missing data is almost always an unpublished migration, not a problem with your users table.
  3. The Livewire units — Sidebar (5s), MessageFeed (3s), MessageComposer, ThreadPanel (3s), ChannelHeader. These are the things that poll and dispatch events; when the UI "feels stale," the interval lives there.

Implementation Patterns

Treat the action classes as the public API

The same classes powering the UI are your integration surface. Prefer them over writing to models directly, because they handle membership, mentions, notifications and receipts consistently.

use Filament\TeamChat\Actions\SendMessage;

$message = app(SendMessage::class)->execute(
    messageable: $channel,       // Channel or Conversation
    userId: $user->id,
    body: 'Deploy finished: **v2.4.1** is live.',
    parentId: null,
    files: [],
);

This is the pattern for: deploy notifications from CI, ticket-created pings from an observer, "your invoice is ready" messages from a queued job. Create a dedicated system/bot user and post as it rather than impersonating a human.

Seed channels and membership as part of onboarding

Rather than asking admins to click around, create the default workspace on user registration or in a DatabaseSeeder:

use Filament\TeamChat\Models\Channel;

$channel = Channel::firstOrCreate(
    ['slug' => 'general'],
    [
        'name' => 'general',
        'type' => 'public',
        'created_by' => $user->id,
    ],
);

$channel->members()->syncWithoutDetaching([$user->id => ['role' => 'owner']]);

Because public channels are visible to everyone and auto-join on click, you mostly need this for private channels (e.g. #leadership, #support-tier-2) where membership must be explicit.

DMs are idempotent — use them as a routing primitive

findOrCreateDirectMessage() returns the existing conversation if one exists. That makes it safe to call from a "Contact support" button, an order-detail page, or a user profile card without creating conversation duplicates:

$dm = $user->findOrCreateDirectMessage($otherUser->id);

// group DM
$group = $user->createGroupConversation(
    userIds: [$user2->id, $user3->id],
    name: 'Incident #4821',
);

A nice day-to-day pattern: an "Open incident channel" action in your own Filament resource that spins up a group DM with the on-call users and posts the incident summary via SendMessage.

Scoped search on your own pages

SearchMessages already enforces membership rules — it won't leak messages from channels or DMs the user isn't in. Reuse it rather than querying tc_messages yourself:

use Filament\TeamChat\Actions\SearchMessages;

$results = app(SearchMessages::class)->execute($user->id, 'deploy', limit: 20);

You can surface this in a global Filament search provider or a dashboard widget so chat content becomes discoverable outside /team-chat.

Read tracking and badge counts

use Filament\TeamChat\Actions\MarkAsRead;

$unread = $channel->unreadCountFor($user->id);   // => 3

app(MarkAsRead::class)->execute($channel, $user->id);

unreadCountFor() is cheap enough to use in a navigation badge or a "You have N unread messages" widget. Mark-as-read is also what a "Mark all read" bulk action should call — don't update the receipt table by hand.

Restricting who can chat (v0.5.0+)

On multi-audience installs, this is the most valuable hook in the package. It narrows the DM user list, @mention autocomplete and profile cards — and it's enforced server-side, so it isn't just cosmetic:

FilamentTeamChatPlugin::make()
    ->modifyAvailableUsersQueryUsing(
        fn (Builder $query, User $user) => $query->where('tenant_id', $user->tenant_id)
    );

Also reach for the features block in the config when a deployment shouldn't have the full Slack surface — a helpdesk-style panel may want DMs and search off, channels on.

Multi-tenant scoping

Enable tenancy.enabled and let the default resolver pick up Filament::getTenant(). Only reach for a custom resolver when chat scope differs from panel scope:

'tenancy' => [
    'enabled' => true,
    'model' => \App\Models\Team::class,
    'resolver' => fn () => auth()->user()->current_team_id,
],

A class with a resolve() method is also supported — useful if the resolution needs DI or caching.

Reactions and presence as a lightweight signal

use Filament\TeamChat\Actions\ToggleReaction;

app(ToggleReaction::class)->execute($message->id, $user->id, '👍'); // true=added, false=removed

The eight built-in emoji are enough for "ack / done / eyes" workflows. Combined with tc_user_statuses, you get a decent lightweight presence layer without adding a broadcast service.

Gotchas and Tips

Tailwind is the silent killer. If the chat renders as unstyled markup, it's the missing @source glob 95% of the time. Second most common: adding the glob but forgetting npm run build. Also rebuild after upgrading — v0.5.0 added responsive and RTL utility classes that won't exist in a stale CSS bundle.

Set user_key_type before migrating. If your users table uses UUIDs or ULIDs, the migrations must create matching foreign keys. Publishing, migrating, then discovering you need UUID FKs means a fresh install or hand-written migration surgery. Same applies to tenancy.key_type. Do this on day zero, not day ninety.

Polling is not realtime, and it costs you. 3s on the message feed plus 5s on the sidebar, per open tab. That's fine for a 20-person internal tool and not fine for 500 concurrent users. Raise the intervals in config/team-chat.php for larger installs, and be aware that the thread panel adds another 3s poll on top. Budget accordingly before you promise "instant."

Attachments need a reachable disk. Defaults are disk => public and directory => team-chat-attachments, max_size => 10240 KB. If images don't preview, check php artisan storage:link and that APP_URL is correct — this is a Laravel filesystem issue, not a chat issue. Bump max_size deliberately, since uploads land in a Livewire request.

Feature toggles change the UI, so rebuild the theme. Disabling threads, reactions or search via features updates the component behaviour, but the classes you're hiding still need to be in the compiled bundle for the remaining components to look right.

Don't touch tc_messages directly for anything user-facing. Sending, editing, deleting, mentions, mentions-driven notifications and read receipts are all coordinated by the action classes and Livewire events. A raw Message::create() will produce a message with no mentions parsed and no notifications dispatched — which looks like a bug and is actually your bug.

Soft-deleted messages are still rows. If you build reports or analytics on tc_messages, filter deleted records explicitly. Same for archived channels — archiving hides them from the sidebar but doesn't remove them from queries.

Thread replies are just messages with parent_id. When counting "messages in a channel" for a dashboard metric, decide whether thread replies count. They live in the same table, polymorphically attached to the parent — a naive count() will overcount the main feed.

Multi-tenant migration is not retroactive. Toggling tenancy.enabled on an existing install with a single shared workspace will strand existing channels with a null team_id. Plan a backfill, or accept that tenancy is a fresh-install decision.

Notifications require the table. If @mentions and DMs don't notify, it's usually because notifications was never created. Check with php artisan migrate:status before digging into the package.

Debugging stale UI: open the Livewire devtools and watch which component fires. MessageFeed updating but Sidebar not → the sidebar's 5s poll is the one to look at. Neither updating → check your Livewire asset build and whether a JS error is killing the polling loop.

Testing: the package ships 102 tests using Orchestra Testbench. From your app you can filter with php artisan test --filter=TeamChat to confirm you haven't broken integration during an upgrade. If you're writing your own tests against the action classes, use SendMessage and MarkAsRead rather than fixtures — you get correct state for free.

Version discipline: 15 stars and a README-level maturity signal. Pin the version in composer.json, read the CHANGELOG before bumping (v0.5.0 was a feature release with migration implications), and keep your published config and theme in version control so upgrades are reviewable diffs rather than surprises.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
mehedimi/laravel-amp-mail
happones/laravel-vue-i18n-generator
jwerd/inertia-table
siberfx/flysystem-wasabi
hollyit/token-replace
ippanelcom/laravel-sdk
adachsoft/changelog-linter
adachsoft/collection
payerurl/telegrambot-crypto-payment-instant-payout
dskripchenko/laravel-hybrid-search
riorizkyrainey/lara-ced
mehedimi/static-vel
zharp/zharp
nonsapiens/bigquery-model-sync
lgu-bayambang-ict-office/lgu-laravel-development-standards
andydefer/laravel-support
se7enxweb/flysystem
lampwick/laravel
jeslage/statamic-emojis
micahdshackelford/rmap-laravel