# Troubleshooting

Common failure modes when running Beacon self-hosted, with the fastest fix for each.

If your problem is not listed, the first three things to check are always:

```bash
docker compose logs -f app
docker compose logs -f horizon
docker compose logs -f nginx
```

---

## First-run setup keeps redirecting to /setup

The setup wizard is gated by a "setup complete" check. The dashboard redirects to `/setup` until **all four** steps are finished: workspace, Twilio credentials, business number, API key.

- Reopen `/setup` and check which step is missing.
- If you closed the browser before the API key step, your team and credentials are saved — pick up at the API key step.
- If the wizard appears to forget state, your `SESSION_DRIVER` may be misconfigured. Confirm `SESSION_DRIVER=database` and that the `sessions` table exists.

## "Unauthorized" / `missing_team_context` from API requests

The API key is missing, malformed, or belongs to a disabled team.

- Check the header is exactly `X-API-Key: beacon_...` (or `Authorization: Bearer beacon_...`).
- Test mode keys must end in `_test`. A key without `_test` will not bypass licensing.
- If you rotated keys in the dashboard, the old one is invalidated immediately.

## License key invalid / `402` on every request

Beacon validates every API request against `storage/app/beacon.license`.

- Confirm the file is present:
  ```bash
  docker compose exec app ls -l storage/app/beacon.license
  ```
- Use a `_test`-suffixed API key to confirm Beacon itself is healthy. If `_test` works and live keys do not, the license is the problem.
- Inspect the validation log in `storage/logs/laravel.log` — it names which check failed (signature, expiry, version, fingerprint).
- See [licensing.md](./licensing.md) for the full validation matrix.

## Twilio webhook returns 401 or nothing arrives

Beacon validates Twilio's `X-Twilio-Signature` against `WEBHOOK_CLIENT_SECRET`. A 401 means the signature did not match.

- Confirm `WEBHOOK_CLIENT_SECRET` is set to the **exact** Auth Token of the Twilio account that owns the receiving number.
- If you are running through a tunnel, Twilio computes the signature against the **public** URL. Make sure `APP_URL` and the URL you registered with Twilio match the tunnel host (no trailing slash, correct scheme).
- Inspect `webhook_calls` to see if Twilio is even reaching you:
  ```bash
  docker compose exec app php artisan tinker --execute 'Spatie\WebhookClient\Models\WebhookCall::latest()->limit(5)->get()->toArray();'
  ```
- See [webhooks.md](./webhooks.md) for full setup.

## Horizon dashboard is empty / shows no jobs

- Confirm the Horizon container is running: `docker compose ps horizon`.
- Confirm Redis is healthy: `docker compose exec redis redis-cli ping` should return `PONG`.
- Hit `/horizon` in a browser. If you get 403, Horizon's view gate is rejecting the current user — log in as a user the gate allows.

## Inbound messages never appear in threads

Walk the path:

1. Did Twilio call your webhook? Check `webhook_calls`.
2. Did Beacon accept it? A row in `webhook_calls` with no error means yes.
3. Did Horizon pick up the job? `docker compose logs -f horizon` and look for `ProcessInboundSmsJob`.
4. Did the job match a business number? Inbound is rejected if the destination number is not a registered business number on any team.

## "Vite manifest not found" in the browser

On the Docker path, frontend assets are baked into `ghcr.io/apxcde/beacon`. After `docker compose pull`, recreate the code volume so Nginx serves the new hashed assets:

```bash
docker compose down && docker volume rm beacon_beacon_code && docker compose up -d
```

If you installed via Composer from `packages.apexcode.dev` and are not using the image's baked assets:

```bash
npm install
npm run build
```

## SQLite database locked / write errors

SQLite is fine for one container, fragile under concurrent writers. If you are running Horizon, scheduler, and the app container against the same SQLite file under load, switch to MySQL:

```bash
docker compose --profile mysql up -d
```

See [self-hosted-docker.md](./self-hosted-docker.md#mysql-profile) for the full env block.

## OTP verification always fails

- OTPs expire (default 5 minutes). Re-check `config/services.php` or your env override.
- The verification request must reuse the **same** business number as the generation request — OTPs are scoped to the sending number.
- Test mode (`_test` keys) generates predictable codes — check the OTP generate/verify responses and logs if your tests are flaky.

## "License expired" warning in logs but requests still work

Beacon honors a 14-day grace period after expiry by default (`LICENSE_GRACE_PERIOD_DAYS`). You will keep serving requests but should renew. Past the grace period, the middleware returns 402.

## Rate limit errors (`429 Too Many Requests`)

Default token bucket: 10 req/sec, burst 50. Adjustable via env vars (search `config/services.php` and the `rate.limit` middleware). For a busy single-tenant install, raise the bucket; for a multi-tenant install, the per-team limits prevent one team from starving others.

## Where to look next

- Full env reference: [configuration.md](./configuration.md)
- API errors: [api.md](./api.md)
- MCP errors: [mcp.md](./mcp.md)
- Webhook flow: [webhooks.md](./webhooks.md)
- Licensing flow: [licensing.md](./licensing.md)
