PHPackages                             wezlo/filament-record-watcher - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Admin Panels](/categories/admin)
4. /
5. wezlo/filament-record-watcher

ActiveLibrary[Admin Panels](/categories/admin)

wezlo/filament-record-watcher
=============================

Subscribe to Eloquent records and receive in-panel Filament notifications when they change, with per-watch condition rules and field diffs.

1.0.0(1mo ago)1401MITPHPPHP ^8.2

Since Apr 11Pushed 1mo agoCompare

[ Source](https://github.com/mustafakhaleddev/filament-record-watcher)[ Packagist](https://packagist.org/packages/wezlo/filament-record-watcher)[ RSS](/packages/wezlo-filament-record-watcher/feed)WikiDiscussions master Synced 1w ago

READMEChangelog (1)Dependencies (5)Versions (2)Used By (0)

Filament Record Watcher
=======================

[](#filament-record-watcher)

Subscribe to individual Eloquent records and receive in-panel Filament notifications whenever they change — with the actor (who) and a field-level diff (what). Watches can carry conditions ("only if status changes", "only if amount &gt; 10K"), can be paused, and live on a personal **My Watches** page scoped to the authenticated user. Every fan-out is also persisted to a permanent event log, so users can review the full change history even after dismissing notifications.

Highlights
----------

[](#highlights)

- **One-click Watch / Unwatch actions** for any Filament Resource record.
- **Conditions DSL** — JSON rules with `changed`, `=`, `!=`, `>`, `=`, `databaseNotifications() // required — watchers receive db notifications
        ->plugins([
            FilamentRecordWatcherPlugin::make(),
        ]);
}
```

Make a model watchable
----------------------

[](#make-a-model-watchable)

Add the `HasWatchers` trait to any Eloquent model:

```
use Wezlo\FilamentRecordWatcher\Concerns\HasWatchers;

class Order extends Model
{
    use HasWatchers;
}
```

You can now:

```
$order->watchFor($user);                                  // subscribe with no conditions
$order->watchFor($user, [                                 // subscribe with rules
    ['field' => 'status', 'operator' => 'changed', 'value' => null],
    ['field' => 'amount', 'operator' => '>',       'value' => 10_000],
]);

$order->isWatchedBy($user);    // bool
$order->watches;               // MorphMany of Watch rows
$order->watchers();            // collection of distinct subscribed users

$order->unwatchFor($user);     // delete the user's subscription (cascades to events)

$order->update(['status' => 'paid']);   // → fans out notifications + events to all matching watchers
```

`watchFor()` is idempotent — calling it twice for the same `(record, user)` pair updates the existing row instead of creating a duplicate (enforced by a unique index).

### Curating which fields users can build conditions on

[](#curating-which-fields-users-can-build-conditions-on)

The **Field** input in the condition modal is rendered as a searchable `Select`, populated from `getWatchableFields()`. The trait's default introspects the model's table and returns every column minus the primary key, `deleted_at`, and any plugin-level ignored columns (default `updated_at`, `created_at`).

Override the method on your model to expose a curated list with friendlier semantics:

```
class Order extends Model
{
    use HasWatchers;

    public function getWatchableFields(): array
    {
        return ['status', 'total', 'client_name', 'address'];
    }
}
```

Filament integration — `WatchAction` / `UnwatchAction`
------------------------------------------------------

[](#filament-integration--watchaction--unwatchaction)

Drop the actions into any Resource that uses a watchable model. They are two separate actions:

- **`WatchAction`** — always visible. Label flips between "Watch" and "Edit watch" depending on whether the current user is already subscribed. Opens a modal with a `Repeater` of condition rows.
- **`UnwatchAction`** — visible only when the current user is already subscribed. One-click confirmation modal.

```
use Filament\Actions\EditAction;
use Filament\Actions\ViewAction;
use Wezlo\FilamentRecordWatcher\Actions\UnwatchAction;
use Wezlo\FilamentRecordWatcher\Actions\WatchAction;

public static function table(Table $table): Table
{
    return $table
        ->columns([
            TextColumn::make('reference'),
            TextColumn::make('status')->badge(),
        ])
        ->recordActions([
            ViewAction::make(),
            EditAction::make(),
            WatchAction::make(),
            UnwatchAction::make(),
        ]);
}
```

Both actions also work in page header arrays (`headerActions()`) on view / edit pages.

### Permissions

[](#permissions)

Both actions expose a `->can()` method that accepts a boolean or a closure receiving `(Model $record, ?Authenticatable $user)` and returning `bool`. Returning `false` hides the action for that user / row. The check is ANDed with the action's built-in visibility rules.

```
WatchAction::make()
    ->can(fn (Order $record, ?Authenticatable $user) => $user?->can('view', $record) ?? false),

UnwatchAction::make()
    ->can(fn (Order $record) => auth()->user()->hasPermissionTo('manage-watches')),
```

Conditions DSL
--------------

[](#conditions-dsl)

Conditions are stored as a JSON array of rules. Empty / null conditions are **fail-open** — every non-ignored change notifies. Multiple rules are **ANDed** together.

```
[
  {"field": "status", "operator": "changed", "value": null},
  {"field": "amount", "operator": ">",       "value": 10000}
]
```

OperatorSemantics`changed`Passes if the field appears in the diff for this update.`=` / `!=`Loose comparison against the **new** attribute value.`>` / `=` / `watch, $event->record, $event->diff, $event->actor
});
```

How change detection works
--------------------------

[](#how-change-detection-works)

`HasWatchers::bootHasWatchers()` registers a static `updated` model hook that:

1. Builds a field-level diff with `DiffBuilder::build($model, $ignored)` using the model's `getOriginal()` + `getChanges()`.
2. Skips ignored columns (`updated_at`, `created_at` by default — configurable).
3. If the filtered diff is non-empty, calls `WatchEngine::fanOut($model, $diff)`.

`WatchEngine::fanOut()`:

1. Eager-loads `$model->watches()->active()->with('user')` (active = `paused_at IS NULL`).
2. Skips the actor's own watch.
3. For each remaining watch, evaluates conditions via `ConditionEvaluator::passes()`.
4. For each passing watch: persists a `WatchEvent`, sends a Filament database notification, dispatches `RecordWatchedChange`.

Database schema
---------------

[](#database-schema)

### `watches`

[](#watches)

ColumnPurpose`id`Primary key`watchable_type`, `watchable_id`Polymorphic target`user_id`Subscriber (FK to `users`, cascade on delete)`conditions`JSON rules array (nullable = notify on any change)`paused_at`When the watch was paused (nullable)`created_at`, `updated_at`TimestampsIndexes:

- Unique `(watchable_type, watchable_id, user_id)` — one watch per user per record.
- `(user_id, paused_at)` — fast lookup for the My Watches page.

### `watch_events`

[](#watch_events)

See the [Persistent event history](#persistent-event-history) section above.

Configuration reference
-----------------------

[](#configuration-reference)

Publish with `php artisan vendor:publish --tag="filament-record-watcher-config"`.

KeyDefaultDescription`user_model``App\Models\User`Model used for the `user_id` relationship.`table_name``watches`Name of the polymorphic subscriptions table.`events_table_name``watch_events`Name of the persistent event history table.`ignored_columns``['updated_at', 'created_at']`Column changes that should NOT trigger watcher notifications.`max_diff_lines``8`Max diff lines included in a notification body.`my_watches.enabled``true`Whether the **My Watches** page registers itself.`my_watches.navigation_group``null`Navigation group for the page.`my_watches.navigation_icon``heroicon-o-bell`Navigation icon.`my_watches.navigation_sort``95`Navigation sort order.You can also configure most of these fluently on the plugin (`->ignoredColumns([...])`, `->navigationGroup(...)`, `->navigationIcon(...)`, `->userModel(...)`, `->registerMyWatchesPage(false)`) — the plugin values win over config values when both are set.

Low-level service API
---------------------

[](#low-level-service-api)

```
use Wezlo\FilamentRecordWatcher\Services\WatchEngine;
use Wezlo\FilamentRecordWatcher\Services\ConditionEvaluator;

// Manually fan out (rarely needed — the observer handles this for you)
app(WatchEngine::class)->fanOut($order, [
    'status' => ['old' => 'pending', 'new' => 'paid'],
]);

// Evaluate a rule set against a model + diff
$passes = app(ConditionEvaluator::class)->passes($order, $diff, $watch->conditions);

// Run a write under an explicit actor (queues, console, system jobs)
WatchEngine::actingAs($systemUser, fn () => $order->update(['status' => 'paid']));
```

The engine performs no authorization of its own — it's a low-level primitive. Enforce permissions at the caller (action, job, command) before invoking it.

Translations
------------

[](#translations)

The package ships English translations under `filament-record-watcher::watcher.*`. Every user-visible string — action labels, modal headings, notifications, table columns, page title, history modal — is routed through `__()`. Add additional locales by publishing:

```
php artisan vendor:publish --tag="filament-record-watcher-translations"
```

Testing
-------

[](#testing)

The package is exercised by 20 Pest tests covering the condition evaluator, the trait surface (`watchFor` / `unwatchFor` / `getWatchableFields`), the observer + engine notification path, the persistent event log (survives notification deletion), the diff builder's ignored-columns behaviour, and the auth-scoped My Watches page. Run them from the host app:

```
php artisan test --compact tests/Feature/FilamentRecordWatcher
```

License
-------

[](#license)

MIT.

###  Health Score

41

—

FairBetter than 87% of packages

Maintenance88

Actively maintained with recent releases

Popularity14

Limited adoption so far

Community7

Small or concentrated contributor base

Maturity46

Maturing project, gaining track record

 Bus Factor1

Top contributor holds 100% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Unknown

Total

1

Last Release

59d ago

### Community

Maintainers

![](https://avatars.githubusercontent.com/u/25182746?v=4)[Mustafa Khaled](/maintainers/mustafakhaleddev)[@mustafakhaleddev](https://github.com/mustafakhaleddev)

---

Top Contributors

[![mustafakhaleddev](https://avatars.githubusercontent.com/u/25182746?v=4)](https://github.com/mustafakhaleddev "mustafakhaleddev (1 commits)")

---

Tags

laravelnotificationsAuditsubscribewatchfilament

###  Code Quality

TestsPest

### Embed Badge

![Health badge](/badges/wezlo-filament-record-watcher/health.svg)

```
[![Health](https://phpackages.com/badges/wezlo-filament-record-watcher/health.svg)](https://phpackages.com/packages/wezlo-filament-record-watcher)
```

###  Alternatives

[awcodes/filament-quick-create

Plugin for Filament Admin that adds a dropdown menu to the header to quickly create new items.

249203.6k11](/packages/awcodes-filament-quick-create)[rawilk/profile-filament-plugin

Profile &amp; MFA starter kit for filament.

3913.7k](/packages/rawilk-profile-filament-plugin)[mradder/filament-logger

Audit logging, activity tracking, exports, alerts, and dashboards for Filament admin panels.

2210.5k](/packages/mradder-filament-logger)[stephenjude/filament-jetstream

A Laravel starter kit built with Filament inspired by Jetstream.

17758.9k2](/packages/stephenjude-filament-jetstream)[stephenjude/filament-two-factor-authentication

Filament Two Factor Authentication: Google 2FA + Passkey Authentication

84192.9k7](/packages/stephenjude-filament-two-factor-authentication)[guava/filament-knowledge-base

A filament plugin that adds a knowledge base and help to your filament panel(s).

207140.2k1](/packages/guava-filament-knowledge-base)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
