Filum
CommunityFilum is a Filament plugin that adds persistent, real-time messaging between panel users - direct conversations and group threads joined by invitation, with presence, unread counts, and entries in Filament's own notification bell - delivered over whatever broadcaster your application already configures, or over polling when it configures none.
Author:
Rati Rukhadze
Package health
BetaAutomated checks of this plugin's Composer package
15 checks
- Failed: GitHub Actions pinned to SHA — View details on Plumb
- Skipped: GitLab CI includes pinned to SHA
- Passed: Open security advisories
- Passed: Dependabot PR responsiveness — No open Dependabot PRs.
- Skipped: Renovate MR responsiveness
- Passed: Dependabot or Renovate configured
-
Failed:
Dependency update cooldown configured
—
No cooldown configured in
.github/dependabot.yml. View details on Plumb - Passed: Provides a security policy
- Passed: Abandoned or archived — No consulted source marks the package abandoned (packagist, github).
- Passed: Commit and release recency — Active: last commit 0 days ago; last release 0 days ago.
-
Passed:
composer.lock not committed by library
—
composer.lockis absent from the released dist archive. - Passed: Dist archive is lean
-
Passed:
Current Laravel version supported
—
Package dependencies resolve together with current Laravel
13.0. -
Passed:
Current PHP version supported
—
Constraint
^8.3supports current PHP8.5. - Skipped: Current Symfony version supported
filament/
namespace. Review the source and install at your own risk. Found
malware or an unresolved security issue the author won't
address?
Report it
.
Documentation
- Requirements
- Installation
- Real-time
- Who can use it
- Notifications
- Groups
- Reactions
- Presence
- Your user model
- Language
- Switching it off
- Testing
- Credits
- License
Real-time chat between admin users, inside your Filament panel.
Click a colleague in the sidebar, the conversation opens, history persists, and messages arrive as they are sent. Available as a full page and as a slide-over overlay on every panel page.
Broadcasting-agnostic, and free. Filum works in an application that has never
configured broadcasting, and it never requires a paid service. If you have a
broadcaster — Reverb, soketi, Pusher, Ably — Filum uses it. If you don't, it
falls back to polling and the chat still works. Nothing in require pulls a
hosted WebSocket vendor.
Dark-first, following your panel's light/dark state. English and Georgian included.
#Requirements
- PHP 8.3+
- Laravel 12 or 13
- Filament 4.11.5+ or 5.6.5+
The Filament floor is a security minimum, not a preference: earlier 4.x and 5.x releases carry published advisories, so Filum will not resolve against them.
#Installation
composer require heyosseus/filum
php artisan filum:install
php artisan migrate
Then add the plugin to your panel provider:
use Heyosseus\Filum\FilumPlugin;
public function panel(Panel $panel): Panel
{
return $panel->plugin(FilumPlugin::make());
}
That's it. You now have a Chat page in the navigation and a chat trigger on every page. No broadcaster required.
#Upgrading an existing install
Each release that changes the schema publishes its migration under a tag of its own. Publish only that tag:
composer update heyosseus/filum
php artisan vendor:publish --tag=filum-migrations-groups # 0.1.x → 0.2.0
php artisan migrate
Do not publish filum-migrations on an install that already has Filum's
tables. That tag covers the whole directory and is for a fresh install. Laravel
stamps a published migration with the moment it was published, so your copy of
create_filum_tables is named after your original install rather than after
Filum's source file; vendor:publish looks for the source name, does not find it,
and copies the file a second time under a fresh stamp. migrate then runs the
duplicate first, hits table already exists, aborts, and never reaches the new
migration — so the new code queries columns that were never added, and all chat
fails, including 1:1. If it has already happened, delete the newer duplicate of
*_create_filum_tables.php before migrating.
Run the migration in the same release step as the code, not after it. New code
on an old schema is a hard outage rather than a degradation: opening a direct
conversation writes kind, state and joined_at, and every membership check
reads state.
#Real-time
Filum picks a transport automatically:
| Your application | What Filum does |
|---|---|
| No broadcaster configured | Polls every 5 seconds |
| A real broadcaster configured | Pushes over it, and reconciles by polling every 30 seconds |
The reconciliation poll stays on under a broadcaster on purpose: a dropped socket, a missed event, or a laptop waking from sleep heals itself instead of leaving someone reading a stale thread.
To go real-time with no third party, install Reverb:
composer require laravel/reverb
php artisan reverb:install # also installs Echo and sets BROADCAST_CONNECTION
php artisan reverb:start
npm run build # Echo has to reach the browser
Filum needs no further configuration — it reads whatever
config/broadcasting.php already says and switches by itself.
Both halves have to be in place. Filum starts pushing as soon as a real
broadcaster is configured, and the browser starts listening as soon as
window.Echo exists in the panel. If you configure a broadcaster but never build
an Echo client into your assets, the socket half is missing and the
reconciliation poll is all that is left — which is slower than the no-broadcaster
default, because it is 30 seconds rather than 5. reverb:install sets up both;
if you wire a broadcaster up by hand, make sure Echo is actually loaded on the
page.
To decide for yourself:
// config/filum.php
'transport' => [
'driver' => 'polling', // 'auto' (default), 'broadcast', or 'polling'
],
#Who can use it
By default, anyone the panel has already authenticated. To narrow it, register a gate in a service provider:
use Heyosseus\Filum\Filum;
Filum::auth(fn ($user) => $user->hasRole('admin'));
One gate governs everything — the page, the overlay, and the broadcast channels — so nobody can subscribe to a conversation the page would not have shown them.
#Notifications
A message to someone who is not looking rings Filament's own notification bell,
writing into the application's existing notifications table. No broadcaster
needed — Filament polls the bell.
It rings once per conversation, on the transition from caught up to behind. A forty-message burst is one bell entry, not forty; catching up and falling behind again earns a fresh one.
It also rings only for a recipient who is not currently present. Someone with
the panel open already sees the unread counter change on the colleague, on the
group, and on the overlay tab, so a bell as well would only tell them what they
can already see. "Present" is exactly the set the board shows as HERE NOW, so
the bell and the board never disagree — tune it with presence.ttl, below. This
applies to invitations too: inviting someone who is at their desk right now grows
the invitations section for them on the next tick and never rings.
'notifications' => [
'enabled' => env('FILUM_NOTIFICATIONS', true),
],
Where the notifications table has never been migrated, or your user model is
not Notifiable, this quietly does nothing — the message still sends. To deliver
somewhere else entirely (mail, Slack, a pager), implement
Heyosseus\Filum\Contracts\Notifier and bind it:
$this->app->singleton(Heyosseus\Filum\Contracts\Notifier::class, MyNotifier::class);
#Groups
Beyond 1:1 chat, colleagues can form a group. Joining is always by invitation: creating a group only seats the creator, who becomes its owner; everyone else appears in the invitations section of their own board and has to accept before the thread, the send path or the broadcast channel will admit them. Declining and leaving land on the same state, so a colleague who said no and a colleague who walked away look identical to the group afterwards.
Permissions are flat with one exception for the owner:
- Any member may invite a colleague.
- Anyone may leave.
- Only the owner may remove someone else, rename the group, or delete it.
An owner does not remove themselves — they leave, like anyone else. When they do, ownership passes automatically to whoever has been joined the longest (ties broken by who joined first, i.e. the lower participant id). If nobody is left joined, the group is deleted rather than left ownerless.
The group's thread header carries an invite picker: a disclosure listing colleagues who are neither invited nor already joined, so growing the group is a click from the thread itself. Someone who has left reappears in that list — leaving does not blacklist a person, it just returns them to "not yet invited."
The owner additionally sees a rename field and a roster of everyone else in the group, each row with a Remove button; a row still marked Pending has not accepted yet, and removing it withdraws the invitation. No row is ever offered against the owner themselves — an owner leaves, which hands the group on. All of it is plain fields and buttons rather than modals, on purpose: a Filament modal inside a Livewire component inside the drawer is the one place Filament 4 and 5 behave differently.
'groups' => [
'enabled' => env('FILUM_GROUPS', true),
],
Disabled means absent, the same as the panel-wide switch below: no groups section on the board, no new-group action, and existing groups genuinely unreachable — a member who was joined before the switch cannot open one, read it, send into it or leave it while it is off, and every group action is refused. Not a feature that half-works underneath a switched-off UI. Nothing is deleted, so switching it back on restores what was there.
#Reactions
Any message can carry emoji reactions. They are a toggle — tapping the same emoji twice takes it back, so there is no separate remove control to find. Several people can add the same emoji, and one person can add several different ones.
'reactions' => [
'enabled' => env('FILUM_REACTIONS', true),
'emoji' => ['👍', '❤️', '😂', '🎉', '👀', '✅'],
],
The set is configuration rather than a picker, and anything outside it is refused rather than stored. Filum ships compiled CSS and runs no build step, so a full picker would mean a JavaScript bundle for something a back office uses six of — name your own six instead. Reactions are ordered by that list rather than by count, so a row of chips never reshuffles under the cursor.
Reactions never ring the notification bell. A thumbs-up is an acknowledgement, not a message, and an entry for each one would bury the entries that matter. They appear for everyone looking at the thread on the next tick, with or without a broadcaster.
#Presence
The sidebar shows who is around. A heartbeat writes last_seen_at on an
interval, and that store is the only source of truth. Under a broadcaster,
presence-channel events additionally prompt an immediate re-read, so the sidebar
updates instantly — but it behaves identically without one, just with up to a
minute of latency.
'presence' => [
'heartbeat_interval' => 60, // seconds between beats
'ttl' => 180, // how long a beat counts as "here"
],
ttl is also what decides whether the notification bell rings, above — and it
is deliberately the only knob for that decision. It would be easy to add a
second, shorter threshold just for notifications ("ring if silent for 30s even
if the board still shows them as here"), but that threshold could then disagree
with what the board displays: a colleague marked HERE NOW who nonetheless gets
a bell, which reads as a bug even though it would be working as configured. One
number, shared by both, means the bell and the board can never contradict each
other — the honest cost is that ttl's default of 180 seconds means someone who
closes their laptop and is written to thirty seconds later still counts as
present, and gets no bell for it. Lower ttl if that lag matters more to you
than a slightly twitchier "here" indicator; there is no separate way to tune one
without the other.
#Your user model
Filum assumes nothing about your schema:
'users' => [
'model' => App\Models\AdminUser::class,
'guard' => null, // null follows the panel's own guard
'name_column' => 'name',
'avatar_column' => null, // e.g. 'avatar_url'
'provider' => Heyosseus\Filum\Users\ConfiguredUserProvider::class,
],
model is the one thing you must set if your panel does not authenticate
App\Models\User. Filum queries it to list colleagues and derives the migration's
foreign key from it, and neither can be guessed — a panel on its own guard has its
own user model. Set it before you migrate:
FILUM_USER_MODEL=App\Models\AdminUser
Unquoted, and note it goes in every environment's .env, not just your local one.
Leaving it wrong is not a quiet failure: Filum is asked for the current user while
the navigation is being built, so an unresolvable model surfaces as an error on
every page in the panel rather than only on the chat.
guard needs no value — Filum follows whichever guard the panel uses. Name one
only to override that.
Integer, UUID and ULID keys all work — migrations derive the foreign key from the
model you name. For anything more involved (a name assembled from two columns, an
avatar from a media library, a restricted set of chattable colleagues), implement
Heyosseus\Filum\Contracts\UserProvider and name it as the provider.
#Language
English and Georgian ship complete. Publish to change or add:
php artisan vendor:publish --tag=filum-translations
#Switching it off
'enabled' => false,
Disabled means absent: no page, no overlay, no channels, no commands. Not a chat that turns people away.
To keep the page but drop the overlay:
'overlay' => ['enabled' => false],
#Testing
composer test # rector, pint, phpstan, type coverage, line coverage
composer test:unit # tests with coverage
composer test:tia # tests via Pest's Test Impact Analysis (local, PHP 8.4+)
Both transports are covered by the suite, including the case with no broadcaster bound at all — the install Filum promises to work in.
#Credits
#License
MIT. See LICENSE.md.
The author
From the same author
Featured Plugins
A selection of plugins curated by the Filament team
Blueprint
Filament Blueprint is a premium Laravel Boost extension that helps AI agents produce accurate, detailed implementation plans and security reports for Filament apps.
Filament
Spotlight Pro
Browse your Filament Panel with ease. Filament Spotlight Pro adds a Spotlight/Raycast like Command Palette to your Filament Panel.
Dennis Koch
Custom Fields
Eliminate custom field migrations forever. Let your users create and manage form fields directly in Filament admin panels with 20+ built-in field types, validation, and zero database changes.
Relaticle