phoenix1331/laravel-jev-moderation
A Laravel moderation package that lets you flag, review, and approve or reject user-generated content with configurable rules, queues, and status tracking. Integrates cleanly with Eloquent models so you can add moderation workflows to posts, comments, and more with minimal setup.
Moderatable trait plus #[Moderate] attribute means moderation is an annotation on existing models, not a re-architecture. Scope this as an enabler for the whole UGC roadmap rather than a line item under one feature.post vs pre mode is a product decision, not an engineering one: pre-moderation trades time-to-publish and user trust for safety and support load; post-moderation trades safety for activation and engagement. Being able to flip modes per environment (JEV_MODERATION_MODE) means you can run a staged rollout — start strict in a beta cohort, loosen on evidence — and measure the cost of each.JEV_MODERATION_FAIL forces the organisation to say out loud what happens when the vendor is down. For a kids' platform or a marketplace with regulatory exposure, the answer differs from a hobby forum. Getting this onto a decision log before launch, rather than after an incident, is a governance win.jev-moderation:stats returns flags and blocks per category and, crucially, how often moderators upheld automatic decisions. That is the input to "do we need one moderator or three?", "is our community policy over-flagging?", and "which categories are eating reviewer time?" It converts a staffing question into a measurable one.community, chat, strict), per-category review_above/block_above thresholds, and an uncertain_below confidence floor are configuration. Product can own tone and strictness per surface — a chat product and a marketplace need different tolerances — and jev-moderation:rescan lets you re-evaluate history after a threshold change, with a billing warning built in. That is an unusually mature loop for a v0.1.DecisionsAreAppendOnly), policy version hashes, answering model version, request ids and optional content hashes give you a defensible record of why a piece of content was hidden and who saw what. This is what turns "the AI blocked it" from an unanswerable support ticket and a legal liability into a reviewable decision. It supports appeals workflows, DSA-style transparency obligations, and enterprise/customer trust questionnaires.sync mode exists but is explicitly retry-less, store_content defaults off to limit data retention, and rescan asks before spending money. That makes moderation cost a per-post variable you can model against ARPU and abuse rates before committing to a launch volume.moderation_status column (jev-moderation:add-column) backfills from decision history and switches visibility scopes from joins to an indexed column. This is a deliberate "cheap now, fast later" path — you can launch on the simple schema and take the performance migration when volume justifies it, without a rewrite.ModerationDriver contract, JevModeration::extend(), and JevModeration::fake() mean Jev is a swappable component, not a foundation. That materially weakens the "what if the vendor raises prices / pivots / is acquired" objection, and lets you A/B a second provider on the same decision schema later.flagForReview(), approve(), reject(), moderateNow(), the ContentReported/ReviewResolved events and the query scopes are the backend for a moderator console. Product can plan the console as a thin UI over an already-specified API, and prioritise it against automated-only moderation with clear evidence of where automation falls short.Adopt when:
FakeDriver with classification assertions makes "content gets blocked" a testable requirement rather than a manual QA step, which matters if moderation is a launch gate.Look elsewhere, or defer, when:
For executives — one paragraph. We can add AI content moderation to the product as a configuration exercise rather than a platform build. This package gives us the three things that are normally expensive: an automatic check on user content, a recorded, tamper-proof decision history we can show a regulator, customer or auditor, and a moderator workflow with reports, approvals and rejections. Estimated effort is days of integration, not a quarter of engineering, and the classifier is a metered vendor API we can swap out — so we are not locked in on price or roadmap. The two decisions we need from leadership are policy, not technology: how strict we are per surface, and what happens when the vendor is unavailable.
For engineering — the short version. A Moderatable trait plus a #[Moderate] attribute hooks every save path — HTTP, jobs, Livewire, seeders — and appends a pending decision; unchanged content and unrelated column updates are skipped, so it does not fire on noise. A queued job classifies after commit, with retry classification that distinguishes 429/529/5xx/timeouts from permanent failures, and records a failed decision when it gives up rather than throwing into the request path. Visibility is enforced by a global scope driven by config (mode, fail), with escape hatches (withoutModeration(), onlyFlagged()) and a per-model moderation_status column option to drop the join at scale. Decisions are append-only by exception — DecisionsAreAppendOnly — which is the right call and means corrections are new rows, not updates. Everything external is behind a ModerationDriver contract, and JevModeration::fake() plus assertClassified() means the moderation path is unit-testable with no network. jev-moderation:install, :ping, :rescan and :stats cover setup, credentials diagnosis, re-evaluation and reporting.
For the trust & safety / community lead. You get thresholds you can change without a deploy, three policy presets as starting points, per-category tuning, a confidence floor for "flag it, do not decide it," and a stats view that tells you how often your team overrode the machine. rescan lets us re-run history after a policy change, with a cost prompt. flagForReview, approve and reject are the primitives for a review console; reports on already-blocked content are recorded without resurrecting it.
For finance and legal. Per-check API cost is a per-post variable; store_content defaults off so we retain hashes, not user text, unless we choose otherwise. Decisions carry a hash of the policy and the model version that produced them, which is the artefact appeals, audits and enterprise security reviews will ask for.
The caveats to state plainly. This is a v0.1.0 release; it is text-only; it provides no moderator UI; and it depends on a single external classifier whose commercial terms and reliability are outside our control. We should pin the version, keep the driver interface as our boundary, and treat "vendor is down" as a rehearsed scenario rather than an assumption. Subject to those, it is a low-cost, high-leverage way to make moderation a product feature instead of a future blocker.
How can I help you explore Laravel packages today?