Documentation

Operate

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:

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:
    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 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:
    docker compose exec app php artisan tinker --execute 'Spatie\WebhookClient\Models\WebhookCall::latest()->limit(5)->get()->toArray();'
    
  • See 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:

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:

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:

docker compose --profile mysql up -d

See self-hosted-docker.md 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

Raw markdown · llms.txt