Skip to content

HTTP Server Configuration

Configure HTTP/HTTPS listening, named sockets, and TLS certificates. The unified HTTP server handles all HTTP-based channels.

Related:

IdentityScribe uses a unified HTTP server for all HTTP traffic:

All channels bind to the same server via named sockets. By default, everything uses the @default socket on port 8080.

flowchart TB
    subgraph Config["http { } Configuration"]
        Default["@default socket<br/>port: 8080, host: auto"]
        Named["http.sockets.* (opt-in)"]
    end

    subgraph Channels["Channels"]
        REST["channels.rest"]
        GQL["channels.graphql"]
        MCP["channels.mcp"]
        MON["monitoring.*"]
    end

    Default --> REST
    Default --> GQL
    Default --> MCP
    Default --> MON
    Named -.->|"socket = name"| REST
    Named -.->|"socket = name"| GQL
    Named -.->|"socket = name"| MCP
    Named -.->|"socket = name"| MON

The primary socket is configured at http {} root level:

http {
# Port for all HTTP traffic (default: 8080)
port = 8080
port = ${?SCRIBE_HTTP_PORT}
# Host binding
# "auto" resolves based on app.mode:
# - dev/test/local: localhost (safe default)
# - production: 0.0.0.0 (all interfaces)
host = auto
host = ${?SCRIBE_HTTP_HOST}
}

Define additional sockets under http.sockets.* for network separation:

http.sockets.internal {
port = 9001
port = ${?SCRIBE_INTERNAL_PORT}
host = "localhost"
}

Named sockets inherit all settings from http {} automatically. Only override what differs.

Channels reference sockets by name. If not specified, they use @default.

channels.rest {
enabled = true
# Socket for REST API (default: @default)
socket = ${?SCRIBE_REST_SOCKET}
}

Monitoring supports per-endpoint socket configuration:

monitoring {
# Default socket for all monitoring (falls back to @default)
socket = ${?SCRIBE_MONITORING_SOCKET}
# Per-endpoint overrides (each falls back to monitoring.socket)
prometheus.socket = ${?SCRIBE_PROMETHEUS_SOCKET}
observe.socket = ${?SCRIBE_OBSERVE_SOCKET}
health.socket = ${?SCRIBE_HEALTH_SOCKET}
}

Channels can bind to multiple sockets using array syntax:

# HOCON array
monitoring.socket = ["@default", "internal"]
# Environment variable (comma-separated)
SCRIBE_MONITORING_SOCKET="@default,internal"

Everything on one port — simplest setup:

http {
port = 8080
host = "0.0.0.0"
}
# All channels use @default automatically
Terminal window
curl http://localhost:8080/api/entries/user
curl http://localhost:8080/metrics
curl http://localhost:8080/readyz

Keep REST public, monitoring internal:

http {
port = 8080
host = "0.0.0.0"
sockets.internal {
port = 9001
host = "localhost"
}
}
monitoring.socket = "internal"
Terminal window
# Public REST API
curl http://public-ip:8080/api/entries/user
# Internal monitoring (localhost only)
curl http://localhost:9001/metrics
curl http://localhost:9001/readyz

Note: The bundled monitoring stack (monitoring/docker, monitoring/helm) uses this pattern with monitoring on port 9001.

For development, you may want monitoring endpoints available on both the default and internal sockets to avoid confusion when accidentally using the wrong port:

http {
port = 8080
sockets.monitoring { port = 9001 }
}
monitoring {
socket = "monitoring"
# Expose on BOTH sockets for dev convenience
observe.socket = ["@default", "monitoring"]
health.socket = ["@default", "monitoring"]
prometheus.socket = ["@default", "monitoring"]
}

This configuration:

  • Keeps the production separation (monitoring.socket = "monitoring")
  • Overrides individual endpoints to be available on both sockets
  • Prevents 404 confusion when hitting the wrong port during development

The config/dev-local.conf uses this pattern.

Enable HTTPS on the default socket:

