unbotable-laravel maintained by unbotable
Unbotable for Laravel
Privacy-respecting bot and spam protection for Laravel forms and logins — no CAPTCHAs for real visitors, no tracking, no third-party data harvesting.
This package is the Laravel client for Unbotable. It talks to the Unbotable service over HTTP, attaches a short-lived signed token to your forms, and verifies it with middleware. There is no account and no API key — you point it at the service and protect a route.
⚠️ Alpha — coming soon
Unbotable is in early testing. The protection works, but it's young: tuning is ongoing, the API may shift before 1.0, and it will not yet catch every bot. It's built to fail open — if anything goes wrong, your forms keep working and real users are never blocked by our bug. Pin an exact version and treat it as defence-in-depth, not a guarantee, until we cut a stable release.
How it works
@unbotableJsloads a tiny script that, in the background, measures device and behaviour signals and asks the Unbotable service for a verdict.- The service returns a signed, 5-minute token (and, for borderline sessions, an invisible proof-of-work the browser solves silently).
- The
unbotablemiddleware verifies the token's signature locally against the service's public key and enforces the verdict.
Raw signals are used for a split second to reach a decision and then thrown away. What little the service stores is one-way and anonymous — you could publish the whole database and not find a single person in it. That's the point.
A local honeypot and timing floor run independently of the service, so even if Unbotable is unreachable you keep a baseline of protection.
Requirements
- PHP 8.2+
- Laravel 12 or 13
Installation
composer require unbotable/unbotable-laravel
The service provider, the unbotable middleware alias, and the Blade directives
register automatically via Laravel package discovery.
Point the package at the service (defaults to the public service):
UNBOTABLE_URL=https://unbotable.com
Optionally publish the config to tune behaviour:
php artisan vendor:publish --tag=unbotable-config
Quick start
Protect a route
use Illuminate\Support\Facades\Route;
Route::post('/login', [LoginController::class, 'store'])->middleware('unbotable');
// During rollout, observe without enforcing:
Route::post('/register', [RegisterController::class, 'store'])->middleware('unbotable:log_only');
Blade forms
Add the script once in your root layout, then drop the honeypot into the form:
{{-- layout <head> or before </body> --}}
@unbotableJs
<form method="POST" action="/login">
@csrf
@unbotableHoneypot {{-- hidden trap field --}}
@unbotableTimestamp {{-- JS-free timing floor --}}
{{-- ...your fields... --}}
</form>
For a plain Blade form with no framework, auto-wire it — the token is attached on submit:
@unbotableWire('#login-form')
Inertia / Vue
Add @unbotableJs to your root Blade layout, then use the composable. The cleanest
way to import it is a Vite alias to the installed package (no copied files to
drift):
// vite.config.js
import { fileURLToPath } from 'node:url'
export default defineConfig({
resolve: {
alias: {
'@unbotable': fileURLToPath(
new URL('./vendor/unbotable/unbotable-laravel/resources/js', import.meta.url)
),
},
},
})
<script setup>
import { useForm } from '@inertiajs/vue3'
import { useUnbotable } from '@unbotable/useUnbotable'
const unbotable = useUnbotable()
const form = useForm({ email: '', password: '', _unbotable_hp: '' })
const submit = async () => {
await unbotable.settled() // let the background assess finish
unbotable.protect(form).post(route('login')) // attaches the token + PoW
}
</script>
<template>
<form @submit.prevent="submit">
<!-- ...your fields... -->
<!-- off-screen honeypot -->
<input type="text" name="_unbotable_hp" v-model="form._unbotable_hp"
tabindex="-1" autocomplete="off" style="position:absolute;left:-9999px" />
</form>
</template>
Configuration
All settings have sensible defaults; override via env or the published config.
| Key (env) | Default | What it does |
|---|---|---|
UNBOTABLE_URL |
https://unbotable.com |
The Unbotable service to verify against |
UNBOTABLE_ON_BLOCK |
fake_success |
On a block verdict: fake_success (redirect back with a flag), abort (422), or log_only |
UNBOTABLE_ON_UNREACHABLE |
open |
If the service is unreachable: open (allow + log, rely on honeypot/timing) or closed (block) |
UNBOTABLE_MIN_SUBMIT_SECONDS |
2 |
Submissions faster than this are treated as bots (JS-free) |
UNBOTABLE_REPLAY_MAX_USES |
3 |
How many times one token may be reused before rejection |
UNBOTABLE_FAKE_SUCCESS_REDIRECT |
null |
Where fake_success redirects (null = back) |
fake_success sets the session key _unbotable_ok so your view can show a
plausible success message to a bot while quietly dropping the submission.
Fail-open by design
If the service times out, errors, or the script never runs, the package sends a
unreachable status and the middleware applies on_unreachable (default:
allow). A genuine block verdict always blocks; only outages fail open. We'd
rather let a borderline request through than block a real person over our own
downtime.
Privacy
No cookies, no third parties, no tracking — not even an analytics beacon. Nothing stored is tied to a person: the service keeps only one-way, anonymous hashes that expire on their own within weeks. See the live data at unbotable.com.
License
MIT — see LICENSE.