fabianwennink/iconcaptcha
IconCaptcha is a lightweight, self-hosted PHP captcha that replaces reCAPTCHA with a privacy-friendly icon-based challenge. No external services, no tracking, fully customizable, and easy to integrate—ideal for GDPR-conscious sites.
IconCaptcha is a framework-agnostic PHP + JavaScript package. In Laravel you install the PHP backend via Composer and the front-end bundle separately, then wire the two together yourself — there is no first-party Laravel service provider to lean on. Budget an hour for the initial integration.
composer require fabianwennink/iconcaptchanpm install iconcaptcha or use the CDN build). Confirm the major version matches the PHP package.public/vendor/iconcaptcha/icons, and record both the filesystem path and the public URL.config/iconcaptcha.php and a small service provider that configures the library once at boot.web middleware group so sessions work.Where to look first: the README's "Basic usage" section, the examples/ directory in the repo, the JS package docs for front-end options, and src/IconCaptcha.php for the public methods (handleRequest(), getPayload(), validate() in v4). Start with a single contact form: render the widget, point it at your endpoint, add the validation rule, and test a correct selection, a wrong selection, and an expired challenge.
Keep one dedicated endpoint controller. Make it invokable and minimal. If the library returns a response, return it; if it writes/echoes directly, capture with output buffering and wrap in JsonResponse. Don't let Laravel's exception handler convert errors into HTML redirects — the JS expects JSON.
Configure once in a service provider. Use env-backed config (ICONCAPTCHA_ICON_PATH, ICONCAPTCHA_ICON_URL, ICONCAPTCHA_STORAGE) and call the library's configuration method in boot(). Avoid scattering options across controllers.
Render through a Blade component. A <x-iconcaptcha /> component that outputs the container, includes the JS once via @once/@push('scripts'), and initialises with route('iconcaptcha.endpoint') keeps forms clean and consistent.
Wrap validation in a custom rule. Implement ValidationRule and call the library's validate(). Attach it to the FormRequests for login, registration, and contact forms so it composes with Laravel's normal error handling and old-input flow.
Use session storage by default, but plan for scale. The session driver is the path of least resistance on a single server. On load-balanced or multi-server deployments, implement or configure a shared storage driver (Redis/DB) through the library's storage interface, or ensure sticky sessions.
Add middleware deliberately. Apply throttle to the endpoint yourself — the package does not ship rate limiting. Decide how to handle CSRF: either configure the JS to send X-CSRF-TOKEN from a meta tag, or exclude the endpoint and accept the trade-off.
Handle Livewire and Inertia. For Livewire, wrap the widget in wire:ignore so DOM morphing doesn't destroy it. For Inertia, load the script once in the root layout and re-initialise on page visits; otherwise the widget works only on the first load.
Disable it in tests. Add an ICONCAPTCHA_ENABLED flag and bypass validation in the test environment, or bind a fake validator in the container. Otherwise every form test needs a real challenge.
Theme with CSS variables or overrides. Keep the icon set and colour scheme consistent with your brand; the widget is designed to be customised rather than used stock.
composer.json and package.json.config/session.php and your driver.dump(), or a dd() before the response breaks the widget. Use logs, not debug output, in the endpoint.api routes have no session by default; web routes have CSRF. Pick deliberately and test the actual request from the browser.application/json. This catches most integration mistakes.How can I help you explore Laravel packages today?