Search by

asdrubalp9 / laravel-changelog

Drup9

Role-targeted changelog entries with per-user read tracking for Laravel.

Package info

gitlab.com/asdrubalp9/laravel-changelog

Issues

pkg:composer/asdrubalp9/laravel-changelog

Statistics

Installs: 11

Dependents: 0

Suggesters: 0

Stars: 0

v0.2.0 2026-10-04 13:44 UTC

This package is auto-updated.

Last update: 2026-10-04 16:46:45 UTC


README

Publish role-targeted changelog entries and track what each user has read. Backend only: you build the interface.

Installation

composer require asdrubalp9/laravel-changelog
php artisan vendor:publish --tag=changelog-config
php artisan vendor:publish --tag=changelog-migrations
php artisan migrate

Set key_type, morph_key_type and scope_type to uuid before you migrate if your users table uses uuid keys. The package works on PostgreSQL, MySQL and SQLite.

Minimal configuration

Define the ability the administration routes check. Without it, every administration route responds 403:

Gate::define('manage-changelog', fn (User $user) => $user->hasRole('platform-admin'));

The default routes.user_middleware and routes.admin_middleware are ['api', 'auth:sanctum']. Override both if you do not use Sanctum.

Audience

An entry lists the roles that can see it. A reader sees an entry if they have any of those roles. An entry with no roles is visible to everyone.

The package does not know your role system. It asks ResolvesAudience. The default implementation calls getRoleNames() when the user model has it (spatie/laravel-permission) and returns no roles otherwise.

$this->app->bind(ResolvesAudience::class, MyAudience::class);

knownRoles() returns the valid role names, or null to skip validation. Role names are stored as text. If you rename a role, entries that list the old name stop being visible and nothing reports it. Return your roles from knownRoles() so the administration API rejects a typo with a 422.

Markdown

body is stored as Markdown. Set render_html to true to also receive body_html. The package removes raw HTML and unsafe links (javascript:) and does not let you turn that off. render_html needs league/commonmark, which laravel/framework already requires.

API

User routes, under routes.prefix (default changelog):

MethodPathResult
GETentriesPage of visible entries with read
GETunread-count{ "data": { "unread": 3 } }
POSTentries/{id}/read204. 404 if not visible
POSTread-all{ "data": { "unread": 0 } }

Administration routes, under admin/:

MethodPathResult
GETentriesList, filter with ?status=draft\|scheduled\|live\|expired
GETentries/{id}Detail with roles
POSTentries201
PUTentries/{id}200, JSON
DELETEentries/{id}Soft delete
POSTentries/{id}/publishSets published_at to now unless already live
POSTentries/{id}/unpublishBack to draft
POSTentries/{id}/imageUpload or replace, multipart
DELETEentries/{id}/imageRemove

The image goes through POST. PHP does not populate uploaded files for a multipart PUT.

Image URLs

By default image_url is the public URL of the disk (Storage::url()). For a private bucket, set image.url to temporary. The package then returns a signed URL that expires after image.url_ttl minutes (60 by default):

CHANGELOG_IMAGE_DISK=s3
CHANGELOG_IMAGE_URL=temporary
CHANGELOG_IMAGE_URL_TTL=60

The disk must be able to sign URLs. s3 can. Any other disk needs buildTemporaryUrlsUsing(). With a disk that cannot sign, every response that includes an entry with an image fails with changelog_image_disk_cannot_sign (HTTP 500), and that includes the list endpoint. Check the disk before you turn the mode on.

Any value of image.url other than temporary behaves as public. A typo does not raise an error.

A signed URL stops working when it expires. Reload the list to get a new one.

Dates

published_at and expires_at are normalized to app.timezone before they are stored. Laravel assumes app.timezone equals the database session timezone. Keep both in UTC.

Scopes

changelog_reads.scope_id holds a value from the ResolvesReaderScope contract. It lets your app write a row-level security policy without a join. The package never reads it.