Laravel package for synchronizing users and teams across multiple Laravel applications via a Publisher/Receiver pattern.
php artisan boost:add-skill madbox-99/laravel-user-team-sync
Save this content to: AGENTS.md
# Laravel User Team Sync
Laravel package for synchronizing users and teams across multiple Laravel applications via a Publisher/Receiver pattern.
## Architecture
- **Publisher**: Sends sync events to other apps via HTTP + queue jobs
- **Receiver**: Exposes API endpoints to accept sync events
- **Both**: Acts as publisher and receiver simultaneously
Config key: `user-team-sync.mode` (`publisher` | `receiver` | `both`)
## Project Structure
```
src/
├── Concerns/
│ ├── LogsOutboundSync.php # Shared trait for publisher jobs (retry config + logging)
│ └── LogsInboundSync.php # Shared trait for receiver controllers (logging)
├── Console/
│ └── InstallCommand.php # php artisan user-team-sync:install
├── Publisher/
│ ├── PublisherService.php # Central orchestrator, dispatches jobs
│ ├── Observers/
│ │ └── UserSyncObserver.php # Auto-syncs on User model changes
│ └── Jobs/
│ ├── CreateUserJob.php # Broadcasts new user to all active apps
│ ├── SyncUserJob.php # Broadcasts field changes
│ ├── CreateTeamJob.php # Creates team on receiver apps
│ ├── ToggleUserActiveJob.php # Toggles active status on specific app
│ └── RemoveTeamMemberJob.php # Ends a team membership on specific app
├── Receiver/
│ └── Http/
│ ├── Controllers/ # API endpoints for incoming sync
│ ├── Middleware/
│ │ └── ValidateSyncApiKey.php # Bearer token auth + receiving guard
│ └── Requests/ # Form request validation
├── Events/ # Dispatchable events for hooks
├── Enums/
│ └── SyncAction.php # create_user, sync_user, etc.
├── Models/
│ ├── SyncLog.php # Audit log model ($guarded = [])
│ └── SyncApp.php # Publisher app model (encrypted api_key, dynamic table)
├── Facades/
│ └── UserTeamSync.php # Facade for PublisherService
└── UserTeamSyncServiceProvider.php
```
## Key Conventions
- **App source**: Publisher apps can be stored in config (`app_source = config`, default) or database (`app_source = database`). The `SyncApp` model uses `encrypted` cast for `api_key` and `toAppArray()` to provide a consistent format to `PublisherService`.
- **Password handling**: Passwords are always hashed on the publisher side. The receiver stores the hash directly (`password_hash` field). Never send plain text passwords.
- **Infinite loop prevention**: `ValidateSyncApiKey` middleware sets `app('user-team-sync.receiving', true)` during inbound requests (with try/finally reset). Observer checks this flag and skips dispatch when true. Controllers use `saveQuietly()` where needed.
- **Observer**: Only watches fields listed in `config('user-team-sync.publisher.sync_fields')` (default: `email`, `role`).
- **Email changes**: The observer maps email changes to `new_email` key, using the original email as identifier.
## API Endpoints (Receiver)
All require Bearer token via `ValidateSyncApiKey` middleware.
| Method | Endpoint | Purpose |
|--------|----------|---------|
| POST | `/api/create-user` | Create user with `password_hash` |
| POST | `/api/sync-user` | Update user fields |
| POST | `/api/toggle-user-active` | Set active/inactive |
| POST | `/api/create-team` | Create team, optionally attach user |
| POST | `/api/remove-team-member` | Detach user from team (account kept) |
| GET | `/api/user-teams` | Get user's team IDs |
| POST | `/api/sync-password` | Sync pre-hashed password |
## Testing
- **Framework**: Pest PHP + Orchestra Testbench
- **Run**: `vendor/bin/pest`
- **Test models**: `tests/Fixtures/User.php` and `tests/Fixtures/Team.php` (SQLite in-memory)
## Shared Traits
- **`LogsOutboundSync`** (publisher jobs): Provides `initRetryConfig()` for retry/backoff from config, and `logOutbound()` for SyncLog entries.
- **`LogsInboundSync`** (receiver controllers): Provides `logInbound()` for SyncLog entries.
## Code Style
- PHP 8.3+, `declare(strict_types=1)` everywhere
- Laravel Pint for formatting (`vendor/bin/pint`)
- All classes are `final`
- Typed properties, constructor promotion, named arguments
- BackedEnum for action types
- Static event dispatch (`Event::dispatch()`) instead of `event()` helper
Laravel package for synchronizing users and teams across multiple Laravel applications via a Publisher/Receiver pattern.
Config key: user-team-sync.mode (publisher | receiver | both)
src/
├── Concerns/
│ ├── LogsOutboundSync.php # Shared trait for publisher jobs (retry config + logging)
│ └── LogsInboundSync.php # Shared trait for receiver controllers (logging)
├── Console/
│ └── InstallCommand.php # php artisan user-team-sync:install
├── Publisher/
│ ├── PublisherService.php # Central orchestrator, dispatches jobs
│ ├── Observers/
│ │ └── UserSyncObserver.php # Auto-syncs on User model changes
│ └── Jobs/
│ ├── CreateUserJob.php # Broadcasts new user to all active apps
│ ├── SyncUserJob.php # Broadcasts field changes
│ ├── CreateTeamJob.php # Creates team on receiver apps
│ ├── ToggleUserActiveJob.php # Toggles active status on specific app
│ └── RemoveTeamMemberJob.php # Ends a team membership on specific app
├── Receiver/
│ └── Http/
│ ├── Controllers/ # API endpoints for incoming sync
│ ├── Middleware/
│ │ └── ValidateSyncApiKey.php # Bearer token auth + receiving guard
│ └── Requests/ # Form request validation
├── Events/ # Dispatchable events for hooks
├── Enums/
│ └── SyncAction.php # create_user, sync_user, etc.
├── Models/
│ ├── SyncLog.php # Audit log model ($guarded = [])
│ └── SyncApp.php # Publisher app model (encrypted api_key, dynamic table)
├── Facades/
│ └── UserTeamSync.php # Facade for PublisherService
└── UserTeamSyncServiceProvider.php
app_source = config, default) or database (app_source = database). The SyncApp model uses encrypted cast for api_key and toAppArray() to provide a consistent format to PublisherService.password_hash field). Never send plain text passwords.ValidateSyncApiKey middleware sets app('user-team-sync.receiving', true) during inbound requests (with try/finally reset). Observer checks this flag and skips dispatch when true. Controllers use saveQuietly() where needed.config('user-team-sync.publisher.sync_fields') (default: email, role).new_email key, using the original email as identifier.All require Bearer token via ValidateSyncApiKey middleware.
| Method | Endpoint | Purpose |
|---|---|---|
| POST | /api/create-user |
Create user with password_hash |
| POST | /api/sync-user |
Update user fields |
| POST | /api/toggle-user-active |
Set active/inactive |
| POST | /api/create-team |
Create team, optionally attach user |
| POST | /api/remove-team-member |
Detach user from team (account kept) |
| GET | /api/user-teams |
Get user's team IDs |
| POST | /api/sync-password |
Sync pre-hashed password |
vendor/bin/pesttests/Fixtures/User.php and tests/Fixtures/Team.php (SQLite in-memory)LogsOutboundSync (publisher jobs): Provides initRetryConfig() for retry/backoff from config, and logOutbound() for SyncLog entries.LogsInboundSync (receiver controllers): Provides logInbound() for SyncLog entries.declare(strict_types=1) everywherevendor/bin/pint)finalEvent::dispatch()) instead of event() helperHow can I help you explore Laravel packages today?