Configuration and Security
This guide centralizes environment configuration and security expectations.
For deployment mode selection, see DEPLOYMENT.md.
For integration-specific setup values, see INTEGRATIONS.md.
Environment Variables
For the complete environment variable reference (all containers, defaults, and status labels), see ENVIRONMENT_VARIABLES.md.
The sections below provide guidance on configuration patterns, security hardening, and operational concerns that go beyond simple variable listings.
Role Split Guidance (Compose/Kubernetes)
For horizontally scaled split deployments, prefer:
- API pods/containers:
BACKEND_PROCESS_ROLE=api - Worker pods/containers:
BACKEND_PROCESS_ROLE=worker
For single-process deployments, keep BACKEND_PROCESS_ROLE=all.
External Access Settings
If users access soundspan from outside your local network, configure CORS and API routing intentionally.
Frontend Build-Time vs Runtime
NEXT_PUBLIC_API_URL, NEXT_PUBLIC_API_PATH_MODE, and NEXT_PUBLIC_LISTEN_TOGETHER_ALLOW_POLLING are frontend build-time variables.
- They work as expected in source-build flows (
npm run dev,npm run buildwith env/build args). - In pre-published frontend images, changing these vars at container runtime does not change browser behavior.
Source-Build Direct Mode (optional)
If you build the frontend yourself and want direct browser calls to backend:
NEXT_PUBLIC_API_URL=https://soundspan-api.yourdomain.com
NEXT_PUBLIC_API_PATH_MODE=direct
ALLOWED_ORIGINS=http://localhost:3030,https://soundspan.yourdomain.com
Pre-Published Image Recommendation (no rebuild)
For users consuming published images:
- Leave
NEXT_PUBLIC_API_URL/NEXT_PUBLIC_API_PATH_MODEunset unless you are publishing your own rebuilt frontend image. NEXT_PUBLIC_LISTEN_TOGETHER_ALLOW_POLLINGis also build-time; runtime container env changes on pre-published images do not switch browser transport behavior.- Route
/api/*to backend in your reverse proxy, and route app traffic to frontend. - Set backend
ALLOWED_ORIGINSto include your frontend origin.
NEXT_PUBLIC_API_PATH_MODE controls how the browser reaches backend APIs:
auto(default): useNEXT_PUBLIC_API_URLwhen set; otherwise use same-origin proxy mode (/api/*).proxy: always use same-origin/api/*calls throughfrontend/app/api/[...path]/route.ts.direct: always call backend directly (usesNEXT_PUBLIC_API_URLwhen provided, else derivesprotocol://<host>:3006).
Set this in frontend build/dev environment (same place you set NEXT_PUBLIC_API_URL).
For pre-published images, see reverse-proxy path routing guidance in REVERSE_PROXY_AND_TUNNELS.md.
For Listen Together, the frontend proxies /socket.io/listen-together to backend by default in split deployments.
If you bypass frontend proxying intentionally, your edge proxy/tunnel must route /socket.io/listen-together to backend :3006.
LISTEN_TOGETHER_ALLOW_POLLING=false is recommended for HA deployments; only enable polling fallback when sticky sessions are guaranteed end-to-end.
For pre-published frontend images, browser polling fallback also requires rebuilding with NEXT_PUBLIC_LISTEN_TOGETHER_ALLOW_POLLING=true.
For multi-replica backend/frontend deployments, configure Redis as a highly available endpoint. A single Redis pod is a runtime SPOF for sessions, queues, and realtime coordination. Redis HA is an operator-managed prerequisite (external managed Redis/Dragonfly, Sentinel, or equivalent); soundspan consumes the configured endpoint for queues, claims, and realtime coordination and does not manage Redis HA topology itself.
Browser Content Security Policy
The frontend sends a nonce-based Content Security Policy on every Next-served document, including unauthenticated /share/* pages and PWA navigations. API response CSP headers are separate and do not protect documents rendered by the browser.
The production policy is:
default-src 'self'; script-src 'self' 'nonce-{PER_REQUEST_NONCE}' 'strict-dynamic'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob:; media-src 'self' blob:; connect-src 'self' ws: wss:; worker-src 'self' blob:; font-src 'self' data:; object-src 'none'; base-uri 'self'; form-action 'self'; frame-ancestors 'none'
The frontend generates a new unpredictable nonce for each request. Next applies it to framework, page, and inline runtime scripts. This avoids script-src 'unsafe-inline'. Development mode adds script-src 'unsafe-eval' because the Next development runtime requires it. style-src 'unsafe-inline' remains necessary for framework and application inline styles.
Nonce propagation requires dynamic rendering, so Next static optimization, ISR, and default CDN document caching are disabled. Static assets remain cacheable. The policy contains no deployment hostname. Browser API, cover-art, provider-thumbnail, playback, and Listen Together requests use same-origin frontend proxy paths by default. Enforcing CSP does not support a separately originated NEXT_PUBLIC_API_URL; use NEXT_PUBLIC_API_PATH_MODE=proxy or the default unset configuration before enforcement.
The policy defaults to Content-Security-Policy-Report-Only. Use this rollout sequence:
- Leave
CSP_ENFORCEunset or set it tofalse. - Review browser console violations during normal playback, Listen Together, offline/PWA use, unauthenticated share pages, and administrator workflows.
- Fix or explicitly account for every required source.
- Set
CSP_ENFORCE=trueand restart the frontend or AIO container. - Repeat the same workflow checks against the enforcing header.
Set CSP_REPORT_URI to a root-relative path handled by your reverse proxy or to an HTTPS collector URL to receive reports. soundspan does not include a report collector. When configured, the frontend emits both the legacy report-uri directive and the report-to directive with a matching Reporting-Endpoints header. Treat reports as potentially sensitive because browsers can include document and blocked-resource URLs. Values containing header delimiters, credentials, fragments, unsupported schemes, or protocol-relative URLs are ignored.
SVG image optimization exception
images.dangerouslyAllowSVG remains enabled for cover-art compatibility. The Next image optimizer is explicitly configured to return optimized images with Content-Disposition: attachment and this image-specific CSP:
default-src 'self'; script-src 'none'; frame-src 'none'; sandbox;
This sandbox applies to optimized image responses independently of the document policy. External MusicBrainz, Last.fm, Deezer, podcast RSS, TIDAL, YouTube Music, and other provider images are fetched through the backend cover/browse proxy or the same-origin Next image optimizer. The browser document policy therefore does not allow provider image hosts directly.
Sensitive Variables
Never commit .env files or credentials.
| Variable | Purpose | Required |
|---|---|---|
SESSION_SECRET |
Required JWT signing fallback (32+ chars) | Yes |
SETTINGS_ENCRYPTION_KEY |
Encryption of stored credentials (32+ chars, must not be the published default) | Yes |
INTERNAL_API_SECRET |
Service-to-service authentication (32+ chars) | Yes |
METRICS_TOKEN |
Bearer authentication for backend and worker Prometheus scrapes | Required to scrape unless metrics are explicitly public |
POSTGRES_PASSWORD |
PostgreSQL authentication | Yes, production |
API_KEY_PEPPER |
HMAC pepper for API keys; keep stable or existing hashed keys become invalid | Optional; falls back to encryption/JWT secrets |
OIDC_CLIENT_SECRET |
OIDC confidential client authentication | If using OIDC |
LIDARR_API_KEY |
Lidarr integration | If using Lidarr |
OPENAI_API_KEY |
AI features | Optional |
LASTFM_API_KEY |
Artist recommendations | Optional |
LASTFM_SHARED_SECRET |
Signed Last.fm user authentication and scrobble forwarding | If using Last.fm scrobbling |
FANART_API_KEY |
Artist images | Optional |
YTMUSIC_STREAMER_URL |
YouTube Music sidecar URL | If using YouTube Music |
TIDAL_SIDECAR_URL |
TIDAL sidecar URL | If using TIDAL |
Soulseek credentials are configured via System Settings and stored encrypted in the database.
Last.fm lookups require a key you provide. Set LASTFM_API_KEY in the environment or store a key in System Settings when you want Last.fm-backed recommendations and metadata; otherwise those lookups remain unavailable.
Last.fm scrobble forwarding also requires LASTFM_SHARED_SECRET as a runtime secret. Per-user Last.fm session keys and ListenBrainz tokens are encrypted with SETTINGS_ENCRYPTION_KEY before database storage and are never returned by status endpoints.
Metrics exposure
GET /metrics is private by default. It accepts only a bearer token matching
METRICS_TOKEN and fails closed when that value is absent. Keep the endpoint on
an internal network, store the token as a runtime secret, and configure the
scraper to send it in the Authorization header.
METRICS_PUBLIC=true removes this gate. Treat that setting as unsafe outside an
isolated private network. The exposition includes process health, route classes,
queue names and state, cache behavior, and federation sync outcomes. It never
uses user IDs, media IDs, raw URLs, cache keys, peer IDs, or error text as
labels.
Authentication and Credential Security
Every authenticated request uses an explicit credential transport — there is no cookie-session authentication, so an ambient cookie cannot silently take precedence over a bearer token or API key and the API has no ambient-cookie CSRF credential surface. The HTTP-only OIDC flow-binding cookie remains; it binds a login transaction to its initiating browser and does not authenticate API requests. The one deliberate exception is the queue dashboard: Bull Board is a server-rendered page that cannot carry a bearer token, so Open queue dashboard on the Admin page mints a 15-minute, HTTP-only, SameSite=Strict cookie whose path is limited to /api/admin/queues. Only that mount accepts it, only for administrators, and the token inside it carries a dashboard-only purpose claim that every other route rejects.
| Surface | Credential transport | Lifetime | Revocation path |
|---|---|---|---|
| Web UI and first-party API | Authorization: Bearer JWT access token plus refresh token in the refresh request body |
Access: 24 hours. Refresh: 30 days. | A self-service password change or administrator-set password increments tokenVersion, invalidating outstanding access and refresh JWTs. Ordinary logout removes the current browser's tokens only; there is no separate logout-all-devices endpoint. |
OpenSubsonic /rest |
Per-request token plus salt, password transport, or an ssap_ app password; API keys use the separate apiKey parameter |
The token is a per-request digest with no independent server-side lifetime. App-password credentials remain valid until revoked. The legacy dedicated Subsonic password is deprecated: it cannot be set from Settings, but direct API updates remain possible and existing stored values are honored. | Revoke an app password individually. Account password changes and administrator-set passwords clear any legacy dedicated Subsonic password. |
| External API clients | X-API-Key header |
90 days from creation | Delete the API key. |
Queue dashboard (/api/admin/queues) |
Bearer token, API key, or the path-scoped dashboard cookie minted by POST /api/admin/queues/session |
Cookie: 15 minutes. | DELETE /api/admin/queues/session (End dashboard access on the Admin page), or let it expire. |
| Federation peer API | Dedicated opaque Authorization: Bearer peer credential |
No automatic expiry. | Rotate the credential, revoke the peer, or delete the peer. |
| Internal sidecar requests | x-internal-secret header |
No automatic expiry. | Rotate INTERNAL_API_SECRET across the backend and sidecars, then restart them. |
- Token refresh uses
/api/auth/refresh. SESSION_SECRETremains required as the JWT signing fallback whenJWT_SECRETis unset.- Encryption key validity is checked at startup.
- Secure OIDC flow cookies default on when
NODE_ENV=productionand can be overridden withSECURE_COOKIES. The OIDC flow cookie drops its__Host-prefix when secure cookies are off.
Reverse Proxy Trust and Rate Limits
soundspan is designed to run behind a reverse proxy. The backend uses its
configured Express trust proxy policy, so the trusted X-Forwarded-For chain
determines the client IP used by per-IP rate limits. Some authentication,
registration, OIDC, OpenSubsonic, share-link, webhook, and federation endpoints
can be unauthenticated and exposed to the internet. Set TRUST_PROXY_HOPS to
the actual number of trusted proxy hops. Do not leave a permissive proxy chain
in a deployment where clients can connect to the backend directly and supply
their own forwarding headers.
Security-relevant counters use the existing REDIS_URL connection. This makes
authentication and account-management, admin and invite-management, OIDC,
OpenSubsonic authentication, share-link, webhook, and federation limits common
to every backend replica and preserves them across backend restarts. Redis keys
are separated by limiter name. A Redis command has a 250 ms deadline. If Redis
is disconnected or a command fails or times out, credential-guarding limits
fall back to an in-process sliding window of up to 10,000 keys and one million
retained hit timestamps per limiter, while other
Redis-backed limits remain availability-first and allow the request. The
fallback is per backend process during an outage, so replicas enforce separate
budgets until Redis recovers; each fallback decision emits a rate-limited
warning.
The general high-ceiling API limiter and high-volume playback, image, download, lyrics, and provider-request limiters remain per-process and in memory. They are hot-path safeguards against client bugs, accidental loops, bandwidth bursts, and upstream-provider overload. They are not the distributed abuse-control boundary, and avoiding Redis on those paths keeps their latency and Redis load bounded.
OIDC and App-Password Security
soundspan uses the OIDC Authorization Code flow with state, nonce, and S256 PKCE. Login and account-link attempts keep their pending state on the server. The callback sends the browser a short-lived, single-use exchange code. It never puts access or refresh tokens in a redirect URL.
OIDC requires the web app and API to be same-site. A single public hostname needs no extra redirect setting. A same-site callback on a sibling API subdomain or different port must set OIDC_WEB_BASE_URL to the canonical web origin. Cross-site deployments on different registrable domains are not supported because the flow-binding cookie is SameSite=Lax.
OpenSubsonic app passwords are restricted to /rest. Their ssap_ secrets are encrypted at rest with authenticated encryption under SETTINGS_ENCRYPTION_KEY. Users can revoke each app password independently.
See the OIDC_SSO.md topology matrix for supported layouts. The same guide covers provider setup, account linking, provisioning, role ownership, app passwords, MFA, and break-glass recovery.
Streaming Credential Security
- YouTube Music and TIDAL OAuth tokens are AES-encrypted before database storage
- Credentials are isolated per user account
- Credentials are only decrypted for active sidecar operations
- TIDAL tokens are refreshed automatically and re-encrypted when needed
Federation Credential Security
- Instance links use dedicated
Authorization: Bearercredentials. They do not reuse user JWTs or API keys and never establish a user identity on the host. - Host-issued credentials are random 32-byte tokens. The raw value is returned only when issued or rotated; the database stores an HMAC hash for verification without storing the raw token.
- Each credential grants an explicit subset of
library:read,stream:read, andembeddings:read. Embedding access also requires library access. library:readincludes the instance's complete subscribed podcast-feed catalog. Treat linked peers as trusted recipients of feed URLs and podcast metadata.- Revocation clears credential material and changes the peer to
REVOKED. Deleting a peer also cascades its consumer-side mirrored catalog rows. - Consumer outbound tokens are encrypted at rest through the authenticated AES-256-GCM settings cipher backed by
SETTINGS_ENCRYPTION_KEY. Thev2:prefix marks the current cipher envelope. On startup, an idempotent backfill treats any non-null outbound token without that marker as legacy plaintext and encrypts it before the process becomes ready. Compare-and-set updates make concurrent API and worker starts safe; plaintext reader compatibility exists only during that rolling-startup window. API and admin responses exclude both outbound tokens and credential hashes. - Peer base URLs must use HTTPS and contain no URL credentials. By default, literal and DNS-resolved destinations in
0/8,10/8,100.64/10,127/8,169.254/16,172.16/12,192.168/16,198.18/15,::,::1,fc00::/7, andfe80::/10are rejected. Administrators who intentionally federate over a private LAN or VPN can setFEDERATION_ALLOW_PRIVATE_PEERS=true; this unsafe opt-in does not relax HTTPS, URL-credential, or connection-pinning checks. - Before each attempt, the consumer resolves every peer address and pins the socket to the validated result while retaining the hostname for TLS and HTTP. The backend then attaches the decrypted token to the bounded request; browser clients never receive it. Redirects are disabled, and Axios failures are converted to safe federation errors before callers can log them.
- Federation egress ignores proxy environment variables by default: connection pinning and proxying are architecturally at odds, because a proxy makes the outbound connection itself and re-resolves the hostname, so pinning at this end cannot prevent DNS rebinding on the proxy's side. Deployments behind a mandatory egress proxy can set
FEDERATION_ALLOW_PROXY=trueto restore Axios env-proxy handling for federation's HTTPS-only requests (HTTPS_PROXY/ALL_PROXY;HTTP_PROXYnever applies). Per-attempt address resolution and validation still run and fail closed in this mode, but no connection is pinned: proxied requests are connected by the proxy, and requests exempted byNO_PROXYconnect directly without a pin. The proxy becomes part of the trust boundary.
Network egress policy remains a defense-in-depth control. Administrators must treat a linked peer as trusted and limit admin access accordingly.
Webhook and Admin Security
- Lidarr webhook signatures are supported and should be configured
- Bull Board (
/api/admin/queues) requires authenticated admin access: a bearer token, an API key, or the path-scoped dashboard cookie minted from the Admin page - Swagger docs: the UI at
/api/docsis always accessible; the raw spec at/api/docs.jsonrequires auth in production unlessDOCS_PUBLIC=true
Optional VPN Notes
If using Mullvad VPN for Soulseek:
- Put WireGuard config in
backend/mullvad/(gitignored) - Never commit private keys
*.confandkey.txtare already ignored
Generating Secrets
# Required JWT signing fallback
openssl rand -base64 32
# Settings encryption key
openssl rand -base64 32
Network Safety Guidance
- soundspan is intended for self-hosted usage
- For internet exposure, place it behind HTTPS reverse proxy/tunnel
- Keep
ALLOWED_ORIGINSstrict and explicit
See also
- Environment Variables — Complete env var reference by container
- OIDC and SSO — Provider setup, account linking, roles, and recovery
- Deployment Guide — Docker and compose deployment options
- Reverse Proxy and Tunnels — Edge routing for split deployments
- Integrations Guide — Integration-specific setup values
- Kubernetes Guide — Helm deployment and HA rollout