zipavlin / redline
Laravel package for Redline - a localized, on-site, visual bug reporting.
Fund package maintenance!
Requires
- php: ^8.3|^8.4
- composer-runtime-api: ^2.0
- ezyang/htmlpurifier: ^4.19
- firebase/php-jwt: ^7.1
- illuminate/contracts: ^12.41.1||^13.0
- illuminate/database: ^12.41.1||^13.0
- laravel/mcp: ^1.0
- symfony/process: ^7.4.5||^8.0.5
Requires (Dev)
- larastan/larastan: ^3.0
- laravel/pint: ^1.14
- nunomaduro/collision: ^8.8
- orchestra/testbench: ^10.0||^11.0
- pestphp/pest: ^4.4.1
- pestphp/pest-plugin-arch: ^4.0
- pestphp/pest-plugin-laravel: ^4.1
Suggests
None
Provides
None
Conflicts
None
Replaces
None
README
A Laravel package for self-hosted, on-page bug-reporting, with a reporting widget and dashboard.
Testers can pin comments, annotate screenshots, record videos, and view existing
pins without leaving the page; developers review and resolve reports in a built-in
dashboard. All data lives in your own storage (SQLite by default).
Requirements
| PHP | 8.3 or 8.4 |
| Laravel | 12.41.1+ or 13.x |
| Extension | pdo_sqlite (for the default storage backend) |
Get started
composer require zipavlin/redline php artisan redline:install
Interactive redline:install walks you through the storage backend, auth provider, guard,
mail setup, and disabled features, writes your choices to .env, publishes config and assets,
and creates a first (admin) user.
Activation
The package is partially gated - know what turns on where:
- Publishing and the artisan commands (
redline:install,redline:update,redline:user,redline:remove) are always available. - The dashboard (SPA at
/{redline.path}, default/redline) is gated by the guard - The API routes and widget injection are also gated by the guard (default when
APP_ENV=staging)
Configuration
All settings are read from config/redline.php, which maps to these env variables:
| Variable | Default | Purpose |
|---|---|---|
REDLINE_PATH |
redline |
URL prefix for all routes (/redline/api/..., /redline) |
REDLINE_STORAGE_PATH_BASE |
redline |
Base dir under storage/app/ |
REDLINE_STORAGE_PATH_SCREENSHOTS |
screenshots |
Screenshot subdir |
REDLINE_STORAGE_PATH_VIDEOS |
videos |
Video subdir |
REDLINE_STORAGE_PATH_DATABASE |
database |
SQLite file subdir |
REDLINE_GUARD_PROVIDER |
Zipavlin\Redline\Providers\Guard\EnvironmentGuardProvider |
When Redline is active (LaravelGuardProvider = host app user logged in) |
REDLINE_GUARD_EXCLUDE |
(none) | Comma-separated paths to skip injection on (exact, subpath, /*, glob) |
REDLINE_AUTH_PROVIDER |
Zipavlin\Redline\Providers\Auth\OpenAuthProvider |
Registration/role model |
REDLINE_AUTH_TOKEN |
3600 |
JWT TTL (seconds) |
REDLINE_AUTH_REMEMBER_TOKEN |
2592000 |
"Remember me" JWT TTL (30 days) |
REDLINE_AUTH_GUEST |
false |
Allow anonymous report submission |
REDLINE_AUTH_GUEST_ORIGINS |
(none) | Comma-separated origins allowed to submit guest reports |
REDLINE_AUTH_GUEST_THROTTLE |
20 |
Guest submissions per minute per IP |
REDLINE_AUTH_ALLOW_SELF_ADMIN |
false |
Allow public registration to claim admin (off by default) |
REDLINE_AUTH_PIN_REPORTER |
(none) | PinAuthProvider: reporter PIN |
REDLINE_AUTH_PIN_DEVELOPER |
(none) | PinAuthProvider: developer PIN |
REDLINE_AUTH_DOMAINS |
(none) | DomainAuthProvider: allowed email domains (comma-separated) |
REDLINE_AUTH_THROTTLE |
10 |
Auth endpoints rate limit (per minute per IP) |
REDLINE_BOOTSTRAP_THROTTLE |
120 |
Bootstrap endpoint rate limit (per minute per IP) |
REDLINE_COOKIE_SECURE |
true |
Secure flag on auth/CSRF cookies |
REDLINE_COOKIE_SAME_SITE |
lax |
SameSite on auth/CSRF cookies |
REDLINE_COOKIE_DOMAIN |
(none) | Cookie domain override |
REDLINE_ALLOWED_ORIGINS |
(none) | Escape hatch for cookie-mutation Origin checks (reverse proxies) |
REDLINE_FEATURES |
(none) | Comma-separated features to enable (additive - all off until listed): screenshots, videos, files, comments, mcp |
REDLINE_MAIL |
true |
Kill switch for all Redline emails |
REDLINE_VITE_URL |
(none) | Dev-only: serve the frontend from the Vite dev server (see "Frontend development") |
Keep the data out of your app's git history:
/storage/app/redline/should be gitignored.
Auth providers & roles
Roles are reporter / developer / admin (plus a shared guest identity for
guest-mode submissions). How roles are claimed depends on the auth provider:
| Provider | How users get in | Role selection |
|---|---|---|
OpenAuthProvider (default) |
Anyone can register | Free choice (admin excluded unless REDLINE_AUTH_ALLOW_SELF_ADMIN=true) |
PinAuthProvider |
Anyone can register with a PIN | PIN decides: reporter PIN → reporter, developer PIN → developer; role selector hidden |
DomainAuthProvider |
Registration limited to REDLINE_AUTH_DOMAINS (+ email verification) |
Provider-defined |
ApprovalAuthProvider |
Self-register, then admin approves | Pending until approved |
ClosedAuthProvider |
No self-registration - admin creates users | Admin assigns |
LaravelAuthProvider |
Host application session - first login provisions a redline reporter; no form (empty-body POST /auth/login), login_mode=session |
Fixed: reporter |
WARNING:
LaravelAuthProvidertrusts the host app's email ownership: anyone who can obtain a host account with a given email inherits the matching redline identity. Do not pair it with host registration open to untrusted users.
Notes:
- If no admin exists, the first developer to register is auto-promoted to admin.
- With the default
REDLINE_AUTH_ALLOW_SELF_ADMIN=false, public registration can never create an admin - seed the first one with:php artisan redline:user
- Authentication is a JWT (HS256) delivered as an HttpOnly
redline_tokencookie. State-changing cookie-authenticated requests must echo the non-HttpOnlyredline_csrfcookie back in theX-Redline-CSRFheader (double-submit CSRF). Cross-origin clients useAuthorization: Bearerand skip CSRF.
Capabilities by role
| Action | Reporter | Developer | Admin |
|---|---|---|---|
| Submit reports / comment | ✅ | ✅ | ✅ |
| Edit own report/comment body | ✅ | ✅ | ✅ |
| Change report status | limited¹ | ✅ | ✅ |
| Change report priority | limited¹ | ✅ | ✅ |
| Set due date | limited¹ | ✅ | ✅ |
| Move pins, upload media | own only | ✅ | ✅ |
| Delete reports/comments | own only | own only | ✅ |
| Manage users | ❌ | ❌ | ✅ |
¹ A report owner may only Reopen (resolved → in progress) or Confirm (resolved → done), and may set priority or a due date on their own reports only; developers/admins drive the main lifecycle transitions and may set priority or a due date on any report.
Using the widget
The widget appears as a floating toolbar (bottom-right by default) on every page of the activated environment. Click any mode button - on first use you are prompted for identity (name/email/role via the identity modal).
- Comment / pin - click any element to pin a comment
- Screenshot - captures the page, then lets you crop and annotate
- Video - records the current tab via
getDisplayMedia(no audio; 2 min max) - View - toggle pin visibility: all / open reports
- Identity chip - settings
Dashboard
https://your-staging-site.com/redline
The dashboard is a Vue SPA served at /{redline.path} (default /redline).
It lists reports and report details, users and user details and simple analytics dashboard.
MCP server
An MCP (Model Context Protocol) server is exposed at
https://your-staging-site.com/redline/api/mcp
(any path prefix lives at /{redline.path}/api/mcp). It lets AI agents read and write bug
reports through the same permission model the dashboard and REST API use. The wire-level
tool contract (inputs, outputs, error semantics) lives in
redline-app's contracts/mcp.md.
Enabling it
The mcp feature is off until listed - features are additive:
php artisan redline:enable # list all features and state php artisan redline:enable mcp # enable; writes REDLINE_FEATURES php artisan redline:enable --disable=mcp
Or directly in .env: REDLINE_FEATURES=... , mcp. The route exists only while the feature
is enabled; with it off, clients get an ordinary 404 (also true after a route:cache -
see §Route caching).
Tokens - bearer only
Agent access needs a long-lived rlm_ API token, created on the server only (there are
no HTTP token endpoints):
php artisan redline:mcp # interactive wizard (default) php artisan redline:mcp --user=me@example.com --name="Claude" php artisan redline:mcp --list --user=me@example.com php artisan redline:mcp --revoke={id} php artisan redline:mcp --revoke-all --user=me@example.com
The token carries the owner's FULL permissions (no scopes) and is delivered over the
Authorization: Bearer header only - never from a cookie or a query string. Its lifecycle:
--revoke/--revoke-all- soft revocation; later uses get 401.- That user's password change invalidates every outstanding token.
- A plain logout does NOT invalidate it (a token is not a session credential).
- Pending/guest users can never be issued or use a token.
Tokens are operator-managed with no expiry: revocation is CLI-only
(redline:mcp --revoke), a password change is the only automatic
invalidation, and a token outlives a session - it survives every logout,
including under LaravelAuthProvider. One residual note: that same rlm_
bearer works on all protected endpoints, which are not individually
rate-limited (only the MCP route is).
The plaintext is shown once at creation. Quoting redline:mcp's warning verbatim:
This token acts with the user's full permissions - treat it as a password; it is NOT invalidated by logout; it IS invalidated by that user's password change.
Tools
Eight tools ship with model-first snake_case names: report_list, report_get,
screenshot_get, comment_list, user_list (reads; screenshot files above ~2 MB come back
as metadata instead of an inline image) and report_create, comment_create,
report_update_status (writes). Writes run through the same code path as the HTTP
endpoints, so sanitization, the acceptor assignment and the new-report notifications behave
identically on both surfaces. user_list returns the compact roster (id, name, role; the
email joins only for developer tokens) an agent needs to resolve people for member
assignment, under the same visibility as the user list endpoint. There is no
screenshot/video upload tool - attach media through the REST endpoints with the same bearer
token; URLs returned by the reads also work with plain Authorization: Bearer.
When filing a report, report_create takes an element CSS selector and/or a pin
position: prefer element (a short, unique document.querySelector selector, max 512
characters) when you can inspect the DOM, and use pin percentages when the issue concerns
an area rather than one specific element.
Client configuration
Run php artisan redline:mcp bare: after issuing the token it prints your
client's ready-to-paste setup block with the real endpoint and token inline
(Claude Code, OpenCode, Codex, PI, Cursor, VS Code and Gemini CLI are
confirmed presets, verified against their documented remote-MCP shapes in
2026-10). The endpoint is always {app}/redline/api/mcp - with APP_URL
unset the block prints a <YOUR-APP-URL> placeholder plus a note instead of
a broken URL.
Every listed client sends the Authorization header per request on the
streamable-HTTP transport, which is why the backend's cookie-source-rlm_
401 rule matters - none of them ever send rlm_ via cookies.
Keep the token out of committed files either way - it is a password. For the
env-var style clients (Codex, PI, Cursor, Gemini CLI) the token goes in an
env var only, nothing inline; VS Code's file config never carries the token
at all (an inputs promptString supplies it, or its CLI alternative with
REDLINE_MCP_TOKEN), and Cursor's ${env:} header reference carries a
caveat against unresolved values in current builds.
Artisan commands
# Interactive setup (storage, auth, mail, features) - writes .env, publishes assets php artisan redline:install # Upgrade an existing install after a composer update (migrations + assets) php artisan redline:update # Create a user / grant admin php artisan redline:user # List / enable / disable features (writes REDLINE_FEATURES) php artisan redline:enable # Interactive token wizard / create / list / revoke MCP API tokens php artisan redline:mcp # Permanently delete all data and remove redline from project php artisan redline:remove
Route caching
The package supports php artisan route:cache: its routes load during the
artisan run, the MCP route included. Three things to know:
- The guard is enforced per request across the package's whole API + web +
MCP surface by the
RedlineActivemiddleware (404 on API endpoints and the MCP endpoint, 403 on the dashboard shell), not at boot - at boot it only ever gated the dashboard shell. That is what makes a cache environment-portable: a cache built on staging and served on a production host still refuses the public surface (plain404) on the production host instead of exposing the API there. php artisan optimizerunsroute:cachein standard deploy scripts, so a cache compiled while the feature list, guard or path config differed must be re-run after changing any of them (orredline:update, which clears the caches).- Enforcement that reads per-request context is never baked into the cache:
the guard re-check happens in
RedlineActiveon every request, and themcpfeature re-check happens inRedlineMcpEnabled- which is why a feature turned OFF after caching still reports a plain404on the MCP endpoint instead of serving a stale server.
The MCP route follows the guard like every other redline surface, and the
registration is wrapped in a route GROUP so the gates lead EVERY shape
laravel/mcp creates for the URI - the POST server route, plus the vendor's
unnamed GET/DELETE probes whose closures answer 405 themselves and which
carry no other middleware. On a host where the guard is inactive the
endpoint answers a bare 404 for all three shapes - with AND without a route
cache - leaving no trace (no rate-limiter side effect, no authentication
attempt, and neither laravel/mcp's 400 header validation nor its 405 probe
is reachable). The mcp feature gate rides the same group, so turning the
feature off after a cache was compiled 404s all three shapes too. While the
mcp feature is on and the guard active, the route keeps round-tripping a cache
as before. As before, re-run route:cache after changing features/guard/path
config.
My Redline routes return 404
Work through these in order - the first two cover almost every report:
- Is the feature on? Features are additive, so a fresh install has none
enabled and
{path}/api/mcpis never registered at boot. Runphp artisan redline:enableto see the state, addmcpif it is missing (php artisan redline:enable mcp), then re-run the route cache. - Stale compiled cache. Re-run
php artisan route:clear(orroute:cache/optimize) after any change toREDLINE_FEATURES,REDLINE_GUARD_PROVIDERorredline.path- the cache pins the route set it was built from, andoptimizerunsroute:cachein standard deploy scripts. - Guard reality check. The default
EnvironmentGuardProvideris active only whenAPP_ENV=staging. On a host where the guard is intentionally inactive, redline deliberately answers404on all surfaces, MCP included - that is the guard working as designed, not a broken route. SetREDLINE_GUARD_PROVIDERif the host is meant to serve redline. - Auth, not the host session, authorises MCP. Under a session-based guard
(
LaravelGuardProvider) the endpoint is authorised per request by the bearerrlm_token fromphp artisan redline:mcp; an absent host user is not a route failure.
Storage
Eloquent models on a dedicated SQLite connection at
storage/app/redline/database/redline.db. Requires pdo_sqlite. The schema
(numbered migrations 0001–0007) is created by php artisan redline:install.
Security considerations
- Widget injection and the API are gated by the guard (
APP_ENV=stagingby default); publishing and artisan commands remain available for operational use. - JWT auth cookie is HttpOnly (JS can never read it) and never returned in JSON
bodies; double-submit CSRF via the
redline_csrfcookie +X-Redline-CSRFheader. - Guest reporting is opt-in (
REDLINE_AUTH_GUEST) with configured-origin checks and a per-IP throttle;REDLINE_ALLOWED_ORIGINSis the reverse-proxy escape hatch. - Uploaded files are renamed to
{ulid}.{ext}(original names discarded) and served only through a controller, never via public URLs. - Report/comment bodies are sanitized with
htmlpurifier. - Public registration can never create an admin by default (
REDLINE_AUTH_ALLOW_SELF_ADMIN=false).
Frontend development
All frontend source lives in the sibling redline-app repo (Vue 3 + TypeScript + Vite + Nuxt UI). To develop against this package locally:
- In
redline-app:npm ci && npm run dev(Vite dev server on:5173, serves both widget and dashboard entries as ESM). - In the host app set
REDLINE_VITE_URL=http://localhost- the blade shells then loadsrc/widget.ts/src/dashboard.tsdirectly from Vite with hot reload.
Production bundles are built in redline-app (npm run build → dist/), copied into
this repo's public/ (committed), and published via
php artisan vendor:publish --tag=redline-assets.
Testing
composer test
Current baseline: 699 passed, 0 skipped (2653 assertions).
Changelog
See CHANGELOG.md.
Credits
License
The MIT License (MIT). See LICENSE.md.