http {
port = 443
ssl {
enabled = true
cert = "/path/to/server.pem"
key = "/path/to/server.key"
}
}

Require client certificates for API access (applies to REST, GraphQL, and MCP):

http {
port = 8443
ssl {
enabled = true
cert = "/path/to/server.pem"
key = "/path/to/server.key"
ca = "/path/to/client-ca.pem"
client-auth = "REQUIRED"
}
}

Settings marked as inheritable flow from http {} to named sockets:

Setting GroupInherits to Named Sockets
concurrency-limitYes
content-encodingYes
protocolsYes
shutdownYes
idle-connection-period/timeoutYes
request-limitsYes
connection-limitsYes
corsYes
sslYes
requested-uri-discoveryYes

Override specific settings in a named socket:

http {
concurrency-limit.max-limit = 200
sockets.internal {
# Inherits concurrency-limit from http {}
port = 9001
# Override only content-encoding
content-encoding.gzip.enabled = false
}
}
VariableConfig PathDefaultDescription
SCRIBE_HTTP_PORThttp.port8080HTTP server port
SCRIBE_HTTP_HOSThttp.hostautoBind address (auto, localhost, 0.0.0.0)
SCRIBE_HTTP2_ENABLEDhttp.protocols.http-2.enabledfalseEnable HTTP/2
SCRIBE_HTTP_GZIP_ENABLEDhttp.content-encoding.gzip.enabledtrueEnable gzip compression
VariableConfig PathDefaultDescription
SCRIBE_REST_SOCKETchannels.rest.socket@defaultSocket for REST API
SCRIBE_MONITORING_SOCKETmonitoring.socket@defaultDefault socket for monitoring
SCRIBE_PROMETHEUS_SOCKETmonitoring.prometheus.socket(inherits)Socket for /metrics
SCRIBE_OBSERVE_SOCKETmonitoring.observe.socket(inherits)Socket for /observe/*
SCRIBE_HEALTH_SOCKETmonitoring.health.socket(inherits)Socket for health probes
VariableConfig PathDefaultDescription
SCRIBE_HTTP_SSL_ENABLEDhttp.ssl.enabledfalseEnable HTTPS
SCRIBE_HTTP_SSL_CERThttp.ssl.certServer certificate (PEM)
SCRIBE_HTTP_SSL_KEYhttp.ssl.keyPrivate key (PEM)
SCRIBE_HTTP_SSL_CAhttp.ssl.caCA cert for client verification
SCRIBE_HTTP_SSL_CLIENT_AUTHhttp.ssl.client-authNONENONE, OPTIONAL, REQUIRED
VariableConfig PathDefaultDescription
SCRIBE_HTTP_MAX_PAYLOAD_SIZEhttp.request-limits.max-payload-sizeunlimitedMax request body size
SCRIBE_HTTP_MAX_TCP_CONNECTIONShttp.connection-limits.max-tcp-connectionsunlimitedMax TCP connections
SCRIBE_HTTP_CONCURRENCY_MAXhttp.concurrency-limit.max-limitautoAIMD max permits

If upgrading from a version that used separate monitoring configuration:

Old ConfigNew Config
monitoring.hostnamehttp.host or http.sockets.<name>.host
monitoring.porthttp.port or http.sockets.<name>.port
channels.rest.listen[].porthttp.port or define named socket
channels.rest.listen[].hosthttp.host or define named socket
SCRIBE_MONITORING_PORTSCRIBE_HTTP_PORT or define named socket
SCRIBE_MONITORING_HOSTNAMESCRIBE_HTTP_HOST or define named socket

Before (old config):

monitoring {
hostname = "localhost"
port = 9001
}
channels.rest {
listen = [{ port = 8080, host = "0.0.0.0" }]
}

After (new config):

http {
port = 8080
host = "0.0.0.0"
sockets.internal {
port = 9001
host = "localhost"
}
}
monitoring.socket = "internal"
# REST uses @default automatically

Or for simpler deployments where everything shares one port:

http {
port = 8080
host = "0.0.0.0"
}
# Everything uses @default — no named sockets needed