androsamp/filament-resource-lock
Record-level locking for Filament v5 edit pages: others instantly see who owns the lock, are blocked from accidental overwrite, and can request handoff. Includes optional audit history with per-field diff and selective rollback, plus polling or Laravel Echo push updates.
Architecture fit. The package is a UI-layer collaboration guard for Filament v5 EditRecord pages, delivered as three opt-in traits/concerns (HasResourceLocks on the model, InteractsWithResourceLock on the edit page, HasResourceAudit on edit/create pages) plus two table columns and one config file. It is not a database lock, an optimistic-concurrency token, or an API-level write barrier — it is an advisory, TTL-based lease with a heartbeat. That distinction drives most of the risk discussion below: it protects the human editing path inside Filament, and nothing else. Any write that bypasses the panel (queues, imports, REST endpoints, console commands, forceFill in a job) will happily overwrite a locked record. It is therefore complementary to, not a substitute for, optimistic locking (updated_at/version column checks) in the persistence layer — and any team that reads "prevents accidental overwrites" as a data-integrity guarantee will be disappointed.
Within that scope the design is sound and appropriately narrow. Locks are stored in database or redis; transport is heartbeat (poll) or broadcast (Echo). Both axes are independently configurable, which is the right seam — it lets us start cheap (DB + polling) and upgrade without touching application code. The permission.save_and_unlock.* / permission.ask_to_unblock.* config with can()-based gates and a null bypass is a reasonable authorization hook that maps onto existing policy patterns.
Integration feasibility. Mechanically easy: one composer require, one install command, one migrate, two traits, two columns. There is no panel plugin registration, no service-provider wiring in application code. That is roughly a half-day for a competent Filament developer on a single resource. The friction is not the API surface; it is the stack prerequisites and the install command's side effects.
Technical risk. Several items would go on my risk register before any pilot:
php ^8.3, laravel ^12 || ^13, filament ^5 — this package is only usable by projects already on Filament 5, which materially narrows the addressable base (and mirrors the 0 dependents / 7 stars signal). It also means the package tracks Filament internals (mount, save, afterCreate, header-action injection) that are not covered by Filament's BC promise. A Filament 5.x minor could break it.save() on the page replaces the trait's wrapper, so syncResourceAuditBeforeSave()/...AfterSave() must be re-invoked manually. Same for afterCreate(). Any team member who later customizes save() for unrelated reasons turns audit off without an error. That is a durability bug waiting to happen and needs an internal convention, a code-review checklist item, or a test that asserts audit rows appear after save.update policy, fires model events, or respects $fillable/mutators. Until verified, treat rollback as a privileged operation and gate it.HasAuditDiffPreview renders callback output as raw HTML; the README itself warns to escape. For any custom field showing user-controlled content, this is an XSS surface.UniqueConstraintViolationException when the same record is open in two tabs of one browser; v2.1.1 fixed a form refresh on post-wait lock acquisition. These are exactly the paths a pessimistic-lock UI must get right, and they were fixed within the last few releases. Expect more.Key questions.
ttl_seconds expires (and can a user self-recover)?ResourceLockColumn in list tables issue per-row queries or eager-load?broadcast driver behave when Echo is unavailable or channel auth fails — fail-open (editable) or fail-closed (locked)?audit.max_entries_per_resource the only retention control? What bounds total audit table growth?Stack fit. Only relevant if the target panel is on Filament 5 / Laravel 12+ / PHP 8.3+. If we are on Filament 3 or 4, this package is not a candidate today and the correct answer is a shortlist deferral, not an integration. Assuming Filament 5: the package is a natural fit for admin panels with genuine multi-editor contention on the same records — operations consoles, order/customer management, CMS-style content, back-office config — and a poor fit for panels where one or two admins work disjoint queues (paying heartbeat/polling cost for a lock nobody contends).
Migration path. Greenfield within the existing app — no schema changes to existing tables; the install publishes two new tables (locks + audits) and a config. The steps I would sequence:
composer require and pin to ~2.2 (not ^2.0) given the patch cadence.resources/js/filament-resource-lock/echo.js, and appends import './filament-resource-lock/echo'; to resources/js/bootstrap.js. That last step is an automated edit to application source. If we are on the polling driver, that import is dead code; either skip it (use the individual vendor:publish --tag= commands) or accept it and confirm the Vite build is clean.APP_URL is correct — the signed soft-release route (filament-resource-lock.release) fails validation silently if it isn't, which manifests as ghost locks.ResourceLockColumn on that resource's table only.HasResourceAudit to edit and create pages, and wire getAuditHistoryAction() into header actions.Compatibility. Two independent dependency surfaces to watch: (a) Filament 5.x minors — pin and test on upgrade; (b) the transport stack. broadcast requires a working broadcasting setup (Reverb/Pusher/Ably), Echo exposed on window, private-channel authorization, and a published echo.js kept aligned with broker/env. If we do not already run broadcasting, broadcast mode is not a config flip — it is a new piece of infrastructure.
Sequencing. Do not turn on all four capabilities at once. Order: (1) heartbeat + database — cheapest, no new infra, proves the UX; (2) add save_and_unlock and ask_to_unblock with real permissions defined; (3) add audit on edit pages, validate diffs render for our field types, confirm the save() override hazard does not apply to our page classes; (4) evaluate redis storage if lock QPS or latency demands it; (5) evaluate broadcast only if user-visible polling latency is an actual complaint. Rollback last, and only after the authorization question is answered.
Maintenance. Bus factor is one, dependents are zero, and the package is at v2.2.1 with several behavioral fixes in the last few releases. Treat it as unsupported OSS: budget for reading the source, expect to fork or vendor-patch, and record the pinned version and the reason for it. The integration surface is small enough (two traits, two columns) that a fork is genuinely viable. Encapsulate usage so a swap-out is a single PR: no direct calls to package internals outside the traits, config read through our own wrapper if we touch it in multiple places.
Support. No vendor SLA. Internal runbook needed for the two support tickets this will generate: "I'm locked out of a record I need to edit" (resolution: wait for TTL / have the owner save-and-unlock / admin force-release if it exists) and "the lock indicator is wrong" (resolution: check heartbeat polling, APP_URL/signed route, Echo channel auth). Localization ships en and ru only — any other locale needs translation work under filament-resource-lock::resource-lock.*.
Scaling. Two cost centers. (1) Transport: heartbeat polling is concurrent editors / interval requests against the app and the lock store — trivial at tens of editors, non-trivial at hundreds, and it is the reason redis storage exists. (2) Audit: each save writes a full previous-state and new-state snapshot plus a computed field diff. Storage grows with saves × record width, and max_entries_per_resource caps per-record history but not total table size — a long-tail of many lightly-edited records grows unbounded. Plan a global retention job and think about snapshot size on wide records with rich text/JSON fields. Also note audit snapshots duplicate PII into a second table; that table must be in the data-retention and erasure scope, not just the source tables.
Failure modes. Ranked by what the user sees:
ttl_seconds comfortably above the poll interval and an explicit "your lock expired" surface (verify one exists).APP_URL; a mismatch or a hard tab-close in poll mode leaves the record visually locked until TTL. Users perceive this as the tool being broken.save() override — detected only by the absence of history, so add a test that asserts an audit row is written after save.Ramp-up. Developer ramp is low: the README is unusually complete for a 7-star package, including a candid section on the save() override limitation and a worked custom-field diff example. Estimate half a day to integrate on a pilot resource, one to two days to configure permissions, validate diff renderers against our actual field set, and write the guard tests. User ramp is the larger cost: editors need a two-minute explanation of lock ownership, request-unlock, and save-and-unlock, or the first response to a disabled form will be a support ticket. Ship it to one resource with a small, known user group first, watch lock contention and ghost-lock reports for two weeks, and only then roll out — and keep the rollback-to-no-lock path (remove one trait) documented and tested, because with a single-maintainer dependency that is the real exit strategy.
How can I help you explore Laravel packages today?