Skip to main content
Production checklist This page consolidates the security contracts a bisibility operator must own. Each section links back to the source configuration or procedure page for exact variable semantics.

TLS and reverse proxy

Terminate TLS at a reverse proxy or CDN in front of the app. The production Compose stack publishes the app on 127.0.0.1:3000; do not expose port 3000 directly to the internet. Route /api/auth/* and /api/v1/* through the proxy. The public host and protocol must match SITE_URL and BETTER_AUTH_URL exactly, including any non-default port. A mismatch breaks OAuth redirects, auth callbacks, and absolute links. See Required environment variables for the origin variables and Production topology for the service layout.

Public port exposure

The default production Compose stack publishes only the app on 127.0.0.1:3000. PostgreSQL and Valkey are reachable only as postgres and redis inside the Compose network and are never published to the host. The opt-in debug override (docker-compose.debug.yml) binds PostgreSQL to 127.0.0.1:5432 and Valkey to 127.0.0.1:6379. These loopback bindings are for deliberate host-side debugging only, not for public exposure. The bundled Temporal server stays inside the Compose network. Its optional temporal-ui profile exposes the Web UI at 127.0.0.1:8233 only. Do not expose Temporal UI or database ports to the public internet without an authenticated gateway.

Trusted client IP

Point BISIBILITY_CLIENT_IP_HEADER at a header your proxy controls. The app never trusts a client-controlled header by default. With nginx or Caddy in front of the app, have the proxy overwrite a dedicated header:
Then set BISIBILITY_CLIENT_IP_HEADER=x-real-ip. Any proxy-overwritten header works, including a CDN header such as cf-connecting-ip. Behind a CDN that appends to x-forwarded-for:
  1. Set BISIBILITY_CLIENT_IP_HEADER=x-forwarded-for.
  2. Set BISIBILITY_CLIENT_IP_XFF_DEPTH to the number of entries your own infrastructure appends (1 for a single edge).
The address is read from the right, so clients cannot spoof their way in.
A depth larger than your real chain reads a client-supplied entry.Trust x-forwarded-for only behind your own appending proxy. Without one, Next.js preserves client-supplied values. Callers could then select their own rate-limit bucket. Appending with $proxy_add_x_forwarded_for or overwriting X-Real-IP removes that control.
Without a trusted header, anonymous API callers share one bucket. Sign-in limits also share one bucket per path, and audits record no source IP. Enforce anonymous limits at the edge with a CDN, WAF, or nginx limit_req. The edge knows the peer address directly. See Client IP behind a proxy for the operations handoff and Optional environment variables for the exact variable rows.

Fixed OTP

The demo login is intentionally insecure and disposable. It accepts a fixed sign-in code from anyone who can reach the app. Both DEMO_FIXED_OTP and DEMO_INSTANCE_INSECURE_AUTH_ACK must stay unset for real users or data. The production Docker image deliberately refuses a fixed OTP without the explicit acknowledgement. See Optional environment variables for the exact rows.

Application secrets

Generate, back up, and protect two secrets for the lifetime of the instance:
  • BETTER_AUTH_SECRET signs auth state. Replacing it signs every user out.
  • BISIBILITY_SECRETS_KEY encrypts provider credentials. Replacing it leaves stored credentials undecryptable; every connected provider must be reconnected by hand.
Generate each with openssl rand -base64 32. See Required environment variables for the full rotation semantics, including BETTER_AUTH_SECRETS and BISIBILITY_SECRETS_KEYS_RETIRED.

PostgreSQL and Valkey exposure

PostgreSQL is the durable application data store. It holds projects, keywords, rank history, audit logs, and encrypted provider credentials. Its durability is an operator responsibility: see Backup and restore. Valkey (or a Redis-compatible endpoint) holds rate-limit counters, idempotency keys, and realtime notification state. It is shared runtime state, not durable data. Valkey normally does not require a backup; a restart clears its state and the app repopulates counters on the next request. Neither PostgreSQL nor Valkey is published in the production Compose stack. Do not expose either to the public internet. If a managed database is used, apply network isolation and require TLS for the connection.

Webhook SSRF and private-network delivery

Webhook delivery targets must be public URLs by default. Private-network and loopback targets are rejected to prevent SSRF. WEBHOOK_ALLOW_PRIVATE_NETWORK=1 is an explicit override for a self-hosted instance that must deliver webhooks to trusted private, LAN, or loopback services. Set it only after reviewing the SSRF risk and never in a multi-tenant context. Signed delivery (HMAC-SHA256, timestamp, replay window) remains a receiver concern. See Verify signatures for the exact verification contract.

OAuth and public origins

Set SITE_URL and BETTER_AUTH_URL to the exact public origin, including any non-default port. Both are required and Compose fails before starting if either is blank. TRUST_REQUEST_ORIGIN=true is an escape hatch for preview deployments behind a trusted proxy that strips client-supplied forwarded-host headers. It defaults to false. Do not enable it without a proxy that overwrites forwarded headers. See Optional environment variables for the exact TRUST_REQUEST_ORIGIN semantics.

Health endpoint exposure

Three health endpoints serve different roles:
  • /api/v1/liveness is the restart probe. It reports whether the process is alive. Use it for container restart policy.
  • /api/v1/readiness is the traffic-admission probe. It reports whether the app is ready to serve requests. Do not route traffic until it returns 200.
  • /api/v1/health provides deeper diagnostics. It requires an API credential or INTERNAL_PROBE_TOKEN.
Anonymous liveness and readiness probes expose aggregate status only. Do not put INTERNAL_PROBE_TOKEN in URLs, query strings, or logs. Request the authenticated health endpoint with a Bearer header. See Verify and troubleshoot startup for the probe commands used during startup.

Admin recovery

If an account has lost its authenticator and all backup codes, an instance admin can perform an audited emergency two-factor reset. The operator must already be an instance admin. First verify the account owner’s identity outside bisibility. Then run the audited command inside the deployed app container:
The command removes the target 2FA factor. It revokes sessions, trusted devices, pending enrollment, and step-up grants. The same transaction appends instance_admin.account_two_factor_reset to the audit log. An audit failure rolls back the reset. The owner receives no replacement secret; they must enroll again and save new backup codes. See Instance admin for the admin seed command and role semantics.

Production security checklist

Complete this checklist before the instance serves anyone other than you.