madbox-99/laravel-user-team-sync
Laravel package for syncing users and teams. It offers straightforward helpers for managing team membership and user assignments, keeping relationships consistent across your Eloquent models and existing Laravel application.
Architecture fit The package description undersells what this actually is. "Synchronization between a subscriber hub and secondary Laravel apps" reads like a config-and-traits helper; the documented internals describe a distributed Publisher/Receiver system: outbound HTTP calls dispatched through queue jobs, inbound API endpoints guarded by bearer tokens, model observers that fire automatically on user changes, and a container-flag guard to prevent sync loops. That is a materially different adoption decision than adding a pivot manager.
The architecture is a good fit only if our topology is genuinely hub-and-spoke with one authoritative identity store and N downstream apps that can tolerate eventual consistency. It is a poor fit for a single app, for a shared-database multi-app setup, or for any peer-to-peer model where multiple apps can authoritatively change a user. The mode config (publisher / receiver / both) is the first thing a TPM must resolve, because both makes every app a potential source of truth and immediately raises conflict-resolution questions the package does not appear to answer.
Note that the UserSyncObserver watches only config('user-team-sync.publisher.sync_fields') — default email and role. Everything else silently diverges. That is a deliberate, defensible design, but it means the product definition of "what is a user" is now split across a config array, and product owners will assume more is synced than actually is.
Integration feasibility
Mechanically the install is easy (composer require, user-team-sync:install, publish config). The feasibility question is not install cost — it is surface area:
users table needs to accept a password_hash copied verbatim from the publisher. That is a schema change on every app in the fleet, not just the one we're integrating.Technical risk Ranked by severity:
create-team on a receiver "creates a team." If receiver-side team IDs are locally generated, the same logical team has different IDs per app, which breaks cross-app URLs, deep links, and any downstream reporting. If IDs are preserved from the publisher, we inherit collision risk with pre-existing receiver rows. This is the single highest-impact unknown and must be resolved before any schema work.initRetryConfig), but no endpoint is documented as accepting an idempotency key or performing an upsert. A retried CreateUserJob against a receiver that already has the row is either an error the queue retries forever or a silent duplicate. Unverified.app() binding set in middleware with try/finally. It works for HTTP. It is not obviously safe for console commands, queue workers, or seeders that mutate users — and a job dispatched synchronously inside an inbound request would inherit the flag and skip outbound work silently. Needs a test, not a reading.Organizational risk: 0 stars, 0 dependents, one maintainer, no disclosed security process — for a component that terminates authenticated write traffic and stores API keys.
Key questions
create-user / create-team?SyncLog written (publisher, receiver, or both), is it pruned, and does it contain PII?api_key rotation supported without downtime, and how are keys distributed to N receivers?illuminate/* constraints — does it match our fleet's oldest app?Stack fit
PHP 8.3+ (per the package's own code style), Composer 2, a durable queue backend on the publisher (Redis/SQS/database — not sync), HTTPS between hub and every receiver, and a real secret store for the per-app bearer tokens. The SyncApp model stores api_key encrypted, which implies APP_KEY discipline across the fleet — if receiver and publisher APP_KEYs differ in a shared table scenario, decrypt fails. Prefer app_source = config for the first pilot and database only once key management is settled.
Confirm the illuminate/* constraint against the oldest app in the fleet, not the newest. That constraint, not our PHP version, will decide the rollout order.
Migration path This replaces bespoke glue, so treat it as a cutover with a parity gate, not an install:
sync_fields stays local; document that explicitly or it will be rediscovered as a bug in month three.SyncLog stays truthful) and diff row counts and hashes.SyncLog against legacy sync records for a full business cycle (at minimum: a password change, an email change, a role change, a team add, a team remove).Compatibility
Pin ^1.7 in the publisher and all receivers, and treat publisher/receiver version skew as an unsupported matrix until proven otherwise. Record the resolved version in the lockfile and add a scheduled check that the fleet is converged. Verify hasher driver and cost parity across apps; a bcrypt round mismatch between hub and a receiver means replicated hashes verify in one place and not the other.
Sequencing Sequence as: diff review → authority map → ID strategy → single receiver in staging → transport hardening → dual-run parity → one production receiver → fleet rollout. The two steps teams skip are the dual-run parity window and the ID strategy, and those are precisely the two that produce silent cross-app data corruption rather than a loud failure.
Maintenance
Higher than the package's size suggests, because it is a two-sided protocol. Every upgrade is a coordinated fleet operation, not a composer update in one repo: publisher and receivers must move together, and with an opaque changelog each hop requires a manual diff read. Budget a recurring release-review tax and a version-convergence check. The good news is that the codebase is small, final, strict-typed, and MIT — fork cost is low, which is the main thing that makes the maintenance burden acceptable.
Support GitHub issues, one maintainer, no SLA, no evidence of a security disclosure process, zero dependents — meaning we would be among the first production users and would find the integration bugs. For a component that authenticates inbound writes and holds credentials, "we'll fork it" needs to be a genuine, resourced plan rather than a throwaway line. Name an internal owner for this dependency and give them authority to hold the pin when upstream moves.
Scaling
Fan-out is O(apps) per user change, so a 50k-user import becomes 50k × N queued jobs — the queue, not the database, is the first thing to fall over. Rate-limit outbound calls per receiver and chunk bulk operations. On receivers, roster loading is users × teams; watch for N+1 through the package's relationships and confirm both pivot foreign keys are indexed before the table is large, because retrofitting indexes on a hot membership table is an outage. SyncLog grows monotonically on both sides — define retention and a prune job on day one.
Failure modes
Ramp-up Low for a Laravel developer to use; moderate to own. The concepts engineers must actually internalize are eventual consistency, idempotency, and loop suppression — not Laravel conventions. Ops needs a runbook covering key rotation, queue backlog response, partial-sync reconciliation, and receiver rollback. The reconciliation tooling is ours to build and is not documented upstream; scope it as a deliverable of the integration project, not a follow-up.
Assessment Adopt conditionally, and only as a pilot on one non-critical receiver. This is infrastructure with an auth surface, a credential-replication model, and a multi-app upgrade matrix — it should go through the same review as any service that terminates authenticated traffic, not the review of a helper library. Two gates are non-negotiable before any production cutover: a read of the v1.6.0...v1.7.0 diff, and a written answer on cross-app team/user ID strategy. If the ID strategy turns out to be locally generated IDs with no mapping, the package becomes a reference implementation and we build the mapping layer ourselves — that single answer determines whether this is a two-week integration or a quarter-long platform project.
How can I help you explore Laravel packages today?