Skip to content

API Access & Networking

IdentityScribe runs a single HTTP server for all traffic — REST, GraphQL, MCP, monitoring, and the Operator UI — but the data APIs are opt-in. On a fresh install the REST and GraphQL channels are off, and interactive API documentation is hidden because the server starts in production mode. This page is the single place to turn endpoints on, understand why one might return 404, choose which port to expose, and lock it all down.

Work through it top to bottom for first-run setup, or jump to Troubleshooting if something is already not responding.

Six states decide whether an endpoint answers

Section titled “Six states decide whether an endpoint answers”

A request succeeds only when all of the following line up. Each is a separate setting, so an endpoint can be "almost on" and still return 404 or 401.

StateControlled byDefaultWhen it is off
Channel enabled — data routes servedchannels.rest.enabled, channels.graphql.enabledoff/api/entries/... or /graphql return 404
Schema exposed — spec files servedchannels.rest.openapi.enabled, channels.graphql.introspection.enabledauto (off in production)/api.json, /api.yaml, /graphql.sdl return 404
Docs UI exposed — browsable docs servedchannels.rest.ui.enabled, channels.graphql.ui.enabledauto (off in production)/api and GraphiQL return 404
Authenticated — caller is allowedauth.enabled and access rulesoffRequests without valid credentials get 401/403
Reachable socket — listening on your portchannels.rest.socket, site.socket, monitoring.observe.socket@default (8080)A request to the wrong port is refused or 404
Directory populated — entries existtranscribe config and initial syncn/aSearches succeed but return no rows until sync converges

The REST channel is disabled by default. Turn it on to serve /api/entries/...:

channels.rest.enabled = true

Or with an environment variable:

Terminal window
SCRIBE_REST_ENABLED=true

Verify the data routes answer:

Terminal window
# Channel status — expect rest.enabled: true
curl -s -H "Authorization: Bearer <token>" http://localhost:8080/observe/channels | jq '.channels.rest'
# Search entries (replace "users" with one of your entry types)
curl -s http://localhost:8080/api/entries/users | jq '.edges | length'

Data routes work on their own — they do not require API documentation or schema endpoints to be exposed. For the full query surface (filters, sorting, pagination, exports), see the REST Channel reference.

Interactive documentation and schema files are documentation, not data. They are controlled separately from the channel and default to auto, which means off in production.

EndpointPurposeSetting
/apiBrowsable, interactive REST documentationchannels.rest.ui.enabled
/api.json, /api.yaml, /openapi.json, /openapi.yamlOpenAPI specification fileschannels.rest.openapi.enabled

Because the default run mode is production, /api returns 404 on a fresh install even after you enable the REST channel. This is expected and safe — it keeps your schema private. Your data routes under /api/entries/... keep working, and the Operator UI still lists entry types and lets you search without any public documentation enabled.

You have three options:

# 1. Evaluation: run in development mode so auto endpoints turn on
# ./identity-scribe-* --mode dev
# 2. Expose documentation explicitly, even in production (publishes your schema)
channels.rest.ui.enabled = true
channels.rest.openapi.enabled = true
# 3. Keep documentation off (recommended for production) and rely on
# the data routes plus the Operator UI

GraphQL is a separate channel with its own three states. Enable the query endpoint:

channels.graphql.enabled = true
Terminal window
SCRIBE_GRAPHQL_ENABLED=true
EndpointPurposeSettingDefault
/graphql (POST)Run querieschannels.graphql.enabledoff
/graphql (GET, browser)GraphiQL explorerchannels.graphql.ui.enabledauto
/graphql.sdl, /graphql.jsonSchema and introspectionchannels.graphql.introspection.enabledauto

As with REST, the GraphiQL explorer and schema introspection are auto, so they are off in production. Queries against /graphql keep working regardless. See the GraphQL Channel reference for query syntax.

Channels share one server, but you control what listens where using named sockets.

Socket Model

How HTTP sockets bind to channels

HTTP Sockets
@default :8080
auto
admin :9090
optional
Channels
REST API
GraphQL
MCP
Monitoring
Default binding
Override via socket =

By default, everything uses the @default socket on port 8080.

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

