# Troubleshooting

> Troubleshoot Alphorn - notifications not arriving, delivery statuses, webhook HTTP errors (400, 401, 403, 404, 413, 429, 508), self-hosting, SSO, and SSE streaming issues.

Source: https://docs.alphorn.dev/troubleshooting/

## Notifications not arriving

### Check the delivery logs

The most common starting point. Go to **Messages** in the dashboard and open the notification. It shows the delivery status for every channel.

- **Pending / Processing** - Still being sent. Wait a moment and refresh.
- **Delivered** - Alphorn sent it successfully. The issue is on the channel side (check Slack/Discord/Email).
- **Failed** - The last attempt failed. Alphorn retries automatically while attempts remain; check the error message for details.
- **Stale** - No worker picked up the delivery in time. On a self-hosted instance, check that the worker is running (see [Architecture & Scaling](/self-hosting#architecture--scaling)).

If a message has **no deliveries at all**, no channel matched: either no channel is linked to the webhook, the channels or their link to the webhook are switched off, or the [filters](/filtering) excluded it. See [Retries](/retries) for how retries and the 24-hour delivery window work.

### Verify your webhook URL

Make sure your webhook URL is correct and accessible:

```bash
curl -s -X POST https://app.alphorn.dev/n/V1StGXR8_Z5jdHi6B-myT \
  -H "Authorization: Bearer $ALPHORN_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"title": "Test", "message": "Testing webhook"}'
```

You should get back:

```json
{"messageId": "0199b2f4-3c1e-7a8d-9f20-5d4c3b2a1e0f"}
```

If you get a `404`, the URL is wrong. If you get a `401`, the API key is missing or wrong. If you get no response, check your network connectivity. See [API issues](#api-issues) for all error codes.

### Check filtering rules

If a channel isn't receiving notifications, it might be filtered out. Filters are set per webhook and channel: open the webhook, expand the channel in the channel list, and check its filter. All conditions in a group must match, but only one group has to. Use the filter's test panel to try a sample notification, or temporarily clear the filter to confirm.

### Channel configuration issues

Common mistakes by channel type:

| Channel | Common issue |
|---|---|
| **Slack** | Webhook URL expired or was regenerated - get a new one from Slack |
| **Discord** | Webhook URL deleted - recreate in Discord channel settings |
| **Email (SMTP)** | Wrong port, missing TLS, or credentials rejected by provider |
| **Telegram** | Bot not added to chat, or wrong chat ID (group IDs are negative numbers) |
| **PagerDuty** | Wrong integration key - use the Events API v2 integration key, not the API key |
| **Microsoft Teams** | Incoming Webhook connector disabled or removed from channel |

## Self-hosting issues

### Database connection errors

**Symptom:** Alphorn won't start, logs show connection errors.

Check your `DATABASE_URL`:

```
postgres://user:password@host:5432/dbname
```

- Is PostgreSQL running and accepting connections?
- Is the hostname correct? Inside Docker Compose, use the service name (e.g., `db`), not `localhost`.
- Is the port correct? Default is 5432.
- Do the credentials match your PostgreSQL configuration?

### Migrations fail on startup

The Docker image applies migrations on every start; from source you run `pnpm db:deploy`. If they fail:

- Check that the database user has permission to create tables and alter schema
- `permission denied to create extension "pg_trgm"`: let a superuser run `CREATE EXTENSION pg_trgm;` in the Alphorn database once, then restart
- The container exits right away if PostgreSQL isn't reachable yet - make sure it starts after the database is ready (e.g. `depends_on` with `condition: service_healthy`)
- Check PostgreSQL logs for specific error messages
- Make sure you're running PostgreSQL 18+ (older versions fail with `function uuidv7() does not exist`)

### Sign-in fails over plain HTTP

Alphorn uses secure cookies, which browsers only accept over HTTPS (or on `localhost`). Put Alphorn behind a reverse proxy with TLS and set `BETTER_AUTH_URL` to the `https://` URL. See [Self-Hosting → Reverse Proxy & HTTPS](/self-hosting#reverse-proxy--https).

### Alphorn starts but isn't accessible

- Check that port 3000 is mapped correctly in your Docker Compose or firewall
- If behind a reverse proxy, make sure the proxy passes requests to port 3000
- Verify `BETTER_AUTH_URL` matches the URL you're accessing Alphorn from

### SSO / OAuth not working

- Verify the callback URL matches exactly: `{BETTER_AUTH_URL}/api/auth/callback/{provider}` for GitHub, Google, and Microsoft, or `{BETTER_AUTH_URL}/api/auth/callback/{OIDC_PROVIDER_ID}` for OIDC
- Microsoft stopped working after an upgrade? The callback changed to `/api/auth/callback/microsoft` - see [Self-Hosting → Microsoft](/self-hosting#microsoft)
- Check that `BETTER_AUTH_URL` includes the protocol (`https://`) and has no trailing slash
- For OIDC providers, verify the discovery URL returns a valid configuration
- Check provider-specific setup in [Self-Hosting → SSO / OAuth](/self-hosting#sso--oauth)

## API issues

### HTTP 400 - Invalid JSON body / JSON body must be an object

You sent `Content-Type: application/json` (or another JSON type), but:

- **Invalid JSON body** - the body isn't valid JSON. Check for trailing commas and unquoted keys.
- **JSON body must be an object** - the body is a JSON array or a plain value. Wrap it in an object, e.g. `{"message": "..."}`.

To send plain text, use `Content-Type: text/plain`.

### HTTP 401 - Missing or invalid Authorization header / Invalid API key

The webhook requires an API key.

- **Missing or invalid Authorization header** - the header is missing or doesn't start with `Bearer ` (exactly like this, case-sensitive). Check that your API key variable isn't empty.
- **Invalid API key** - the key is wrong, or it was regenerated. If you've lost the key, use **Regenerate key** on the webhook page - the full key is only shown once.

The key is checked before the webhook's enabled state, so a disabled webhook called with a wrong key also returns `401`.

### HTTP 403 - Webhook is disabled

The webhook was disabled in the dashboard, or automatically after a plan downgrade. Enable it on the webhook page.

### HTTP 404 - Not found

The URL doesn't match any webhook. Check for typos. If the webhook was deleted, or its URL was regenerated, the old URL no longer works.

### HTTP 413 - Payload too large

The request body is larger than 1 MiB. Self-hosted instances can raise the limit with `WEBHOOK_MAX_BODY_BYTES`.

### HTTP 429 - Message quota exceeded

You've hit your plan's monthly message limit. Check your usage under **Billing**. Options:

- Upgrade your plan
- Reduce notification volume with more selective filtering
- Wait for the next billing cycle

### HTTP 508 - Loop detected

The notification was forwarded from one Alphorn webhook to another through Webhook channels too many times (3 hops by default), or it came back to a webhook it already passed. Check your Webhook channels for a forwarding loop.

## Streaming issues

### SSE connection drops

Server-Sent Events connections may drop due to network issues, proxy timeouts, or server restarts. Your client should reconnect automatically:

```javascript
const eventSource = new EventSource(
  "https://app.alphorn.dev/api/stream/3f2a9c1e-7b4d-4e8a-9c2f-1d5e6b7a8c90"
);

eventSource.onerror = () => {
  // EventSource reconnects automatically
  // Implement backoff if you need custom reconnection logic
  console.log("Connection lost, reconnecting...");
};
```

Alphorn sends a keepalive comment every 30 seconds. If connections still drop frequently behind a reverse proxy, increase the proxy timeout:

```nginx
# Nginx
proxy_read_timeout 86400;
proxy_buffering off;
```

### Live Stream returns 503

"SSE runs in standalone mode; route /api/stream/* to the SSE server" means `SSE_MODE=standalone` is set, but your reverse proxy sends `/api/stream/*` to the web app. Route it to the SSE server's `/stream/*` instead - see [Streaming → Self-Hosting](/streaming#self-hosting).

### Live Stream deliveries fail with MODE=worker

A separately running worker has no connected stream clients. Run the standalone SSE server and set `SSE_MODE=standalone`, `SSE_SERVER_URL`, and `SSE_INTERNAL_SECRET` on the web app and the worker.

## Still stuck?

- Check the [FAQ](/faq) for common questions
- Open an issue on [GitHub](https://github.com/alphorn-dev/alphorn/issues)
