Skip to content
Feeblo Docs
Esc
navigateopen⌘Jpreview
On this page

Troubleshooting

Fix the failure modes that hit self-hosted Feeblo deployments, each with a cause you can verify in the source.

Each entry below follows the same shape: the symptom, the cause in Feeblo’s code or compose configuration that produces it, and the fix. If your symptom is not here, check OAuth login for sign-in setup and environment variables for the full variable reference.

Sessions invalid after restart

Symptom: Every restart or redeploy of the server logs all users out at once. Users report they were signed in yesterday and now must sign in again. Verification links and one-time codes that were still pending stop working.

Cause: The server signs session tokens and encrypts verification state with AUTH_ENCRYPTION_KEY. packages/auth/src/config.ts reads it and passes it to better-auth as the secret, and packages/domain/src/auth/config.ts encrypts verification OTP cookies with the same value. Change the key and every existing cookie fails verification on the next request, so all sessions die together.

The key changes when you regenerate .env between restarts, when a script mints a fresh value on every boot, or when two server instances behind a load balancer carry different keys and each rejects the other’s cookies.

Fix: Generate the key once and keep it.

openssl rand -hex 32

Store it in your secret manager or .env, and never regenerate it. Every server instance that shares the database must hold the same value. The production compose file already forces the issue: the server container refuses to start without AUTH_ENCRYPTION_KEY set. The same key also encrypts webhook endpoint credentials when INTEGRATION_ENCRYPTION_KEY is unset, and a value shorter than 32 bytes makes webhook create and update fail with “Webhook credentials could not be encrypted”.

Widget requests blocked by CORS

Symptom: The public board renders, but widget API calls from board pages fail in the browser console with CORS errors. Sign-in from a board subdomain can also fail with INVALID_ORIGIN or INVALID_CALLBACK_URL. Everything works when you open the API URL directly.

Cause: Two layers gate browser requests, and both derive their rules from APP_URL, API_URL, and APP_ROOT_DOMAIN.

apps/server/src/http/cors.ts allows an origin only when it exactly matches API_URL or APP_URL, or when it ends with .<APP_ROOT_DOMAIN> and carries the same protocol and port as APP_URL. The auth server’s trusted origins (packages/auth/src/utils.ts, plus the compose default AUTH_TRUSTED_ORIGINS=$APP_URL,$API_URL,*.$APP_ROOT_DOMAIN) use the same variables for the INVALID_ORIGIN and INVALID_CALLBACK_URL checks.

The misconfigurations that break both at once:

  • APP_ROOT_DOMAIN=localhost (the compose default) while boards run on feedback.example.com. No board host ends with .localhost, so every board origin fails the check.
  • APP_URL left as http://localhost:3001 from .env.example while the dashboard serves https://app.example.com. The subdomain rule then demands the board’s protocol and port match http://localhost:3001, which no real board does.
  • Boards on a domain outside your root, for example feedback.example.com with APP_ROOT_DOMAIN=example.net.

Fix: Make the three variables describe reality: APP_URL must be the full dashboard origin (protocol and non-default port included), APP_ROOT_DOMAIN the bare root domain with no protocol or port, and wildcard DNS (*.example.com) must resolve to the web server. Set AUTH_TRUSTED_ORIGINS only when browsers legitimately call the API from origins outside that set, and include every one of them, because the variable replaces the defaults rather than extending them.

Emails not sending

Symptom: Verification codes, password resets, and welcome emails never arrive. The server log shows SMTP provider submission failed with an ECONNREFUSED or ETIMEDOUT error code.

Cause: The mailer always sends through nodemailer over SMTP (packages/transactional/src/mailer.ts). It reads SMTP_HOST, SMTP_PORT, SMTP_USERNAME, SMTP_PASSWORD, SMTP_SECURE, and SMTP_UNSAFE_IGNORE_TLS. SMTP_TRANSPORT appears in .env.example with values smtp-auth, smtp-api, and resend, but no code reads that variable, so changing it does not switch transports. The failure is a mismatch between the SMTP_* values and the relay the server can reach.

Two defaults hide until mail matters:

  • packages/transactional/src/config.ts defaults SMTP_HOST to 127.0.0.1 and SMTP_PORT to 2500. Inside the compose network nothing listens there, so the connection is refused.
  • nodemailer sends authentication only when SMTP_USERNAME is set. A relay that requires auth receives anonymous connections and rejects them.

The local dev stack runs Mailpit on 127.0.0.1:1025 (see docker/docker-compose.dev.yml), which is why .env.example uses that port. A production deployment gets no SMTP values from compose at all, so defaults apply.

Fix: Point the server at a relay it can reach and authenticate with it.

SMTP_HOST=smtp.example.com
SMTP_PORT=587
SMTP_USERNAME=your-username
SMTP_PASSWORD=your-password
SMTP_FROM_ADDRESS=noreply@example.com

Use the relay’s hostname inside the compose network if it is another container, not localhost. Set SMTP_SECURE=true only for implicit TLS on port 465; leave it unset for STARTTLS ports like 587. Set SMTP_UNSAFE_IGNORE_TLS=true only when the relay does not support STARTTLS at all. Verify the SMTP_FROM_ADDRESS domain passes the relay’s sender checks, since many relays reject mail from unregistered domains.

Webhook deliveries rejected

Symptom: Creating or updating a webhook endpoint fails, or deliveries fail, with one of these errors: “Webhook endpoint cannot target localhost”, “Webhook endpoint cannot target a private or reserved address”, “Webhook endpoint must use HTTPS in production”, or “Webhook endpoint hostname resolved to a private or reserved address”.

Cause: Every webhook endpoint passes through the egress policy in integrations/webhook/src/webhook-endpoint-security.ts before it is persisted and again before each delivery. In production the policy allows HTTPS URLs only, rejects localhost, and rejects every private or reserved IPv4 and IPv6 range, including NAT64 prefixes and documentation ranges. At delivery time the server resolves the hostname itself and pins only public addresses, so a hostname that resolves to a private address fails even when the URL looks public. That second check closes a DNS rebinding hole, and it runs for every delivery.

INTEGRATION_ALLOW_PRIVATE_NETWORK does not override this in production. The compose layer in apps/server/src/app/layers.ts honors it only when NODE_ENV=development, and .env.example warns never to enable it in production.

Fix: Give webhooks a public endpoint that resolves to public addresses only:

  • Serve the receiver over HTTPS with a valid certificate. Plain HTTP endpoints are rejected in production, and a broken TLS handshake fails every delivery.
  • Keep the endpoint off localhost, containers on the server’s own network, and VPN or NAT ranges. A hostname with any private A or AAAA record fails the delivery-time check.
  • For local development against a local receiver, set INTEGRATION_ALLOW_PRIVATE_NETWORK=true together with NODE_ENV=development on the server. Never ship that combination to production.

Next steps

Was this page helpful?