http {
port = 8080
port = ${?SCRIBE_HTTP_PORT}
# "auto" → localhost in dev, 0.0.0.0 in production
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 {}. Only override what differs.

Channels reference sockets by name. Unspecified channels use @default.

channels.rest {
enabled = true
socket = ${?SCRIBE_REST_SOCKET} # default: @default
}
monitoring {
socket = ${?SCRIBE_MONITORING_SOCKET} # default for all monitoring
prometheus.socket = ${?SCRIBE_PROMETHEUS_SOCKET} # per-endpoint override
observe.socket = ${?SCRIBE_OBSERVE_SOCKET}
health.socket = ${?SCRIBE_HEALTH_SOCKET}
}

Channels can bind to multiple sockets:

monitoring.socket = ["@default", "internal"]

Everything on one port:

http {
port = 8080
host = "0.0.0.0"
}

Public APIs on one port, monitoring on another:

http {
port = 8080
host = "0.0.0.0"
sockets.internal {
port = 9001
host = "localhost"
}
}
monitoring.socket = "internal"

The bundled monitoring stack (monitoring/docker, monitoring/helm) uses this pattern.

During development, expose monitoring on both sockets to avoid 404 confusion:

http {
port = 8080
sockets.monitoring { port = 9001 }
}
monitoring {
socket = "monitoring"
observe.socket = ["@default", "monitoring"]
health.socket = ["@default", "monitoring"]
prometheus.socket = ["@default", "monitoring"]
}

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

The Operator UI at /ui and the Observe dashboard at /ui/observe are enabled by default. The UI reads live entry types and status directly from the server, so it shows your entries even when public API documentation is disabled.

The UI depends on two other endpoints being reachable on the same socket:

  • REST (/api/...) — for entry browsing and search
  • Observe (/observe/...) — for status and health

This is enforced at startup. When the UI is enabled, the server refuses to start unless REST and Observe are enabled and bound to every socket the UI uses. On the default single-socket setup this is automatic; the moment you split traffic across named sockets you must keep them aligned. If you do not need the web UI (headless or service-only deployments), turn it off instead:

site.ui.enabled = false

Site UI socket topology (1/2/3 socket examples)

Section titled “Site UI socket topology (1/2/3 socket examples)”

When site.ui.enabled is true, startup enforces this fail-closed contract:

  • channels.rest.enabled = true
  • monitoring.enabled = true and monitoring.observe.enabled = true
  • every socket in site.socket must also be present in both channels.rest.socket and monitoring.observe.socket
channels.rest.enabled = true
site.socket = "@default"
channels.rest.socket = "@default"
monitoring.observe.socket = "@default"

Valid: UI and REST stay on public (@default), while monitoring endpoints can also be exposed on internal.

http {
port = 8080 # public
sockets.internal {
port = 9001
host = "localhost"
}
}
channels.rest.enabled = true
site.socket = "@default"
channels.rest.socket = "@default"
monitoring.observe.socket = ["@default", "internal"]
monitoring.prometheus.socket = "internal"
monitoring.health.socket = "internal"

Invalid: UI on public, but observe only on internal (startup fails).

site.socket = "@default"
channels.rest.socket = "@default"
monitoring.observe.socket = "internal" # missing @default

Startup suggests two explicit remediation paths:

# Option A: keep UI on public.
# This exposes /api and /observe on both public and internal sockets.
channels.rest.enabled = true
channels.rest.socket = ["@default", "internal"]
monitoring.observe.socket = ["@default", "internal"]
# Option B: keep Observe internal.
# Move the UI and REST API to the internal socket with Observe.
site.socket = "internal"
channels.rest.enabled = true
channels.rest.socket = "internal"
monitoring.observe.socket = "internal"

3) Three sockets (public + internal + site)

Section titled “3) Three sockets (public + internal + site)”

Remediation for invalid cases:

# Option A: keep UI on the dedicated site socket.
# This exposes /api and /observe on every socket already used by UI, REST, or Observe.
channels.rest.enabled = true
channels.rest.socket = ["site", "@default", "internal"]
monitoring.observe.socket = ["site", "@default", "internal"]
# Option B: keep Observe internal.
# Move the UI and REST API to the internal socket with Observe.
site.socket = "internal"
channels.rest.enabled = true
channels.rest.socket = "internal"
monitoring.observe.socket = "internal"

Application API authentication is off by default, while Observe endpoints require authentication by default because they expose operational data. Before exposing the server, turn application API authentication on too:

Terminal window
SCRIBE_AUTH_ENABLED=true
SCRIBE_AUTH_ISSUER=https://auth.example.com/realms/mycompany

With authentication on, requests without valid credentials receive 401, and requests that are authenticated but not permitted receive 403. Method selection (bearer tokens, ROPC, LDAP bind) and access rules are covered in the Authentication guide, and the go-live steps in the Production Checklist.

