The User plugin was initially produced by a human, including CI & Semgrep Actions, and then substantially advanced using AI-directed methodology.
A WinterCMS plugin providing UUID-based user management as an abstraction layer over WinterCMS's built-in backend_users. Adds front-end user accounts, group membership with temporal history, roles, languages, and ethnicities.
- WinterCMS 1.2+ (Laravel 9+)
- PostgreSQL 13+ (for
gen_random_uuid();DATERANGEandEXCLUDE USING GISTare available since PostgreSQL 9.2) - PHP 8.1+
- Acorn module installed at
modules/acorn/
Two features depend on PostgreSQL specifically:
- DATERANGE columns with GiST indexes make point-in-time membership queries a single indexed join rather than a row-by-row subquery.
- EXCLUDE USING GIST enforces the constraint that a user cannot be in the same group twice during overlapping periods while still permitting leave-and-rejoin (two sequential, non-overlapping rows).
MySQL is not supported.
- Copy this plugin to
plugins/acorn/user/. - Ensure the Acorn module is at
modules/acorn/. - Run migrations:
php artisan winter:up - Navigate to Settings → Acorn → User to configure login mode, password rules, etc.
| Concept | Table | Notes |
|---|---|---|
| Front-end users | acorn_user_users |
UUID PK, SoftDelete, avatar, IP logging |
| Groups | acorn_user_user_groups |
Nested tree, translatable name/description |
| Current membership | acorn_user_user_group |
Fast pivot for admin UI |
| Temporal membership | acorn_user_user_group_users |
Daterange history — who was in a group on a given date |
| Roles | acorn_user_roles |
Many-to-many with primary flag |
| Languages | acorn_user_languages |
User language preferences |
Current membership (acorn_user_user_group) is the simple pivot used by the admin UI and fast $group->users lookups.
Temporal membership (acorn_user_user_group_users) answers the historical question "who was in group G on date D?" using a PostgreSQL DATERANGE column:
// Who is in this group right now?
$group->currentMembers();
// Who was in this group on a specific date?
$group->membersOn('2025-01-15');period = daterange(join_date, NULL) means currently active. Leave-and-rejoin creates two sequential non-overlapping rows — both are valid.
The temporal membership table enables an efficient single-query calendar month view. Instead of per-event PHP loops, join once in SQL:
SELECT ep.*
FROM acorn_calendar_event_parts ep
JOIN acorn_calendar_events e ON e.id = ep.event_id
JOIN acorn_calendar_calendars c ON c.id = e.calendar_id
JOIN acorn_user_user_group_users m
ON m.user_group_id = c.owner_user_group_id
AND m.user_id = :current_user
AND m.period @> ep.start::date
WHERE ep.start >= '2026-01-01'
AND ep.start < '2026-02-01'The GiST index on (user_group_id, period) makes this query fast even on large datasets.
All tables use UUID primary keys generated by gen_random_uuid(). This supports distributed record creation across plugins and installations without central counters, and avoids predictable sequential IDs in URLs.
The Acorn module is a hard requirement — models, controllers, and migrations all extend Acorn base classes. Install it at modules/acorn/ before running migrations.
This plugin declares no $require plugin dependencies (the $require array in WinterCMS is for plugin-to-plugin dependencies; module load order is guaranteed by WinterCMS separately). Every other plugin can optionally depend on this one via created_by_user_id columns — declaring a hard $require here would create circular dependencies. Install this plugin before any plugin that references its tables.
- The
acorn_user_user_group_usersperiod is currently set via raw SQL; there is no dedicated form widget for PostgreSQL daterange editing in the WinterCMS backend. - MySQL is not supported by design.
MIT