geekcodev/filament-max-users
Filament Max Users lets you enforce a maximum number of user accounts in your Filament panel. Set a limit, prevent new users from being created once it's reached, and keep registrations under control with simple configuration. Handy for licensing, plan tiers, demos, and single-tenant apps.
This is a Filament 5 plugin (filament/filament ^5.0, dev-pinned to 5.9.0) that mirrors MAX messenger data into two panel resources — «Max пользователи» and «Max чаты» — backed by geekcodev/max-php-client. It is a read/observe surface, not an authoring tool: you browse users and chats, and drive one write-ish operation (refresh chat metadata from the MAX API).
Minimal path:
composer require geekcodev/filament-max-users on Laravel 11/12 with a Filament 5 panel. Make sure your panel resolves to ≥5.9.0 — 1.1.1 pulls that in for advisory CVE-2026-104181, and a constrained lockfile can leave you on 5.7.8.PanelProvider (->plugin(...)) — confirm the exact class name and builder from the README, which is kept in sync with releases (1.1.1 updated both the config block and .env.example).vendor:publish for the package's config/lang tags), then set your panel's navigation group — FILAMENT_MAX_USERS_NAVIGATION_GROUP is now documented in .env.example, which tells you the nav group is env-tunable and worth setting per environment.max-php-client; the resources read from your local tables, and only the refresh action hits the network.displayName/participants/last activity. That single action is the best smoke test — if it works on a group (not just a dialog), your max-php-client is at 1.1.9 and the participants-map fix is in place.Before going further, check the access story: permissions are namespaced (users.*, chats.*) and unchanged in this patch, so your existing policies/gates carry over.
Treat the column toolbar as the customization contract. As of 1.1.1 every column in both lists is toggleable, and visibility state is persisted per Filament user. That means per-user table shape is a supported feature, not an accident — point your users at the column toggle instead of shipping variants of the resource. Defaults are deliberately narrow (identity, badges, avatar, phone, counters); the wide fields (email, username, description, full_avatar_url, last_activity_time, profile_checked_at, and for chats icon_url, title, link, chat_checked_at) are opt-in per user.
Extend by subclassing, not by patching. Order and visibility are now locked by dedicated feature tests (test_list_shows_columns_in_expected_default_order, test_list_exposes_every_column_as_toggleable, test_view_page_shows_entries_in_expected_order). That's great news for upgrades — upstream can't silently reshuffle your tables — but it also means your overrides won't be caught by upstream tests. If you need a different default order or a bespoke column, subclass the resource and override the table/infolist schema in your app, and write your own assertions.
Reuse the presenter for any link you render yourself. ChatPresenter::safeUrl() is what keeps javascript: / data: payloads out of href. If you add a custom column, a Blade view, an export, or a widget that shows the chat link, route it through the presenter rather than printing the API string. The same discipline applies to displayName, which does the title → interlocutor name → chat_id fallback — don't reimplement that resolution in your own views or the labels will drift.
Model relations are the join points. The chat count on a user is counts('chatLinks') and the participant count on a chat is counts('chatUsers'). Any dashboard widget or custom page you build should use the same relations with withCount/with to stay consistent and avoid N+1 — these are exactly the relations the refresh action writes through.
If you build your own sync, parse participants as a map. The 1.1.1 fix exists because real GET /chats/{id} returns {user_id: activity} for groups and channels, not a list. Any ingestion job or command you write against the client must handle the map shape, otherwise you'll reproduce the crash the package just fixed. Same for ordering: the {"user_id": activity} values are the source for the "последняя активность" column.
Localization is an override surface. lang/ru and lang/en are key-synchronized and additive in this release (users.table.name, users.table.description, users.table.full_avatar_url, users.table.phone_verified_at, chats.table.description, chats.table.link). Publish the lang files and rebrand strings in your app rather than editing vendor — new keys will land cleanly next release.
table.username and view.username in lang/ru now read «Никнейм» instead of «Username»; lang/en still says "Username". If you published lang files and merged overrides, re-publish or diff — and any test or docs screenshot asserting "Username" in a Russian panel will fail.email, username, description, phone_verified_at, last_activity_time, profile_checked_at; title, link, icon_url, chat_checked_at on chats) are hidden by default. Filament persists toggle state per user, so returning users can end up with a mix of old and new state. When in doubt, have them reset the column toggle for that table.title is a legacy field. «Название из MAX» is now hidden-by-default in the chats list and removed from the chat view page, because it duplicates displayName for groups/channels and is empty for dialogs. Use displayName for anything user-facing; title stays only for diagnostics.view.title duplicate key removed from lang/{ru,en}/chats.php. Behaviour is unchanged (the second, effective value was always the one used), but a hand-merged published lang file with the stale duplicate will now diverge from vendor — clean it up. chats.table.title and the chats.title.* section are untouched.geekcodev/max-php-client 1.1.9, not in the plugin. composer update geekcodev/filament-max-users alone can leave the old client resolved; update the client explicitly. There's a regression test (test_refresh_metadata_action_parses_group_participants_map) guarding it upstream, but not in your app.laravel.log for the client exception — not the resource class.php artisan filament:assets (and clear cached views/components) after upgrading, especially if you have custom table/infolist overrides that touch column APIs.phone_verified_at; unverified numbers render plain. Don't chase it as a data bug.is_bot is a badge, not a filter contract. It renders «Бот»/«Пользователь»; the label is unchanged in this release. Filter on the underlying boolean, not on localized strings.composer security-audit at zero. That means subclassing and typed overrides are safe bets — but keep docblocks on your own overrides if you run PHPStan in the host app.How can I help you explore Laravel packages today?