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.

v0.3.1 2026-07-11 23:07 UTC

This package is auto-updated.

Last update: 2026-07-11 23:09:15 UTC


README

Loupe — Pin feedback to the live UI. Hand it to Claude.

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.

Packagist version Packagist downloads PHP version Laravel 11, 12, 13 100% test coverage MIT license

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.

Pin a comment to any element

Table of contents

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.

Triage board

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
Hand the backlog to Claude

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_tableloupe_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 main syncs the split repo's main;
  • every vX.Y.Z tag 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:

  1. Create the target repo (default loupekit/laravel; override via the LARAVEL_SPLIT_ORG / LARAVEL_SPLIT_REPO repository variables).
  2. Add a Personal Access Token with repo scope as the ACCESS_TOKEN secret.
  3. 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

Author

Created and maintained by Mohamed Ashraf ElsaedLinkedIn · GitHub · m.ashraf.saed@gmail.com

License

MIT © Mohamed Ashraf Elsaed