Skip to content

All versions since 🚧 Unreleased

🚧 Unreleased Latest

Major release with REST and GraphQL APIs, MCP integration for AI assistants, OpenTelemetry-based observability, and a ready-to-run monitoring bundle. Multiple breaking changes to config keys, endpoints, and metrics. Plan an upgrade window.

Highlights

Breaking Changes

If you’re upgrading an existing deployment, start here: Upgrading IdentityScribe.

Environment Variable Prefix

BREAKING: Environment variable prefixes have been standardized.

  • IDENTITY_SCRIBE_* β†’ SCRIBE_*
  • SCRIBE_TELEMETRY_* β†’ SCRIBE_*

Migration:

Terminal window
# Old
IDENTITY_SCRIBE_READONLY=true
SCRIBE_TELEMETRY_TRACES_ENABLED=true
# New
SCRIBE_READONLY=true
SCRIBE_TRACES_ENABLED=true

See Upgrading: env prefix.

Logger Configuration Consolidated

BREAKING: Logger configuration has been consolidated to 5 canonical loggers with standardized environment variable overrides. Old logger config keys are removed.

Old ConfigNew ConfigPurpose
log.EventStorelog.IngestTranscription pipeline, event store
log.Scribelog.IngestTranscription pipeline
log.Settingslog.ConfigConfiguration parsing
log.LDAP(removed)Use segment/wide logging via Monitoring
log.AsyncSearch(removed)Use segment/wide logging via Monitoring
log.TaskExecutor(removed)Use segment/wide logging via Monitoring

New canonical loggers:

LoggerEnv OverridePurpose
log.SuperVisorSCRIBE_LOG_SUPERVISORStartup/shutdown, orchestration, infrastructure
log.IngestSCRIBE_LOG_INGESTTranscription pipeline, event store
log.MonitoringSCRIBE_LOG_MONITORINGWide-log output, observability, channels
log.LicenseSCRIBE_LOG_LICENSELicense verification
log.ConfigSCRIBE_LOG_CONFIGConfiguration parsing and validation

All loggers inherit from log.level by default. Set log.Monitoring = "off" to disable wide-log output entirely.

Migration:

# Old
log.EventStore = "debug"
log.Settings = "warn"
# New
log.Ingest = "debug"
log.Config = "warn"
Terminal window
# New environment variables
SCRIBE_LOG_INGEST=debug
SCRIBE_LOG_CONFIG=warn
SCRIBE_LOG_MONITORING=off # Disable wide-log output

See Upgrading: logging.

HTTP Configuration Restructured

BREAKING: All HTTP channels now use a unified socket model. Old monitoring and REST listener config keys are removed.

Old ConfigNew Config
monitoring.hostnamehttp.host or http.sockets.<name>.host
monitoring.porthttp.port or http.sockets.<name>.port
SCRIBE_MONITORING_PORTSCRIBE_HTTP_PORT or define named socket
SCRIBE_MONITORING_HOSTNAMESCRIBE_HTTP_HOST or define named socket

New configuration model:

  • Default socket binds to localhost:8080 (safe-by-default)
  • Named sockets defined in http.sockets.* for network separation
  • Channels reference sockets by name: channels.rest.socket, monitoring.socket

Migration:

# Old
monitoring {
hostname = "localhost"
port = 9001
}
# New (simple: everything on one port)
http {
port = 8080
host = "0.0.0.0"
}
# New (separated: monitoring on internal port)
http {
port = 8080
host = "0.0.0.0"
sockets.internal { port = 9001, host = "localhost" }
}
monitoring.socket = "internal"

See HTTP Server Guide for migration details and Upgrading: HTTP sockets.

Endpoint Path Changes

BREAKING: Status endpoints have been renamed. No aliases are provided.

