geekcodev/filament-max-chat
Filament plugin for an operator chat with MAX messenger users. View dialogs with unread badges, reply with HTML formatting and media attachments, receive real-time Echo/Reverb updates with a poll fallback, and configure permissions, channel, disk and navigation via config.
What v1.1.5 is. A single additive migration (0000_02_000003_add_unread_index_to_max_chat_messages.php) adding composite index max_chat_messages_unread_index on (direction, read_at) to max_chat_messages. No code paths, public signatures, config keys, translation keys, or defaults changed. This is a performance patch, not a feature release.
Architecture fit. The package already owns its schema via versioned migrations, so shipping an index as a migration is the correct Laravel-idiomatic mechanism — host applications get the fix through the normal php artisan migrate path with no publish step, no config edit, and no service-provider changes. The index targets exactly the predicate used by MaxMessageService::totalUnreadCount() and UnreadCountController (direction = 'in' AND read_at IS NULL), and deliberately leaves markRead() on the existing max_chat_id-leading index. That is a well-scoped, low-blast-radius change.
Integration feasibility. Highest-band feasibility: no env vars, no migrations to backfill, no data movement, no API surface change. up() is guarded by Schema::hasIndex, so re-running or installing into a host that already added an equivalent index will not fail the deploy. down() drops the index cleanly. The accompanying MaxChatMessagesSchemaTest covers presence, removal, and idempotency, and the suite reports 271 tests / 628 assertions / 98.31% line coverage with PHPStan at max — quality signal is strong for a patch of this size.
Technical risk.
CREATE INDEX, which takes an ACCESS EXCLUSIVE lock and blocks writes for the duration of the build; there is no CONCURRENTLY path here. On MySQL/InnoDB, adding a secondary index is generally INPLACE/LOCK=NONE, but still requires a brief metadata lock and can be slow on a large table. php artisan migrate runs synchronously inside the deploy, so table size — not code correctness — determines whether this deploy is safe.Schema::hasIndex has a minimum Laravel version. The package's composer.json floor is not stated in the notes; if the constraint permits older Laravel releases, hasIndex may not exist and the migration errors on those installs.EXPLAIN before/after, no row-count-at-scale benchmark, and Gate step 7 (clean resolve in .ci-sim/) was skipped locally due to host disk pressure and deferred to CI.Key questions for the maintainer.
totalUnreadCount() timings and query plans before/after?Schema::hasIndex availability across the full constraint range?max_chat_messages table on MySQL 8 and PostgreSQL 14+, and is there any documented guidance for hosts that need to build the index out-of-band (e.g. CREATE INDEX CONCURRENTLY manually, then run migrate relying on the idempotency guard)?Stack fit. Drop-in for any Laravel host already running this package: standard package migration discovery, no vendor publish, no config cache invalidation, no queue or scheduler changes. Works across the three claimed drivers (SQLite, MySQL, PostgreSQL) through the schema builder with no driver-specific branches.
Migration path. Run php artisan migrate. Nothing to backfill, nothing to rewrite, no downtime-window requirement beyond what the index build itself imposes on a large table. Because no application code references the new index, the migration is order-independent relative to the code deploy — it can be applied before, with, or after the release without correctness risk. Prefer before the code rollout if you deploy code and migrations as separate steps, so the perf win lands with the first request.
Compatibility. No breaking changes: public signatures, config keys/defaults, and translation keys are untouched. up() is idempotent via Schema::hasIndex, which makes the migration safe for hosts that pre-created the index and for repeated CI database resets. Rollback is a clean index drop, though it reinstates the pre-patch scan behaviour — treat rollback as a perf regression, not a functional one.
Sequencing for a large install. For tables in the low millions of rows or under, the default path is fine: deploy the version bump, let migrate run, monitor migration duration. For tables in the tens of millions or beyond, recommend a staged approach: (1) verify Schema::hasIndex behaviour on your Laravel version in staging, (2) on PostgreSQL, create the index manually with CREATE INDEX CONCURRENTLY max_chat_messages_unread_index ... during a low-traffic window so the migration's guard short-circuits, (3) set a deploy-time statement/lock timeout so a stalled DDL fails fast rather than pinning the deploy, (4) roll the application code, (5) confirm the index is present and the badge query plan changed. Keep the package version pinned in composer.lock and roll forward deliberately rather than absorbing it in a bulk dependency update.
Validation gates before merge. Run the host's own migration against a production-shaped dataset in staging, capture EXPLAIN for the unread count query, and confirm the pre-existing max_chat_id-leading index is still used by markRead() (i.e. the new index did not displace it). Confirm CI ran Gate step 7 green for the tag, since that step was skipped locally.
Maintenance. Effectively zero steady-state maintenance: one index, no new config, no new env vars, no cron or queue entry. Write-path cost is a modest increase in index maintenance on max_chat_messages inserts and on read_at updates (which is what markRead() does) — measurable only at high message-ingest rates. The down() path is clean, so the change is fully reversible.
Support. The blast radius if something goes wrong is a slow or failed migration, not broken messaging. Support scripts should know: (a) failure mode = migration timeout / lock wait on a large table, remedy = build the index out-of-band and rely on the idempotency guard; (b) a redundant index appearing alongside a host-created one is expected if names differ and is harmless but worth pruning; (c) rollback reinstates the slow-scan behaviour immediately.
Scaling. This materially improves the failure mode described in the notes: the unread badge was a full table scan per panel render, so tab-switching traffic could exhaust Octane workers, starve /up, and trigger container restarts — a cascading availability incident driven by a read query. The index converts that to a bounded range scan and should remove the healthcheck-restart loop at the observed scales. Two caveats for capacity planning: the query remains O(unread rows) rather than O(1), so the ceiling moves out but does not disappear; and the underlying architectural coupling (synchronous per-render aggregate for a navigation badge) is unchanged. Flag the denormalised-counter follow-up as the durable fix.
Failure modes to instrument.
max_chat_messages; alert if migrate exceeds the deploy budget.max_chat_messages after rollout, particularly if markRead() fires per message rather than per chat./up failing under load remains the tripwire for this class of problem; keep the restart-loop runbook entry and point it at unread-count query cost.Ramp-up. Minimal. No new concepts, APIs, or config for the team to learn — engineers need only the one-paragraph operational note (migrate, index may be built out-of-band on large tables, rollback is a perf regression) and the reminder that php artisan migrate is now a potentially long-running DDL step on big installs. Budget well under a day of engineering time including staging verification; the real cost is the staging dataset you need to validate the index build against, not the change itself. Confidence to adopt: high, contingent on verifying the DDL duration at your own table size and confirming Gate step 7 is green in CI.
How can I help you explore Laravel packages today?