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:
- Set
BISIBILITY_CLIENT_IP_HEADER=x-forwarded-for.
- 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.