This minimal config serves REST and GraphQL data routes, keeps the Operator UI working, and stays on the default socket. It boots in production mode with documentation hidden; add --mode dev while evaluating to browse /api and GraphiQL.

database.url = "jdbc:postgresql://localhost:5432/scribe"
database.user = "scribe"
database.password = ${DB_PASSWORD}
ldap.url = "ldap://your-ldap-server:389"
ldap.bind-dn = "cn=reader,dc=example,dc=com"
ldap.bind-password = ${LDAP_BIND_PASSWORD}
transcribes {
users {
ldap {
base = "ou=users,dc=example,dc=com"
filter = "(objectClass=person)"
scope = sub
attributes = "cn, sn, givenName, mail, telephoneNumber"
}
}
}
# Serve the HTTP API channels (both are off by default)
channels.rest.enabled = true
channels.graphql.enabled = true

The Start Here walkthrough uses this shape end to end. For every available key, see the Configuration Reference.

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

Or via environment variables:

Terminal window
SCRIBE_HTTP_SSL_ENABLED=true
SCRIBE_HTTP_SSL_CERT=/path/to/server.pem
SCRIBE_HTTP_SSL_KEY=/path/to/server.key

Require client certificates for API access:

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"
}
}

client-auth options: NONE (default), OPTIONAL, REQUIRED.

Most often this is expected: interactive documentation is auto, and the default run mode is production, so /api is hidden.

  1. Confirm the REST channel itself is on and serving data — this is the part that matters:
    Terminal window
    curl -s -H "Authorization: Bearer <token>" http://localhost:8080/observe/channels | jq '.channels.rest'
    curl -s -o /dev/null -w '%{http_code}\n' http://localhost:8080/api/entries/users
    A 200 on the data route means REST is healthy and /api being 404 is just hidden documentation.
  2. If you want the documentation page, run with --mode dev, or set channels.rest.ui.enabled = true (see Production API documentation).
  3. If the data route is also 404, the channel is disabled — set channels.rest.enabled = true.

The Operator UI does not need public API documentation. Check, in order:

  1. REST disabledcurl -s -H "Authorization: Bearer <token>" http://localhost:8080/observe/channels | jq '.channels.rest.enabled' must be true.
  2. Wrong socket — the UI, REST, and Observe must share a socket. Compare the socket the UI loaded from against the REST and Observe sockets (see A request hits the wrong socket).
  3. Not authenticated — if authentication is on, an unauthenticated UI session shows an access error rather than entries. Sign in or supply credentials.
  4. Directory still syncing or emptycurl -s -H "Authorization: Bearer <token>" http://localhost:8080/observe/convergence | jq shows whether initial sync has finished. A correctly configured but freshly started instance can legitimately have zero entries until it converges.

Authentication is on and the request is missing or failing credentials (401), or is authenticated but denied by an access rule (403). Supply a valid token or fix the rule — see Authentication — Access rules. Observe endpoints also have monitoring.observe.auth.enabled, which defaults to on. To confirm whether application API auth is enabled, check your auth.enabled setting and the Production Checklist.

Every endpoint listens on a named socket; the default is @default on port 8080. A request to a port where the channel is not bound is refused or returns 404.

Terminal window
# Which sockets exist, and which channel is on which socket?
curl -s -H "Authorization: Bearer <token>" http://localhost:8080/observe/channels | jq '{sockets, channels}'

Match the port you are calling against the socket the channel reports. If you split traffic across named sockets, keep the Operator UI, REST, and Observe aligned as shown in Site UI socket topology.

The local docs or home page does not render

Section titled “The local docs or home page does not render”

The server serves its home page at /, this documentation at /docs, and the Operator UI at /ui. If any of these is blank or 404:

  1. Confirm site serving is on/, /docs, and /ui require site.enabled = true; the documentation specifically requires site.docs.enabled = true. Both default to on.
  2. Confirm the socket — site routes follow site.socket (default @default). Call them on the same port the UI uses.
  3. Use the offline copy — every release archive ships a self-contained docs/ folder. Open it directly from the unpacked archive when the served site is unavailable.
  4. Use the bundled reference — the reference.conf file in the archive documents every configuration key with its default, independent of the docs site.
  5. Ask the server directly/observe/channels reports enabled channels and sockets, and /observe/config returns the resolved configuration with secrets redacted, so you can diagnose setup even with no docs UI at all.