loupekit / laravel
Loupe for Laravel — embeddable visual feedback that stores comments in your own database, gates access per user, serves the Loupe dashboard, and exposes the backlog to Claude Code over MCP.
Requires
- php: ^8.4
- illuminate/console: ^11.0|^12.0|^13.0
- illuminate/database: ^11.0|^12.0|^13.0
- illuminate/filesystem: ^11.0|^12.0|^13.0
- illuminate/http: ^11.0|^12.0|^13.0
- illuminate/routing: ^11.0|^12.0|^13.0
- illuminate/support: ^11.0|^12.0|^13.0
Requires (Dev)
- laravel/mcp: ^0.8
- mockery/mockery: ^1.6
- orchestra/testbench: ^9.0|^10.0|^11.0
- phpunit/phpunit: ^11.0|^12.0
Suggests
- laravel/mcp: Enables `php artisan mcp:start loupe` — hands the feedback backlog to Claude Code. Use ^0.8 (supports Laravel 11, 12 and 13).
README
loupekit/laravel
Loupe for Laravel — visual feedback, in your own app.
Your users pin a comment to any element on your live product and capture a screenshot.
Comments are stored in your database, gated to your users, triaged on your dashboard,
and handed to Claude Code over MCP — no separate backend to run.
Website · GitHub · Full guide · Changelog · SDK
Overview
Traditional feedback — "the revenue card looks off on the dashboard" — loses the one thing an engineer needs: which element, in what state, on which page. Loupe captures all of it at the moment of the comment. This package brings that loop into any Laravel app: the widget is a single Blade directive, comments are Eloquent rows in your database, and the triage board is a route you own behind your auth.
Table of contents
- Features
- Requirements
- Install
- Quick start
- How it works
- Authorization — who can use it
- The dashboard
- Claude Code over MCP
- Configuration
- Data model
- Try it locally
- Publishing
- Testing
- Related packages
Features
| 🎯 Click-to-comment inspector | Hover-highlight any element, click to pin a comment — dropped in with one @loupeWidget directive. |
| ▭ Free-region screenshots | Drag a free-size box, screenshot exactly that area, comment on it. Anchors to the element under its center so it tracks reflow and scrolling. |
| 🔁 Redeploy-surviving re-anchoring | A multi-signal fingerprint re-locates the element after the UI changes; if it can't, the pin detaches instead of pointing at the wrong thing. |
| 🗄️ Your database | Comments are an Eloquent Comment model in a loupe_comments table. Swap in your own subclass to add relations/scopes. |
| 🔐 Per-user gating | loupe:use / loupe:admin Gate abilities and config closures decide who sees the widget and who opens the dashboard. |
| 📋 Dashboard on your routes | The full Kanban triage board at /loupe/dashboard, behind your session auth. |
| 🤖 Claude Code over MCP | php artisan mcp:start loupe hands Claude the fully-contextual backlog. |
| 🔑 No secrets to manage | Authenticates with your existing session + CSRF token. No HMAC keys. |
| ✅ 100% tested | A Testbench suite with a hard 100% line-coverage gate, across Laravel 11/12/13. |
Requirements
| Supported | |
|---|---|
| PHP | 8.4 and higher |
| Laravel | 11, 12, 13 |
| Database | anything Eloquent supports (MySQL, PostgreSQL, SQLite, SQL Server) |
| MCP (optional) | laravel/mcp ^0.8 — Laravel 11, 12 & 13 |
Install
composer require loupekit/laravel php artisan loupe:install php artisan migrate
loupe:install publishes the config, migration and browser assets (to public/vendor/loupe),
then publishes and registers an App\Providers\LoupeServiceProvider where you control access.
Quick start
1. Add the widget to your Blade layout, just before </body>:
@loupeWidget </body>
2. Decide who sees it. By default the widget and dashboard are visible only in local.
Open app/Providers/LoupeServiceProvider.php and grant access:
Gate::define('loupe:use', fn ($user) => $user->is_staff); // who sees the widget Gate::define('loupe:admin', fn ($user) => $user->is_admin); // who opens the dashboard
3. Open the board at /loupe/dashboard. That's the whole setup.
How it works
flowchart LR
subgraph App["Your Laravel app"]
W["@loupeWidget<br/>(Loupe SDK)"]
API["/loupe/v1/*<br/>Comment + Blob controllers"]
DASH["/loupe/dashboard<br/>Kanban board"]
DB[("loupe_comments<br/>your database")]
FS[["screenshots<br/>your disk"]]
MCP["php artisan<br/>mcp:start loupe"]
end
CLAUDE["Claude Code"]
W -->|"session cookie + CSRF"| API --> DB
API --> FS
DASH --> API
MCP --> DB
CLAUDE <-->|MCP| MCP
Loading
Identity is always the authenticated session user (auth()->user()); the store endpoint rejects
a comment whose author.id is not the current user, so nobody can post as someone else.
Authorization — who can use it
Two abilities, checked in this order — config closure, then Gate ability:
// Option A — Gate abilities (in the published App\Providers\LoupeServiceProvider) Gate::define('loupe:use', fn ($user) => $user->hasRole('staff')); Gate::define('loupe:admin', fn ($user) => $user->hasRole('admin')); // Option B — config closures (config/loupe.php); take precedence over the Gates 'authorize' => [ 'use' => fn ($user) => $user->can_give_feedback, 'dashboard' => fn ($user) => $user->is_admin, ],
Denied users never receive the widget markup, and the API/dashboard return 403.
The dashboard
The full Kanban board — open / in progress / done, page filter, screenshot thumbnails, status
moves, delete, and Copy for Claude — served at /loupe/dashboard behind your web+auth
middleware and the loupe:admin ability. Configuration is injected server-side, so no secret
ever reaches the browser.
Claude Code over MCP
With laravel/mcp installed, a local MCP server named loupe is registered automatically:
php artisan mcp:start loupe
It reads your database directly (no HTTP hop, no admin key) and exposes three tools:
| Tool | Arguments | Returns |
|---|---|---|
list_comments |
status?, url? |
the backlog, newest first |
get_comment |
id |
Claude-ready package: request + element HTML + computed styles + screenshot (or region rect) |
update_status |
id, status |
marks a comment open / in_progress / done |
Configuration
config/loupe.php (published by loupe:install):
| Key | Default | Purpose |
|---|---|---|
enabled |
true |
Master switch. |
path |
loupe |
Route prefix for the API + dashboard. |
project_key |
app |
Scopes comments (one app = one project). |
middleware.api |
['web','auth'] |
Guards the JSON API. |
middleware.dashboard |
['web','auth'] |
Guards the dashboard. |
authorize.use / authorize.dashboard |
null |
Closures fn($user): bool (take precedence over Gates). |
user_resolver |
null |
Customize the {id,name,email} payload sent to the SDK. |
comment_model |
Loupekit\Loupe\Models\Comment |
Swap for your own subclass. |
disk |
public |
Filesystem disk for screenshots. |
See the full guide for Sanctum/SPA setups, private screenshot disks, and the complete reference.
Data model
Migration create_loupe_comments_table → loupe_comments:
| Column | Type | Notes |
|---|---|---|
id |
string (PK) | client-generated UUID |
project_key |
string, indexed | scopes to this app |
url |
text | normalized (utm / click ids stripped) |
status |
string, indexed | open · in_progress · done |
body |
text | the comment |
kind |
string | element · region |
author / author_id |
json / string | {id,name,email?} + denormalized id |
anchor / context / offset |
json | fingerprint, element HTML + styles, pin position |
region |
json, nullable | rectangle for region comments |
screenshot_url |
text, nullable | URL of the stored screenshot |
created_at / updated_at |
timestamps |
Try it locally
Point a scratch Laravel app at this package with a path repository:
// composer.json of your test app "repositories": [ { "type": "path", "url": "../loupe/packages/laravel" } ]
composer require loupekit/laravel:@dev php artisan loupe:install && php artisan migrate # add @loupeWidget to resources/views/…, log in, and open /loupe/dashboard
Publishing
This package lives in the Loupe monorepo under packages/laravel. Packagist reads a
repo's root composer.json, so it's mirrored to a dedicated repo automatically by
.github/workflows/laravel-split.yml:
- every push to
mainsyncs the split repo'smain; - every
vX.Y.Ztag is forwarded to the split repo → Packagist auto-updates.
The same tag also drives the npm release (@loupekit/*), so one vX.Y.Z tag ships the
npm packages and the Packagist package together.
One-time setup:
- Create the target repo (default
loupekit/laravel; override via theLARAVEL_SPLIT_ORG/LARAVEL_SPLIT_REPOrepository variables). - Add a Personal Access Token with
reposcope as theACCESS_TOKENsecret. - Submit the split repo once at packagist.org/packages/submit and enable Auto-update (the Packagist GitHub webhook).
After that, releasing is just: git tag -a vX.Y.Z && git push --tags.
Testing
composer install composer test # run the suite composer test:coverage-100 # run with the hard 100% coverage gate
Every push runs the suite across Laravel 11/12/13 in CI
(.github/workflows/laravel.yml).
The browser bundles in resources/dist are vendored from @loupekit/sdk and
@loupekit/dashboard; refresh them with bin/sync-assets.sh after changing either.
Related packages
- @loupekit/sdk — the embeddable widget (bundled here).
- @loupekit/mcp — the standalone MCP server.
- @loupekit/shared — canonical types +
normalizeUrl.
Author
Created and maintained by Mohamed Ashraf Elsaed — LinkedIn · GitHub · m.ashraf.saed@gmail.com