Old PathNew Path
/status/observe/status
/healthy/observe/health or /healthz
/ (metrics)/metrics
/-/* (legacy monitoring endpoints)Removed

Migration: Update monitoring tools, dashboards, and Kubernetes probes to use the new paths. Use /observe/health/ready or /readyz for readiness-only checks.

Note: /observe now serves interactive OpenAPI documentation for the Observe endpoints.

See Upgrading: endpoints.

Health Endpoint Format

BREAKING: Health endpoints now return MicroProfile Health JSON format.

// Old /healthy response
{"healthy": true, "ready": true, "sync": {...}}
// New /observe/health/ready response
{"status": "UP", "checks": [{"name": "ingest", "status": "UP"}]}

Use /observe/health for combined MicroProfile health status.

Migration for Kubernetes:

# Old
livenessProbe:
httpGet:
path: /healthy
# New
livenessProbe:
httpGet:
path: /livez
readinessProbe:
httpGet:
path: /readyz
startupProbe:
httpGet:
path: /startedz

See Upgrading: endpoints.

Metric Naming Standardization

BREAKING: All metrics now use the scribe.* prefix with dot notation. Prometheus auto-converts to snake_case (scribe_*).

Migration: Update Prometheus queries and Grafana dashboards. See Observability for the full migration mapping.

Common renames:

OldNew
directory.query.timescribe.directory.query.time
channel.ldap.errorsscribe.channel.ldap.errors
hints.persistence.*scribe.hints.persistence.*
scribe.tasks.ratioscribe.tasks.pressure

See Upgrading: metrics names.

Virtual Attribute Syntax

BREAKING: Virtual attribute placeholders renamed for consistency.

  • {{current.*}} β†’ {{self.*}}

Migration: Update virtual attribute configurations. Startup fails with instructions if legacy syntax is found.

# Old
filter = "(member={{current.entryDN}})"
# New
filter = "(member={{self.entryDN}})"

See Upgrading: virtual attributes.

Query Scoping Validation

BREAKING: Broad searches that could span multiple entry types must now include an explicit type constraint. Ambiguous queries are rejected early with a clear, actionable error.

Migration:

  • Add a type constraint (for example entryType or a type-identifying condition such as objectClass) to any broad queries.
  • If you rely on wildcard/broad bases, test queries in staging and update filters/config accordingly.

See Upgrading: query scoping.

Error Code Naming

BREAKING: Error codes are now standardized to SCREAMING_SNAKE_CASE. If clients match on error codes, update them to the new values.

Migration: See Failures for HTTP/REST error codes, headers, and response schema.

Support workflow: When reporting issues, include Error-Id, error.timestamp, and (if present) Trace-Id and your Correlation-Id. See β€œReporting Issues to Support” in the Failures guide.

See Upgrading: error codes.

Transcribes Configuration Syntax

BREAKING: The transcribes configuration now uses object syntax where the key is the transcribe type:

transcribes {
user {
ldap { base = "ou=users,o=data", filter = "(objectClass=Person)" }
}
group {
ldap { base = "ou=groups,o=data", filter = "(objectClass=groupOfNames)" }
}
}

Legacy array syntax still works but logs a deprecation warning. Transcribes are processed alphabetically for deterministic behavior.

See Upgrading: transcribes.

Deprecations

Configuration Key Format

Configuration keys have been standardized to kebab-case. Legacy camelCase and underscore_case keys still work but log deprecation warnings.

Old KeyNew Key
ldap.bindDNldap.bind-dn
ldap.bindPasswordldap.bind-password
database.maxPoolSizedatabase.max-pool-size
channels.ldap.listen.tcpNoDelaychannels.ldap.listen.tcp-no-delay

Action: Update configuration files. Deprecated keys will be removed in a future major release. See the Configuration Reference for all current keys.

What’s New

Documentation Site

IdentityScribe now includes an embedded documentation site at /docs (on-prem) with /ref/* deep links into config/telemetry/error references, and publishes the same content as a public reference at scribe.identity-hub.io.

Improved 404 Handling

Unmatched HTTP routes now consistently return 404 Not Found instead of 500 Internal Server Error. A catch-all handler ensures clean 404 responses for any path that doesn’t match a registered route.

REST API Channel

A new REST/JSON channel provides HTTP access to directory data with OpenAPI documentation.

  • OpenAPI 3.1.0 specification with an interactive UI (Scalar) at /api
  • Collection endpoints at /api/entries/{type} with filtering, sorting, and pagination
  • Single entry lookup at /api/entries/{type}/{id} β€” accepts Global ID, UUID, UOID, or DN identifiers
  • Global ID field β€” entries include a Relay-style id field (base64url("{type}:{base36(uoid)}")) when operational attributes are selected
  • Multi-format filters β€” FleX, JSON, SCIM (RFC 7644), or LDAP (RFC 4515) syntax auto-detected
  • OpenAPI content negotiation β€” JSON, YAML, or HTML (Scalar UI) at /api based on Accept header
  • JSON mapping β€” attribute names preserve requested alias/case; wildcard selection (* / +) uses canonical catalog names
  • Parameters β€” list-type parameters (fields, sort, include) accept JSON arrays/objects in addition to comma/whitespace-separated strings
  • Conditional requests (LOOKUP) β€” ETag + If-None-Match β†’ 304 for efficient polling (ETags are weak validators because representations vary by fields)
  • Code samples β€” embedded curl, JavaScript, TypeScript, and Go examples in the OpenAPI spec
# REST binds to the unified HTTP server (`http {}`).
http {
host = "0.0.0.0"
port = 8080
}
channels.rest {
enabled = true
# socket = "api" # Optional: bind REST to a named socket
}

Or via environment: SCRIBE_REST_ENABLED=true SCRIBE_HTTP_PORT=8080

See the REST Channel Guide for endpoints, parameters, and examples.

  • REST Conditional Requests: Added Last-Modified response header and If-Modified-Since request support for timestamp-based cache validation. ETag remains the preferred validator. RFC 7232 precedence is enforced: If-None-Match takes precedence over If-Modified-Since.

  • REST Cache-Control Headers: Added explicit Cache-Control defaults:

    • Lookup: private, max-age=0, must-revalidate (enables conditional requests for authenticated/PII data)
    • Search: no-store (prevents caching of streaming/parameterized results)
  • REST Prefer Header (RFC 7240): Added Prefer: handling=strict|lenient support for attribute validation:

    • handling=lenient (default): Unknown attributes in fields/sort/filter are silently ignored
    • handling=strict: Unknown attributes cause 400 Bad Request with INVALID_ARGUMENT code listing all unknowns
    • Responses include Preference-Applied header when handling is explicitly requested
  • CORS Defaults: Default expose-headers now includes Cache-Control and Vary so browser clients can read caching headers.

GraphQL Channel

A new GraphQL channel provides flexible query access to directory data and change history with dynamic schema generation.

  • Dynamic schema generated from entry type configuration β€” types, connections, and query fields
  • Relay Node interface β€” all types implement Node with id: ID!; polymorphic lookup via node(id: ID!)
  • Collection queries with filtering, sorting, and cursor pagination
  • Connection counts β€” request estimated or exact totals via count(mode: ESTIMATED|EXACT) on entry and change connections (count-only queries skip data fetch). See GraphQL connection counts.
  • Single entry lookup β€” accepts Global ID, UUID, UOID, or DN
  • Change history API β€” global changes feed and per-entry changes field with filtering by time, type, and attributes
  • Temporal lookup β€” query entries at any point in time via the at argument
  • GraphiQL UI β€” interactive query builder at /graphql with schema explorer
  • Automatic Persisted Queries (APQ) β€” reduce bandwidth with query caching
  • Query security limits β€” depth and complexity limits protect against DoS

Enable via environment: SCRIBE_GRAPHQL_ENABLED=true

See the GraphQL Channel Guide for query patterns and configuration.

MCP Channel (Model Context Protocol)

AI coding assistants (Cursor, Claude Desktop, VS Code, Windsurf) can now query IdentityScribe directly from your IDE.

  • Data MCP (/mcp) β€” Search entries, browse change history, look up schemas, and access reference docs
  • Observe MCP (/observe/mcp) β€” Check health, diagnostics, and golden signals without leaving your editor

Enable via SCRIBE_MCP_ENABLED=true and SCRIBE_MONITORING_MCP_ENABLED=true.

See the MCP Channel Guide for setup, one-click install badges, and the full tool reference.

API Discovery

  • API Catalog at /.well-known/api-catalog (RFC 9727) lists all API channels and schema URLs
  • Schema endpoints β€” /api.json, /api.yaml, /graphql.sdl, /graphql.json for programmatic access
  • GraphQL error codes now include both code (IdentityScribe-specific) and graphqlCode (standard) for client compatibility
  • GraphQL performance β€” history queries only load requested fields (diff, patch, merge, meta) from the database

Rule-Based Log Filtering

Suppress log noise from fast/successful operations using glob patterns and LDAP-style filters:

monitoring.log.rules = [
{ action = exclude, where = "(&(scribe.result=ok)(duration.seconds<=50ms))" }
{ action = include, where = "(duration.seconds>=5s)" }
]

Rules evaluate first-match-wins. Unmatched operations fall back to monitoring.log.sample-rate.

See Observability: Wide Logs for syntax and examples.

FleX Query Language

FleX is a flexible filter syntax for runtime queries and configuration filters. Supports natural prefixes (status is active), lenient whitespace, logical combinators, IN lists, attribute groups, and all SCIM operators.

See the Filters Reference for syntax and examples.

OpenAPI for Operational Endpoints (/observe)

IdentityScribe now serves interactive OpenAPI documentation for the /observe/* endpoints at /observe (Scalar UI).

  • OpenAPI 3.1 spec: JSON at /observe?format=json, YAML at /observe?format=yaml
  • Scalar API Reference: HTML UI at /observe
  • Status JSON: /observe/status (see Breaking Changes)

Configuration inherits from the root openapi {} section with per-endpoint overrides:

openapi {
enabled = true
ui {
enabled = "auto" # UI: enabled in dev/test, disabled in prod
theme = "default"
}
}
# Example override for Observe
monitoring.observe.ui.theme = "purple"

UI defaults are app.mode-aware (auto) for both REST and Observe. Developer tools and the test request button are also auto-gated; override via openapi.ui.* or channels.rest.ui.* (env: SCRIBE_OPENAPI_UI_*, SCRIBE_REST_UI_*).

Portal and Operator UI

Built-in web interface for operators:

  • Portal (/) β€” System status, golden signals, and navigation to all features
  • Entries (/ui/entries) β€” Browse and search entries with point-in-time navigation
  • Changes (/ui/changes) β€” Change feed with filtering and diff viewing
  • Observe (/ui/observe) β€” Health dashboard with pressure gauges and JVM metrics

See the Monitoring Guide for all views and operational workflows.

Change History API

Two new REST endpoints provide access to change history:

  • GET /api/changes β€” Global change feed across all entries
  • GET /api/entries/{type}/{id}/changes β€” Change history for a specific entry

Both endpoints support time range filtering (since/until), event type filtering (type), attribute change filtering (affects), and standard pagination. Response shapes include nodes, edges, patch, merge, meta, and data options.

See the interactive API documentation at /api for full parameter details and examples.

Point-in-Time Lookup

Entry lookups now support temporal queries via the at parameter:

Terminal window
# Get entry state at a specific timestamp
GET /api/entries/users/{id}?at=2024-04-22T08:43:50Z
# Get entry state from 1 hour ago
GET /api/entries/users/{id}?at=1h
# Get entry state at a specific event (using cursor from change history)
GET /api/entries/users/{id}?at=AAFntW9XAAAAAAApQwAA

Use with the Change History API to navigate between versions.

Entry Verification Timestamp

Added verifiedTimestamp for tracking when an entry was last verified during reconciliation.

  • Accepted names: lastVerifiedAt, lastSeenVerified, verified_at, verifiedAt, verifiedTimestamp
  • LDAP name: verifiedTimestamp (matches createTimestamp / modifyTimestamp)
  • REST JSON: preserves the requested alias/case for attribute keys
  • Type: ISO-8601 in REST, GeneralizedTime in LDAP
  • Delegation: not delegated to upstream LDAP backends

Unified HTTP Server

All HTTP traffic now runs through a single WebServer with named sockets.

  • AIMD-based concurrency limiting with auto-derived limits from global concurrency setting
  • Request limits β€” max-payload-size and max-in-memory-entity per socket
  • Connection limits β€” max-tcp-connections and max-concurrent-requests per socket
  • CORS support β€” per-socket CORS with magic origins (same-origin, host, origin, *) and explicit origin lists
  • Gzip compression enabled by default
  • Graceful shutdown with configurable drain period
  • HTTP/2 support via SCRIBE_HTTP2_ENABLED
  • PII-safe telemetry defaults β€” common sensitive headers and client addresses are redacted in HTTP telemetry/logs by default
http {
request-limits {
max-payload-size = 10MiB # or unset for unlimited
max-in-memory-entity = 128KiB
}
connection-limits {
max-tcp-connections = 1000 # or unset for unlimited
max-concurrent-requests = 500
}
cors {
enabled = true
allow-origins = ["same-origin"] # or explicit: ["https://app.example.com"]
allow-credentials = false
}
}

See the HTTP Configuration Reference for the http {} configuration block.

OpenTelemetry-Native Observability

Migrated from Micrometer to OpenTelemetry SDK.

  • Prometheus at /metrics (OTel-backed)
  • OTLP export for traces and metrics to collectors like Grafana Alloy
  • Histogram exemplars link metrics to traces in Grafana
  • Wide event logs β€” one structured log per operation with configurable sampling
  • Pressure metrics β€” single-number health indicators (<1 = healthy, β‰₯1 = contention)
monitoring {
traces.enabled = true
exemplars = "traceBased"
}

See the Monitoring Guide for setup and Observability for metrics inventory.

Monitoring Bundle

IdentityScribe now ships a ready-to-run monitoring bundle (Grafana dashboards, Docker Compose, and a Helm chart). It’s included in the distribution under monitoring/.

See the Monitoring Guide for setup and deployment options.

MicroProfile Health Endpoints

Health checks now follow MicroProfile Health 4.0 with Kubernetes-friendly aliases.

EndpointPurpose
/readyzReadiness (plain text)
/livezLiveness (plain text)
/startedzStartup (plain text)
/healthzCombined health (plain text)
/observe/healthCombined health (JSON)
/observe/health/readyReadiness (JSON)
/observe/health/liveLiveness (JSON)
/observe/health/startedStartup (JSON)
/observe/health/check/{name}Single health check (JSON)

Operational Endpoints

New endpoints for operational visibility:

EndpointPurpose
/observe/statusBasic status with uptime
/observe/doctorHealth report with threshold checks and recommendations
/observe/pressurePressure metrics with actionable hints
/observe/servicesService lifecycle status and uptime
/observe/indexesIndex build status and concurrent build detection
/observe/channelsEnabled channels, sockets, and runtime binding info
/observe/configResolved configuration (passwords redacted)
/observe/licenseLicense status and renewal request
/observe/hintsPersisted hints and recommendations
/observe/signaturesQuery signature inventory
/observe/stats/valuesValue size statistics per entry type and attribute
/observe/stats/entriesEntry blob size percentiles per entry type
/observe/stats/eventsEvent rate windows (dashboard-friendly buckets)
/observe/stats/ingestIngest lag and checkpoint positions

Readiness probes now incorporate index build status; use /observe/indexes to diagnose readiness delays.

Notes:

  • /observe/hints is available only when hint persistence is enabled.
  • /observe/signatures requires EXPLAIN sampling to be enabled.
  • /observe/doctor and /observe/pressure require metrics recording to be enabled.

Connection Hints

Tune PostgreSQL session parameters per-channel for different workload profiles.

database.connection-hints {
statement-timeout = 60s
work-mem = "256MB"
}
channels.rest.connection-hints {
statement-timeout = 30s # Override for REST only
}

Environment variables: SCRIBE_REST_STATEMENT_TIMEOUT, SCRIBE_LDAP_WORK_MEM, etc.

Virtual Attribute Delegation

Virtual attributes can now be forwarded to upstream LDAP backends when they support the attribute.

ldap.virtualAttributes {
memberOf {
filter = "(&(entryType=group)(member={{self.entryDN}}))"
value = "{{other.entryDN}}"
delegatable = true # Forward to backend
}
securityEquals {
# ...
delegatable = "equivalentToMe" # Rewrite name when delegating
}
}

Virtual Attribute Type Scoping

Virtual attributes can now be scoped to specific entry types via the types field:

ldap.virtual-attributes {
memberOf {
types = ["user"] # Only expose on user type
filter = "(&(entryType=group)(member={{self.entryDN}}))"
value = "{{other.entryDN}}"
}
}

Omit types to expose on all types (backward compatible). Transcribe-level VAs are automatically scoped to their transcribe’s type. REST strict mode (Prefer: handling=strict) and GraphQL schema validation enforce type scoping at runtime.

Note: Transcribe-level VAs are now scoped to their defining transcribe by default. See the LDAP Configuration Reference for details.

Query Planner v2

The SQL generation engine has been rewritten for better PostgreSQL performance.

  • Smart index utilization with truncation-aware patterns for long text values
  • Stable query plans through padded IN-lists (up to 8 values) and array operators for larger sets
  • Virtual attribute support including nested cross-references
  • Cursor-based pagination with efficient forward/backward paging

Backpressure Under Query Load

Under high query load, IdentityScribe applies backpressure to protect the database:

  • HTTP channels (REST/GraphQL) fail fast with 503 UNAVAILABLE and Retry-After when the system is busy
  • LDAP blocks up to the configured time limit (rather than failing fast)

Tune via database.query-http-acquisition-timeout (see Database Configuration).

Transcription Throughput Tuning

Transcription uses a bounded worker pool with a bounded queue to reduce memory pressure under load.

  • Configure per-transcribe throughput with transcription.workers and transcription.queue-capacity (defaults are {auto})
  • See the Transcribes Configuration Reference for tuning guidance and defaults

Sortable Index Configuration

Attributes listed in indices.sortable now receive sortable indexes even if they are single-valued, improving ORDER BY performance.

If you previously relied on automatic filtering of single-valued attributes, re-check your index configuration (see the Configuration Reference).

Platform Support (PostgreSQL + Toolchain)

  • PostgreSQL: 18 is the primary target, 17 is fully supported, and 15/16 are deprecated compatibility targets

  • Native binary: the distributed artifact is a native executable (no Java runtime required to run)

  • Building from source: the build toolchain targets Java 25 / GraalVM 25

  • 50-60% faster startup through parallel service initialization

  • Non-blocking index builds via CREATE INDEX CONCURRENTLY

Logging and Operator Experience

Improved startup banner, log formatting, and sampling behavior.

Startup banner now shows mode, license, config sources, backend URLs, channel bindings, and portal URLs. Printed after services are running, so port bindings are accurate:

identity-scribe v1.2.3 ready in 847ms
┃ Mode production
┃ License ACME Corp β€” expires 2027-01-15 (365d)
┃ Readonly true
┃ Config
┃ /etc/scribe/identity-scribe.conf
┃ environment variables
┃ Database postgresql://db.example.com:5432/scribe (ssl)
┃ LDAP ldaps://dc01.example.com:636
┃ Channels
┃ REST API http://localhost:8080/api
┃ LDAP ldap://localhost:10389

Other improvements:

  • PRETTY format uses ANSI colors for result status (green/yellow/red) and attribute prefixes. Auto-detected; disable with NO_COLOR=1.
  • Exception bypass β€” operations with exceptions now bypass log sampling (same as warnings)
  • Third-party loggers β€” HikariCP, Flyway, and others are configurable via SCRIBE_LOG_* env vars

See the Configuration Guide for log format options.

Failure Handling and Support

The Failures guide includes a β€œReporting Issues to Support” checklist, including which IDs/headers to include (Error-Id, Trace-Id, Correlation-Id).

Log Noise Reduction

Reduced log verbosity during initial sync and routine operations:

  • Default noise gates β€” Hints.* and Metrics.* suppressed unless slow or failing
  • Queue wait threshold β€” monitoring.log.queue-wait-threshold (default: 90s) controls when warnings emit
  • Segment leak suppression β€” automatically suppressed during initial sync
  • LDAP search events β€” short-lived segments instead of long-lived parent, eliminating leak warnings

Fixes

  • Graceful shutdown β€” HikariPool connections are now soft-evicted before pool closure, eliminating timeout warnings
  • Timezone handling β€” Fixed timestamp drift in non-UTC JVM timezones; JDBC connections now set PostgreSQL session timezone to UTC
  • LDAP virtual attribute delegation β€” Virtual attributes are forwarded correctly during backend delegation instead of being rejected
  • Virtual attribute warnings β€” Fixed a false warning for self-referencing virtual attributes; the entryType warning now only triggers for cross-referencing templates
  • Pagination metadata β€” Cursor pagination now correctly indicates whether more results exist before/after the current page
  • Backward paging β€” Backward navigation streams efficiently without buffering entire pages
  • Time limit enforcement β€” Search time limits now cancel database work promptly
  • Query cancellation β€” Failed or timed-out requests cancel remaining database work immediately