spatie/ignition
Spatie Ignition is a beautiful, customizable error page for PHP apps, with a dark mode and simple registration. Laravel, Symfony, and Drupal integrations are available.
For a Laravel developer, the important thing to internalize up front is that you almost never install spatie/ignition directly — Laravel ships spatie/laravel-ignition (which wraps this core package), and that's what renders the error page you already see locally. So "getting started" here is less about composer require and more about learning the extension points this package exposes so you can bend the error page to your app's needs.
The first place to look is the exception page itself: throw something in a local route and note the two things you can customize — the "Solutions" panel and the Flare reporting pipeline.
Minimal mental model:
// What laravel-ignition effectively does under the hood:
\Spatie\Ignition\Ignition::make()
->applicationPath(base_path()) // trims base path from stack traces
->setTheme('dark') // optional
->shouldDisplayException(app()->isLocal())
->register();
Your first practical use case: teach the error page to solve your own domain exceptions. Any exception in your app can implement ProvidesSolution so that instead of a raw stack trace, you get a title, description, and documentation link right in the browser:
use Spatie\Ignition\Contracts\ProvidesSolution;
use Spatie\Ignition\Contracts\Solution;
class TenantNotProvisionedException extends Exception implements ProvidesSolution
{
public function getSolution(): Solution
{
return new TenantNotProvisionedSolution($this->tenantId);
}
}
That's the highest-ROI first step — it turns recurring "what does this even mean?" exceptions into self-documenting ones.
Use ProvidesSolution when the fix is intrinsic to the exception. Good candidates: misconfiguration, missing env vars, invalid credentials, deprecated API usage. Return a Solution with title, description, and getDocumentationLinks() pointing at your internal runbook.
class MissingWebhookSecretException extends Exception implements ProvidesSolution
{
public function getSolution(): Solution
{
return new class implements Solution {
public function getSolutionTitle(): string { return 'Webhook secret not configured'; }
public function getSolutionDescription(): string {
return 'Set WEBHOOK_SECRET in .env and run `php artisan config:clear`.';
}
public function getDocumentationLinks(): array {
return ['Webhook setup' => 'https://wiki.internal/webhooks'];
}
};
}
}
When you can't modify the exception class (vendor code, framework exceptions, third-party SDKs), implement HasSolutionsForThrowable. This is where you get leverage: a provider can pattern-match on message/class and return contextual guidance.
class StripeSolutionProvider implements HasSolutionsForThrowable
{
public function canSolve(Throwable $t): bool
{
return $t instanceof \Stripe\Exception\ApiErrorException
&& str_contains($t->getMessage(), 'No such customer');
}
public function getSolutions(Throwable $t): array
{
return [new StripeCustomerMissingSolution()];
}
}
Register it in the Laravel service provider bootstrap (in laravel-ignition you register providers via the package's config, or in bare Ignition via addSolutionProviders([...])). This is the pattern to standardize across a team: one provider per external system that frequently bites you (Stripe, S3, Redis, queue workers).
OpenAiSolutionProvider is genuinely useful for novel errors, but it means sending exception context off-box. Treat it as a local-only convenience:
$ai = new OpenAiSolutionProvider(config('services.openai.key'));
$ai->applicationType('Laravel ' . app()->version());
$ai->useCache($psr16Cache, 3600); // avoid repeat billing for the same error
Key workflow tips:
useCache) in local dev — you'll hit the same error dozens of times while debugging.applicationType() — it measurably improves suggestion quality.app()->isLocal().The error page is local; the production counterpart is Flare reporting. The pattern to follow:
->runningInProductionEnvironment(app()->isProduction())
->sendToFlare(config('services.flare.key'))
->configureFlare(function (Flare $flare) {
$flare->context('Tenant', tenant()?->id);
$flare->group('Deploy', ['commit' => config('app.commit'), 'env' => app()->environment()]);
$flare->censorRequestBodyFields(['password', 'token', 'card_number']);
$flare->anonymizeIp();
})
context() for single key-value pairs that you want surfaced prominently (tenant, user, request id).group() for a bundle of related diagnostics (deploy metadata, feature flags).censorRequestBodyFields(['password']). Add your API tokens too.anonymizeIp() if you operate under GDPR or similar.When you want the same enrichment on every report (trace id, tenant, feature flags), register a FlareMiddleware rather than duplicating calls to context():
class RequestContextMiddleware implements FlareMiddleware
{
public function handle(Report $report, Closure $next)
{
$report->context('request_id', request()->header('X-Request-Id'));
return $next($report);
}
}
This is the cleanest place to hook in correlation IDs so debugging in Flare ties back to your logs.
shouldDisplayExceptionNever leak Ignition in production. In Laravel this is configured via config/ignition.php ('editor', plus the framework's APP_DEBUG), but if you're using core Ignition in a non-Laravel PHP app or a bespoke bootstrap, call:
->shouldDisplayException($env === 'local' || $env === 'development')
Belt-and-braces: also never let a ProvidesSolution accidentally embed credentials in its description — it renders to the browser.
1. spatie/ignition ≠ spatie/laravel-ignition. In a Laravel app, adding spatie/ignition directly will not integrate — you need the Laravel wrapper. If you're on an older Laravel version and error pages look broken, mismatch between laravel-ignition and core ignition is the usual culprit. Check composer why spatie/ignition.
2. applicationPath() must be your real base path. Pass the actual app root (in Laravel: base_path()). Passing a parent directory trims too much and stack traces lose their file context; passing a subdirectory leaves ugly absolute paths.
3. Solution providers run on every exception. canSolve() is called on each throwable for each registered provider — keep it cheap. Avoid DB queries or HTTP calls inside canSolve; guard with class checks first. Do the expensive lookup inside getSolutions().
4. AI provider can be slow and can fail open. If OpenAI times out, the whole error page can stall before rendering. Wrap it: register it last, use caching, and be comfortable with the page falling back if it errors. Also — the docs are explicit that solutions may be wrong for your context; treat them as hints, not gospel.
5. Censorship is field-name based and shallow. censorRequestBodyFields(['password']) matches top-level request fields. Nested payloads (user.password) or values inside JSON bodies may slip through. If you have deeply nested sensitive data, add a middleware that strips it before sending.
6. runningInProductionEnvironment vs shouldDisplayException do different things. The former controls both rendering and Flare sending; the latter controls only rendering. Don't reach for shouldDisplayException(true) as "show the page in prod" — that's a security incident waiting to happen.
7. Dark mode is a string, not a boolean. setTheme('dark') — pass 'light' to revert. Typos silently fall back to default; no exception thrown.
8. Extension point hierarchy — pick the right hook:
ProvidesSolution.HasSolutionsForThrowable provider.FlareMiddleware.configureFlare(fn ($flare) => $flare->context(...)).9. Debugging "why isn't my solution showing?" Solutions only render if the page itself renders — check APP_DEBUG is on and that the exception is actually unhandled. canSolve returning false due to a type hint mismatch (e.g., expecting Exception but getting an Error) is a common silent failure: use Throwable in signatures.
10. getDocumentationLinks() is your leverage point. Link every solution to an internal runbook or wiki page. This is what turns Ignition from "pretty error page" into "developer onboarding tool" — the next person to hit the error gets the fix without asking Slack.
How can I help you explore Laravel packages today?