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.
| State | Controlled by | Default | When it is off |
|---|---|---|---|
| Channel enabled — data routes served | channels.rest.enabled, channels.graphql.enabled | off | /api/entries/... or /graphql return 404 |
| Schema exposed — spec files served | channels.rest.openapi.enabled, channels.graphql.introspection.enabled | auto (off in production) | /api.json, /api.yaml, /graphql.sdl return 404 |
| Docs UI exposed — browsable docs served | channels.rest.ui.enabled, channels.graphql.ui.enabled | auto (off in production) | /api and GraphiQL return 404 |
| Authenticated — caller is allowed | auth.enabled and access rules | off | Requests without valid credentials get 401/403 |
| Reachable socket — listening on your port | channels.rest.socket, site.socket, monitoring.observe.socket | @default (8080) | A request to the wrong port is refused or 404 |
| Directory populated — entries exist | transcribe config and initial sync | n/a | Searches succeed but return no rows until sync converges |
Enable REST data routes
Section titled “Enable REST data routes”The REST channel is disabled by default. Turn it on to serve /api/entries/...:
channels.rest.enabled = trueOr with an environment variable:
SCRIBE_REST_ENABLED=trueVerify the data routes answer:
# Channel status — expect rest.enabled: truecurl -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.
Production API documentation
Section titled “Production API documentation”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.
| Endpoint | Purpose | Setting |
|---|---|---|
/api | Browsable, interactive REST documentation | channels.rest.ui.enabled |
/api.json, /api.yaml, /openapi.json, /openapi.yaml | OpenAPI specification files | channels.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 = truechannels.rest.openapi.enabled = true
# 3. Keep documentation off (recommended for production) and rely on# the data routes plus the Operator UIEnable GraphQL
Section titled “Enable GraphQL”GraphQL is a separate channel with its own three states. Enable the query endpoint:
channels.graphql.enabled = trueSCRIBE_GRAPHQL_ENABLED=true| Endpoint | Purpose | Setting | Default |
|---|---|---|---|
/graphql (POST) | Run queries | channels.graphql.enabled | off |
/graphql (GET, browser) | GraphiQL explorer | channels.graphql.ui.enabled | auto |
/graphql.sdl, /graphql.json | Schema and introspection | channels.graphql.introspection.enabled | auto |
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.
Socket model
Section titled “Socket model”Channels share one server, but you control what listens where using named sockets.
How HTTP sockets bind to channels
socket = By default, everything uses the @default socket on port 8080.
@default socket
Section titled “@default socket”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}}Named sockets
Section titled “Named sockets”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.
Channel socket binding
Section titled “Channel socket binding”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"]Common patterns
Section titled “Common patterns”Single socket (default)
Section titled “Single socket (default)”Everything on one port:
http { port = 8080 host = "0.0.0.0"}Monitoring separation
Section titled “Monitoring separation”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.
Dev: dual-socket monitoring
Section titled “Dev: dual-socket monitoring”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.
Operator UI, Observe, and sockets
Section titled “Operator UI, Observe, and sockets”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 = falseSite 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 = truemonitoring.enabled = trueandmonitoring.observe.enabled = true- every socket in
site.socketmust also be present in bothchannels.rest.socketandmonitoring.observe.socket
1) One socket (@default) - valid baseline
Section titled “1) One socket (@default) - valid baseline”channels.rest.enabled = truesite.socket = "@default"channels.rest.socket = "@default"monitoring.observe.socket = "@default"2) Two sockets (public + internal)
Section titled “2) Two sockets (public + internal)”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 = truesite.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 @defaultStartup 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 = truechannels.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 = truechannels.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 = truechannels.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 = truechannels.rest.socket = "internal"monitoring.observe.socket = "internal"Add authentication
Section titled “Add authentication”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:
SCRIBE_AUTH_ENABLED=trueSCRIBE_AUTH_ISSUER=https://auth.example.com/realms/mycompanyWith 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.
A complete first-run config
Section titled “A complete first-run config”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 = truechannels.graphql.enabled = trueThe 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:
SCRIBE_HTTP_SSL_ENABLED=trueSCRIBE_HTTP_SSL_CERT=/path/to/server.pemSCRIBE_HTTP_SSL_KEY=/path/to/server.keyRequire 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.
Troubleshooting
Section titled “Troubleshooting”/api returns 404
Section titled “/api returns 404”Most often this is expected: interactive documentation is auto, and the default run mode is production, so /api is hidden.
- Confirm the REST channel itself is on and serving data — this is the part that matters:
A
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/users200on the data route means REST is healthy and/apibeing404is just hidden documentation. - If you want the documentation page, run with
--mode dev, or setchannels.rest.ui.enabled = true(see Production API documentation). - If the data route is also
404, the channel is disabled — setchannels.rest.enabled = true.
The UI shows no entries
Section titled “The UI shows no entries”The Operator UI does not need public API documentation. Check, in order:
- REST disabled —
curl -s -H "Authorization: Bearer <token>" http://localhost:8080/observe/channels | jq '.channels.rest.enabled'must betrue. - 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).
- Not authenticated — if authentication is on, an unauthenticated UI session shows an access error rather than entries. Sign in or supply credentials.
- Directory still syncing or empty —
curl -s -H "Authorization: Bearer <token>" http://localhost:8080/observe/convergence | jqshows whether initial sync has finished. A correctly configured but freshly started instance can legitimately have zero entries until it converges.
Requests return 401 or 403
Section titled “Requests return 401 or 403”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.
A request hits the wrong socket
Section titled “A request hits the wrong socket”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.
# 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:
- Confirm site serving is on —
/,/docs, and/uirequiresite.enabled = true; the documentation specifically requiressite.docs.enabled = true. Both default to on. - Confirm the socket — site routes follow
site.socket(default@default). Call them on the same port the UI uses. - 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. - Use the bundled reference — the
reference.conffile in the archive documents every configuration key with its default, independent of the docs site. - Ask the server directly —
/observe/channelsreports enabled channels and sockets, and/observe/configreturns the resolved configuration with secrets redacted, so you can diagnose setup even with no docs UI at all.
Related
Section titled “Related”- Start Here — Full install-to-first-query walkthrough
- REST Channel — Query syntax, filters, exports
- GraphQL Channel — Schema generation and queries
- Authentication — Bearer tokens, ROPC, LDAP bind, access rules
- Production Checklist — Schema exposure, TLS hardening, network isolation
- HTTP configuration reference — All socket settings, env vars, inheritance rules
- SSL configuration reference — Cipher suites, protocols, certificate options
- Endpoints Reference — Health probes and the Observe API