Configuration Options
Quick reference for all harper-config.yaml top-level sections.
For how to apply configuration (YAML file, environment variables, CLI, Operations API), see Configuration Overview.
http
Configures the Harper component server (HTTP, REST API, WebSocket). See HTTP Configuration for full details.
http:
port: 9926
securePort: 4443
cors: true
timeout: 120000
mtls: false
logging:
level: info
path: ~/hdb/log/http.log
sessionAffinity— Route requests from same client to same worker thread (ipor header name)compressionThreshold— Response size threshold (bytes) for Brotli compression;0disables compression; Default:0cors— Enable CORS; Default:truecorsAccessList— Allowed origins for CORS requests. From v5.4.0, an entry such ashttps://*.app.example.comallows any one label in place of*; seehttp.corsAccessListcorsAccessControlAllowHeaders—Access-Control-Allow-Headersvalue for OPTIONS preflightheadersTimeout— Max wait for complete HTTP headers (ms); Default:60000maxHeaderSize— Max HTTP header size (bytes); Default:16394requestQueueLimit— Max estimated request queue time (ms) before 503; Default:20000keepAliveTimeout— Inactivity before closing keep-alive connection (ms); Default:30000port— HTTP port; Default:9926securePort— HTTPS port; requires TLS configuration; Default:nullhttp2— Enable HTTP/2; Default:false(Added in: v4.5.0)timeout— Request timeout (ms); Default:120000mtls— Enable mTLS authentication for incoming connections; sub-options:user,required,certificateVerification(see Certificate Verification)logging— HTTP request logging (disabled by default, Added in: v4.6.0); sub-options:level,path,timing,headers,id. See Logging Configuration
threads
Worker thread pool configuration.
threads:
count: 11
maxHeapMemory: 300
count— Number of worker threads; Default: CPU count minus onemaxHeapMemory— Heap limit per thread (MB). When unset, each thread's limit is derived from available memory and the number of HTTP worker threads, including isolated applications' threadsmaxIsolatedAdded in: v5.3.0 Changed in: v5.4.0 — How many isolated applications may run, each in a dedicated worker thread; Default:8. Applications with a running dedicated worker keep it, and the rest are admitted in root-config order up to this limit. An isolated application past it is not loaded anywhere. Adeploy_componentof one that would not get a worker is refused with a409. An application whose live release failed closed gets no worker and does not count. Before v5.4.0, every isolated application on disk counted, and a deploy of one that boot order would not admit could be accepted and then never start a worker.0admits none. Dedicated threads are added tocount, not taken from it, so a node runs up tocount+maxIsolatedHTTP worker threads. WhenmaxHeapMemoryis unset, they all count when Harper divides available memory into each thread's default heap limit. Each one lowers that limit for threads started afterward, until it reaches its 512 MB floor, after which more threads add to the total heap the node may useheapSnapshotNearLimit— Write a.heapsnapshotfile when a thread nears its heap limit (loadable in Chrome DevTools Memory tab); Default:false. See Worker Thread Debuggingdebug— Enable Node.js inspector; sub-options:port,startingPort,host,waitForDebugger. See Worker Thread DebuggingpreloadAdded in: v5.2.0 — Module, or list of modules, to load via Node's--importbefore any Harper or application module on each worker thread. Intended for instrumentation and APM agents. Worker threads only (not under Bun).preloadRequireAdded in: v5.2.0 — The same, but via Node's--require(CommonJS). Worker threads only (not under Bun).v8FlagsAdded in: v5.4.0 — V8 flag, or list of V8 flags, applied to the whole Harper process before its first worker thread starts. Each entry must start with--. Takes effect on process start. Not applied under Bun. See Applying V8 flagsshutdownTimeoutAdded in: v5.4.0 — Longest time (ms) Harper spends shutting down after SIGTERM, SIGINT or SIGQUIT before it exits anyway; Default:8000(30000underharper dev).0exits as soon as the signal arrives. Takes effect when Harper restarts. See Shutting down on a termination signal
Preloading an instrumentation agent
An APM or instrumentation agent has to load before the code it instruments. These two keys put a module on each worker thread's startup, ahead of Harper's own modules and your application's.
The two keys differ in how the module is loaded, and that determines what it can do. --import evaluates the module as ESM, which is how an agent installs Node's module loader hooks so modules loaded later by import can be instrumented. --require runs the module's body, which is how an agent's initialization entry starts it.
Installing loader hooks and starting an agent are separate jobs, and which of an agent's entry points does which is specific to that agent. Some ship a single entry that does both; others split them across an --import entry and a --require entry. When they are split, set both keys.
Bare specifiers resolve against the node_modules of your installed components, so an agent can ship as a dependency of a deployed component. Absolute paths also work.
Follow your agent's own documentation for worker-thread setup, and confirm the telemetry you expect actually reaches your collector.
Example: a split-entry agent
dd-trace is a split-entry agent, which makes it a useful illustration. On dd-trace 6.x, dd-trace/register.js installs the ESM loader hooks but never calls init(), so preload on its own leaves the tracer uninitialized — it still hands out spans with plausible trace ids, but they are no-ops and nothing is exported. dd-trace/init is the entry that calls init(). Setting both keys covers both jobs:
threads:
preloadRequire: dd-trace/init # the entry that calls init()
preload: dd-trace/register.js # ESM loader hooks for automatic instrumentation
dd-trace/initialize.mjs looks like a single-entry shortcut around that pairing, but it is not one on a worker thread: it gates both its init() call and its loader-hook registration behind isMainThread, so under --import on a worker it starts nothing and registers nothing.
An agent that is one of several preloaded modules still needs its own pairing:
threads:
preloadRequire: dd-trace/init # the entry that calls init()
preload:
- dd-trace/register.js
- /opt/instrumentation/agent.mjs
The dd-trace behavior described here was observed on 6.x. Check your agent's current documentation, and confirm your spans arrive at your collector.
Applying V8 flags
Some V8 flags, such as --optimize-for-size, cannot be passed through NODE_OPTIONS (Node refuses them there) or to a worker thread. v8Flags applies them inside Harper instead: the main thread calls Node's v8.setFlagsFromString() for each entry before the first worker thread starts, so every worker thread runs with them. V8 flags are process-wide, so they also affect the main thread from that point on. The main thread's isolate already exists, though, so flags V8 reads only when an isolate is created (such as heap sizing) do not change it. With count: 0, the main thread serves requests, so those creation-time flags do not affect request handling; flags V8 reads while running still apply.
threads:
v8Flags:
- --optimize-for-size
- --max-semi-space-size=1
The value can also be set from the environment, for example HARPER_SET_CONFIG='{"threads":{"v8Flags":["--optimize-for-size","--max-semi-space-size=1"]}}'.
- Validation. Surrounding whitespace is trimmed, and an empty entry is ignored. An entry that does not start with
--is refused byset_configuration, and stops Harper at startup with an error namingthreads.v8Flags. V8 skips a flag it does not recognize and only printsError: unrecognized flagto standard error, so check the spelling against your Node version (node --v8-options). - Implied flags are not applied. On the command line, some flags turn on others;
--optimize-for-sizealso caps the young-generation semi-space at 1 MB. Applied at runtime, a flag does not do that, so list the implied flags explicitly, as the example does with--max-semi-space-size=1. - When it takes effect. Flags are read once per process. A changed value takes effect when the whole process restarts: the
restartoperation, which restarts only the node that receives it, or a process or container restart.restart_servicerestarts only worker threads, which keep the process's flags, so it does not apply a change, even with"replicated": true. To roll back, remove the setting fromHARPER_SET_CONFIGorHARPER_CONFIGif it came from there, and fromharper-config.yaml(for exampleset_configurationwith"threads_v8Flags": null), then restart the process. ATHREADS_V8FLAGSenvironment variable is written intoharper-config.yamlat every startup, so unset it as well; unsetting the variable alone leaves the flags in the file. - Expert setting. Flags change V8's behavior for the whole process and are not validated beyond the
--prefix. A conflicting combination can stop the process at startup. Measure memory and throughput with your workload before relying on a flag in production.
Shutting down on a termination signal
Added in: v5.4.0When the Harper process receives SIGTERM, SIGINT (Ctrl+C) or SIGQUIT, it shuts down in order instead of exiting at once. These are the signals harper stop, docker stop, Kubernetes and systemd send.
- Harper stops accepting new connections.
- Requests already in flight get a few seconds to finish (see the first note below).
- Component scopes close. Their
'close'listeners run once requests in flight have had a few seconds to finish. - The process exits with code
0.
threads.shutdownTimeout bounds this sequence. If it has not finished in time, Harper logs Shutdown did not finish within <n> ms (threads.shutdownTimeout) and exits anyway. A second signal during the shutdown makes Harper exit at once; running harper stop again sends one. Harper exits with code 0 in all three cases, so a 0 exit status does not mean every 'close' listener finished.
threads:
shutdownTimeout: 25000 # Kubernetes' default 30-second grace period, no preStop hook
The environment variable is THREADS_SHUTDOWNTIMEOUT. Like other settings, a changed value takes effect when Harper restarts; harper stop reads the configuration file, so its wait uses the new value right away. 0 restores the behavior from before v5.4.0: Harper exits when the signal arrives, without draining requests or closing scopes.
- Requests get a few seconds, not the whole timeout. After it stops accepting connections, Harper waits a few seconds for requests in flight (about 2.5 seconds on Node.js, 5 on Bun), however high
threads.shutdownTimeoutis set. A request still running then may not finish: on Node.js its connection is closed, and its handler can still be running when'close'listeners start. The rest of the timeout is for component cleanup. - Fit it inside your orchestrator's grace period. A container platform or service manager sends SIGTERM, waits for its grace period, then kills the process. After the shutdown above, Harper closes its databases as the process exits, and
threads.shutdownTimeoutdoes not limit that step. Setthreads.shutdownTimeouta few seconds below the grace period to leave time for it. The default of 8 seconds fits Docker's default. On Kubernetes, the grace period also covers apreStophook, which runs before SIGTERM, so subtract its time too.
| Platform | Grace period setting | Default |
|---|---|---|
Docker (docker stop) | --timeout (-t) | 10 seconds |
| Docker Compose | stop_grace_period | 10 seconds |
| Kubernetes | terminationGracePeriodSeconds | 30 seconds |
| systemd | TimeoutStopSec | 90 seconds |
authentication
Authentication and session configuration. Added in: v4.1.0; enableSessions added in v4.2.0. See Authentication Configuration.
authentication:
authorizeLocal: true
cacheTTL: 30000
enableSessions: true
operationTokenTimeout: 1d
refreshTokenTimeout: 30d
authorizeLocal— Auto-authorize loopback requests as superuser; Default:true. From v5.4.0, these requests run as the built-inlocaluser.cacheTTL— Session cache duration (ms); Default:30000enableSessions— Cookie-based sessions; Default:trueoperationTokenTimeout— Access token lifetime; Default:1drefreshTokenTimeout— Refresh token lifetime; Default:30dlogging— Authentication event logging (Added in: v4.6.0); sub-options:path,level,tag,stdStreams. See Logging Configuration
operationsApi
Harper Operations API endpoint configuration. See Operations API Overview.
operationsApi:
network:
port: 9925
cors: true
tls:
certificate: ~/hdb/keys/certificate.pem
privateKey: ~/hdb/keys/privateKey.pem
componentFile.maxSize— Maximum file size in bytes returned byget_component_file. Requests for files larger than this limit are rejected with HTTP 413. Default:5242880(5 MB)network.cors/network.corsAccessList— CORS settings.network.corsAccessListtakes the same entries ashttp.corsAccessListnetwork.domainSocket— Unix socket path for CLI communication; Default:<rootPath>/hdb/operations-servernetwork.headersTimeout/network.keepAliveTimeout/network.timeout— Timeout settings (ms)network.port— Operations API port; Default:9925network.securePort— HTTPS port; Default:nulltls— TLS override for the Operations API; sub-options:certificate,certificateAuthority,privateKey. Seetls
tls
Global TLS configuration for HTTPS and TLS sockets (used by HTTP and MQTT). Can be a single object or an array for SNI. See TLS and Certificate Management.
tls:
certificate: ~/hdb/keys/certificate.pem
certificateAuthority: ~/hdb/keys/ca.pem
privateKey: ~/hdb/keys/privateKey.pem
certificate— Path to TLS certificate; Default:<rootPath>/keys/certificate.pemcertificateAuthority— Path to CA file; Default:<rootPath>/keys/ca.pemprivateKey— Path to private key; Default:<rootPath>/keys/privateKey.pemciphers— Allowed TLS cipher suitesunixDomainSocketsAdded in: v5.1.0 —truemakes each HTTP worker thread also listen on a Unix domain socket under<rootPath>/socketsfor each secure port, for a proxy on the same host to forward to. Isolated applications are reachable only this way, so they require it; Default:false
With tls as a single object, put unixDomainSockets: true inside it. With an SNI certificate array, put tls_unixDomainSockets: true at the root of harper-config.yaml, alongside tls; placing unixDomainSockets inside an array entry does not enable the global setting. On v5.3.0, a standalone TLS_UNIXDOMAINSOCKETS=true environment variable does not enable this SNI configuration; use the root YAML setting:
tls_unixDomainSockets: true
tls:
- host: app.example.com
certificate: ~/hdb/keys/app-certificate.pem
privateKey: ~/hdb/keys/app-private-key.pem
mqtt
MQTT protocol configuration. Added in: v4.2.0. See MQTT Configuration.
mqtt:
network:
port: 1883
securePort: 8883
webSocket: true
requireAuthentication: true
network.port— Insecure MQTT port; Default:1883network.securePort— Secure MQTT port; Default:8883network.mtls— Enable mTLS for MQTT connections; sub-options:user,required,certificateAuthority,certificateVerificationwebSocket— Enable MQTT over WebSocket on HTTP port; Default:truerequireAuthentication— Require credentials or mTLS; Default:truelogging— MQTT event logging (Added in: v4.6.0); sub-options:path,level,tag,stdStreams. See Logging Configuration
logging
Application logging. Added in: v4.1.0; per-component logging added in v4.6.0. See Logging Configuration.
logging:
level: warn
root: ~/hdb/log
stdStreams: false
auditLog: true
rotation:
interval: 1D
maxSize: 100M
level— Log verbosity (trace→debug→info→warn→error→fatal→notify); Default:warnfile— Write to file; Default:trueroot— Log directory; Default:<rootPath>/logpath— Explicit log file path (overridesroot)stdStreams— Write to stdout/stderr; Default:falseconsole— Includeconsole.*output in the log file; Default:falseauditLog— Enable table transaction audit logging; Default:trueauditRetention— Audit log retention duration; Default:3dexternal— Logging for components using the logger API; sub-options:level,pathrotation.enabled/rotation.compress/rotation.interval/rotation.maxSize/rotation.path— Log file rotation (activates whenintervalormaxSizeis set)auditAuthEvents.logFailed/auditAuthEvents.logSuccessful— Log failed/successful authentication events; Default:false
node
This node's identity within the cluster. See Replication.
node:
hostname: server-one
hostnameChanged in: v5.3.0 — This node's identity: it becomes the node's TLS certificate common name and the host that replication advertises to peers and dials to reach this node. Must be a bare hostname or IP literal (e.g.server-one,10.0.0.5, or the unbracketed IPv6 form::1) — not a URL and nothost:port. Harper fails to start if the value carries a scheme, port, path, credentials, query string, or fragment, is a bracketed IPv6 literal ([::1]), or is not a string; the startup error names the offending value and the reason. Earlier versions accepted such values and silently corrupted certificate matching and replication — a node configured ashttp://host:9926advertised and dialed a host literally namedhttp. The same requirement applies toreplication.hostname. If unset, Harper uses the first valid bare host amongreplication.hostname, the host inreplication.url, the TLS certificate common name, and the Operations API host, falling back to127.0.0.1.
replication
Native WebSocket-based replication (Plexus). Added in: v4.4.0. See Replication and Clustering.
replication:
hostname: server-one
url: wss://server-one:9933
databases: '*'
routes:
- wss://server-two:9933
hostnameChanged in: v5.3.0 — This instance's hostname within the cluster. Subject to the same bare hostname or IP literal requirement asnode.hostname— a URL,host:port, or non-string value fails startup. When both are set,node.hostnamewins.url— WebSocket URL peers use to connect to this instancedatabases— Databases to replicate; Default:"*"(all). Each entry supportsname,replicateTo, andshardedroutes— Peer nodes; URL strings or{hostname, port, startTime, revokedCertificates}objectsport— Replication portsecurePort— Secure replication port; Default:9933(changed from9925in v4.5.0)threads— Worker threads dedicated to replication, which then own the replication port and every subscription and load no application code; requiresportorsecurePort; see Dedicated Replication Threads; Default:0(replication runs on the HTTP worker threads) (Added in: v5.4.0)enableRootCAs— Verify against Node.js Mozilla CA store; Default:truepingInterval— Interval between replication keep-alive pings (ms); Default:30000pingTimeout— Time without received socket activity before a replication connection is considered dead (ms); Default: twicepingInterval.confirmationTimeout— Timeout in milliseconds for explicitreplicatedConfirmation, read when a waiter starts. Default:900000, orblobTimeoutwhen greater. An explicitly configured value overrides that fallback, including a smaller value. Range:1–2147483647. (Added in: v5.4.0)blobTimeout— Blob transfer timeout (ms); Default:120000blobGapReconnectMs— Interval (ms) for the blob-gap watchdog: when a transient blob save failure pins a replication resume cursor, the connection is forced to reconnect on this cadence so the gapped blob is re-streamed and, during a bulk copy, the copy resumes from the last banked cursor. Lower values heal gaps faster at the cost of more reconnects on a link whose faults never heal; Default: theblobTimeoutvaluecopyCursorFlushBytes— Bytes of applied bulk-copy data between durable flushes of the copy resume cursor (RocksDB); Default:67108864copyCursorFlushIntervalMs— Maximum time (ms) between durable flushes of the bulk-copy resume cursor (RocksDB); Default:5000blobSendDrainTimeout— Maximum time (ms) a worker waits for in-flight replication blob sends to finish before shutting down during a restart, so a rolling restart (e.g., a component deploy reload) doesn't interrupt a transfer in progress. Only sends that are still making progress are waited on;0disables draining; Default:600000failOver— Failover to alternate node if peer unreachable; Default:trueshard— Shard ID for traffic routing; see Shardingshards— Complete array of unique shard IDs, integers0–65535; order does not matter. Required for a declarative database with no installed map; custom-map conversion can omit it. All peers must agree, and the localshardmust be included when configured. Changing it never recomputes an installed map; see Complete shard-id set. (Added in: v5.4.0)mtls.certificateVerification— Certificate revocation checking (CRL/OCSP) for replication connections; see Certificate Verificationlogging— Replication event logging; sub-options:path,level,tag,stdStreams. See Logging Configuration
storage
Database storage configuration. See Storage Tuning for guidance on tuning these options for production workloads, Database Overview for general database concepts, and Compaction for reclaiming space inside existing files.
storage:
path: ~/hdb/database
caching: true
compression: true
compactOnStart: false
engine: rocksdb
-
engine— The database storage engine to use for new databases. Currently supported engines arerocksdbandlmdb. The default isrocksdb. Existing databases will use the engine that was used when they were created. -
fullTextAdded in: v5.4.0 — Full-text search work and native process limits. Sub-options:maxRawHitsPerRequest— Maximum fetched hits per request, including bounded prefix replay when traversal is needed; default10000.maxResultsPerRequest— Maximum retained offset plus limit. A request must also fit the separate raw-hit budget; default10000.requestBudgetMilliseconds— Execution budget across native batches, source checks and highlights, capped by the native execution limit; default5000ms. Reader acquisition and freshness waiting have separate bounds.runtime.maxResidentIndexes— Maximum resident native indexes; default128.runtime.maxIndexingThreads— Maximum aggregate indexing-thread reservations across resident native indexes; default128.runtime.maxSearchThreads— Maximum aggregate search-thread reservations across resident native indexes; default256.runtime.maxWriterMemoryBytes— Aggregate writer-memory reservations across resident native indexes; default4294967296(4 GiB).runtime.maxQueuedBytes— Aggregate queued-write reservations across resident native indexes; default8589934592(8 GiB).runtime.maxExpensiveSearches— Maximum concurrent expensive native searches; default8.
The runtime budgets apply together and are not hard bounds on total process memory. See Set Configuration for when request and native runtime changes take effect. Use the usual underscore-separated parameter names, such as
storage_fullText_maxRawHitsPerRequest. See full-text query execution for costs; see readiness and recovery for failures. -
writeAsyncEngine: LMDB — Disable disk sync on commit for the legacy LMDB storage engine (disables local disk durability guarantees on LMDB). Has no effect on RocksDB, which does notfsynccommits; see Storage Tuning — Durability vs. Throughput; Default:false -
caching— In-memory record caching; Default:true -
compression— LZ4 record compression; Default:true(enabled by default since v4.3.0). Sub-options:dictionary,threshold -
compactOnStart— Compact all non-system databases on startup; Default:false(Added in: v4.3.0) -
compactOnStartKeepBackup— Retain compaction backups; Default:false -
maxTransactionQueueTime— Per-thread write-commit duration threshold for HTTP 503 backpressure, and the limit on how long a write keeps retrying conflicts with other writes (a separate 503, codeTRANSACTION_COMMIT_CONFLICT_TIMEOUT); see Storage Tuning; Default:45s -
maxTransactionOpenTime— Milliseconds a transaction may stay open without activity before Harper aborts its uncommitted writes (which only writes renew) or releases its read-only snapshot (which reads renew; RocksDB only, LMDB uses a fixed 30 seconds); see Storage Tuning; Default:30000 -
noReadAhead— Advise OS against read-ahead; Default:false -
prefetchWrites— Prefetch before write transactions; Default:true -
randomAccessFieldsAdded in: v5.1.0 — Encode records as typed random-access structures, so a single field can be read without decoding the whole record. Best for tables whose records have a stable set of fields with stable types; leave off for wide, sparse, or variably typed schemas. Read by each table when its store opens, so a change applies to existing tables on restart, and ignored entirely by tables declaring the@table(randomAccessFields:)directive; Default:false. See Storage Tuning — Record Encoding -
path— Database files directory; Default:<rootPath>/database -
backupPath— Directory for managed database backups (created bycreate_backup), one subdirectory per database; Default:<rootPath>/backup(Added in: v5.2.0) -
blobPaths— Blob storage directory or directories; Default:<rootPath>/blobs(Added in: v4.5.0) -
pageSize— Database page size (bytes); Default: OS default -
reclamation.threshold— Free-space ratio below which reclamation begins evicting from caching tables; Default:0.4(Added in: v4.5.0) -
reclamation.interval— Free-space check interval; Default:1h -
reclamation.evictionFactor— Heuristic factor for early eviction under disk pressure; Default:100000. See Storage Tuning — Reclamation -
rocks.blockCacheSize— RocksDB shared block cache size in bytes; Default: 25% of constrained memory. See Storage Tuning — RocksDB Memory (Added in: v5.1.0) -
rocks.writeBufferManagerSize— Process-wide cap (bytes) on RocksDB memtable memory across all databases.0disables; Default: one third ofblockCacheSize(enabled). See Storage Tuning — RocksDB Memory (Added in: v5.1.0) -
rocks.writeBufferManagerCostToCache— When enabled, memtable memory and the block cache share a single, unified memory pool. During heavy write bursts, the block cache dynamically shrinks to give memtables more room. Once those memtables flush to disk, the block cache automatically reclaims that memory space. Default:true. Has no effect whenwriteBufferManagerSizeis0. (Added in: v5.1.0) -
rocks.writeBufferManagerAllowStall— Stall writes when memtable memory exceedswriteBufferManagerSize(hard cap) instead of allowing brief overshoot with more aggressive flushing (soft cap); Default:falseChanged in: v5.3.0 (truein v5.2.8 and earlier). See Storage Tuning (Added in: v5.1.0) -
rocks.writeBufferManagerFlushPolicyAdded in: v5.4.0 — Which memtable is flushed when memtable memory is overwriteBufferManagerSize:oldest,largest, orlargestAcrossDBs. Has no effect whenwriteBufferManagerSizeis0. A changed value applies when the worker threads restart, but removing the key restores the default only after a fullrestart; an invalid value is ignored, the same as removing the key. Default:largestAcrossDBswhenwriteBufferManagerAllowStallistrue, otherwiseoldest. See Storage Tuning -
rocks.arenaBlockSizeAdded in: v5.4.0 — Size (bytes) of the blocks each column family's memtable allocates from. Every column family that has been written holds at least one block, charged towriteBufferManagerSizeeven while idle. Invalid values are ignored. Read when a database opens, so a fullrestartis what reliably applies a change; Default: automatic,writeBufferManagerSize / 2048between 64 KiB and 1 MiB (RocksDB's 1 MiB whenwriteBufferManagerSizeis0). See Storage Tuning -
rocks.transactionLogAbortMarkersAdded in: v5.4.0 — Mark the transaction-log entry of a transaction that fails after logging as aborted, so replication, subscriptions,read_audit_logand crash replay skip it, and give up on a write that keeps conflicting instead of retrying it without limit.falsekeeps the 5.3 behavior. Read once per process: a change takes effect after arestart, notrestart_service. Harper 5.3.4 and earlier cannot read the markers. Default:true. See Storage Tuning — RocksDB Transaction Log -
rocks.transactionLogWriteIntentsAdded in: v5.4.0 — Make a commit that would conflict with an older, still-open transaction wait for that transaction to finish, usually before writing its transaction-log entry. Shortens the longest waits under heavy write contention, at the cost of more retries and lower throughput when transactions are large or contention is low. No effect whenrocks.transactionLogAbortMarkersisfalse. Read once per process: a change takes effect after arestart, notrestart_service. Default:false. See Storage Tuning — RocksDB Transaction Log
databases
Per-database and per-table file path overrides. Must be set before the database/table is created. See Database Overview.
databases:
myDatabase:
path: /data/myDatabase
auditPath: /data/myDatabase-audit
tables:
myTable:
path: /data/myTable
<dbName>.path— Database files directory<dbName>.auditPath— Audit log directory for this database<dbName>.tables.<tableName>.path— Table files directory
analytics
Analytics aggregation configuration. See Analytics Overview.
analytics:
aggregatePeriod: 60
replicate: false
aggregatePeriod— Aggregation interval (seconds); Default:60(Added in: v4.5.0)storageInterval— Aggregation cycles between storage-volume measurements (0disables); Default:10replicate— Replicate analytics data across cluster; Default:falselogging— Per-subsystem logger override for analytics writes. See Logging ConfigurationfullTextAdded in: v5.4.0 — Optional full-text telemetry collection and export; defaults to omitted/off. See Full-text analytics.profilingChanged in: v5.4.0 — Harper Pro only. Run the CPU profiler that recordscpu-usage; it samples only in short windows and flags theutilizationsamples it affects. Seeanalytics.profiling; Default:true
analytics.fullText
Added in: v5.4.0
Engine: RocksDB
Type: object
Default: omitted; collection is disabled
Optionally collect telemetry from already-open full-text indexes. Defaults are implied; add only the settings you want to change. Operator collection and persistence require Harper analytics to be enabled on a writable node. analytics.aggregatePeriod: -1 disables analytics; read-only mode (storage.readOnly, HARPER_READONLY or the --readonly flag) also disables it. To enable full-text collection when those prerequisites hold:
analytics:
fullText:
enabled: true
| Option | Type | Implied value | Description |
|---|---|---|---|
enabled | boolean | false | Enable scheduled collection and persistent full-text analytics when Harper analytics is enabled and the node is writable. Changes apply without reopening indexes. |
intervalMilliseconds | number | 60000 | Per-handle interval, an integer from 1000 through 2147483647. Calls are staggered across handles. |
histograms | boolean | false | Record optional latency histograms. |
snapshotBytes | boolean | false | Request cached reader-byte observations; enabling does not trigger index publication or reader reload. |
Set enabled: false or remove the block to stop operator collection and export. Invalid edits retain the last valid policy. Deleting the configuration file removes the file-based analytics.fullText setting and reapplies HARPER_DEFAULT_CONFIG, HARPER_CONFIG and HARPER_SET_CONFIG overlays to an empty file; collection remains enabled if one of those overlays still requests it. An in-process override is retired if deletion changes its matching configuration value after those overlays are applied; recreating the file does not restore retired overrides. Internal implementation hooks can independently keep collection active without enabling persistence; they are not a public SDK or operator prerequisite. With operator export enabled, persisted fields reflect the union of operator and internal-consumer recording requests. A handle already closing retains its last options until close settles.
The interval controls periodic live sampling. Successful handle closes also collect final counters, so it does not cap total snapshot or export frequency. Qualify reader churn as well as index count and sampling cadence.
See Full-text analytics for fields, cumulative-counter interpretation and collection costs.
localStudio
Local Harper Studio GUI. See Studio.
localStudio:
enabled: true
enabled— Enable local Studio athttp://localhost:<port>; Default:false
componentsRoot
Path to local component files. Added in: v4.2.0 (previously customFunctionsRoot). See Components.
componentsRoot: ~/hdb/components
rootPath
Root directory for all Harper persistent data, config, logs, and components.
rootPath: /var/lib/harper
agent
Added in: v5.2.0
Built-in Harper agent — an LLM loop that operates this instance through Harper's own operations, scoped filesystem access, followup scheduling, the V8 inspector, and outbound HTTP. Disabled by default, so it never incurs LLM cost unless you turn it on. Driven through the Agent operations, which also describe the privilege boundary before you enable it.
agent:
enabled: true
model: default
maxTurns: 50
maxTokens: 16384
maxToolResultBytes: 65536
autoApprove: false
allowDestructive: false
user: hdb_agent
enabled— Enable the agent component; Default:falseprovider— Recorded on the session but not yet used to route the model call — onlymodelreaches the provider. Set the provider through themodelsconfiguration insteadmodel— Model id override, passed through to the model call; Default: themodelsgenerative defaultmaxTurns— Maximum tool-call iterations in a single run; a positive integer, validated from v5.4.0. A baremaxTurns:with no value fails the agent's start like any other invalid value; Default:50maxTokensAdded in: v5.4.0 — Output-token cap sent with every model request the agent makes. A reply cut off at this cap ends the run with statuserror, notcompleted, andlastErrorsays it was cut off; raise the cap if replies keep hitting it. A cut-off reply can also mean the session's transcript filled the model's context window, which a higher cap does not fix; start a new session instead. Keep it within the model's own output limit: most providers reject a larger value on every request, and the run then endserrorwith the provider's message, so lower it for a model with a small output limit (some older models allow 4096 or 8192). Before v5.4.0 the agent sent no cap: Anthropic models, called directly or through Bedrock, then used 4096, and the other backends sent none; Default:16384maxToolResultBytesAdded in: v5.4.0 — Largest tool result, in bytes of JSON, the agent adds to its conversation. A larger result is cut to this size, ending with a note that gives the original size and tells the model how to ask for less. The file-reading tools return pages of at most half this size, measured as JSON, so a page arrives whole unless the file's path alone takes up most of the cap. Lower it for a model with a small context window. It must be an integer from1024to1048576; at startup an invalid value is logged and the default is used. See When a conversation outgrows the model's context window; Default:65536maxSessionTokensAdded in: v5.4.0 — Per-session cap on prompt plus completion tokens, summed over every model call in the session that returns, a reply the agent rejects as cut off or empty included; a positive integer. See Spend caps; Default: nonemaxCostUsdChanged in: v5.4.0 — Per-session cap in USD, priced from thepricingon themodels.generativeentry that served each call; a number of0or more, and0stops every session before its first call. See Spend caps. Before v5.4.0 this setting was stored but never enforced, and defaulted to5.00; Default: noneautoApprove— Run without per-action approval gates; Default:falseallowDestructive— Include the tools marked destructive in the agent's toolset:write_file, the inspector's code-evaluation tools, and any operations tool carrying MCP'sdestructiveHint(drop_table,delete,restart,set_configuration, ...). Whenfalsethey are removed entirely rather than gated. That hint comes from a curated set in core which does not cover every damaging operation, so this is not a complete safety boundary on its own — see Agent operations; Default:falseuser— Harper user the agent's operations tools run as; the filesystem, HTTP, schedule, and inspector tools always run at process privilege regardless. If it cannot be resolved and it is not the default, the agent fails closed and runs with no operations tools; Default:hdb_agent, which falls back to asuper_userbootstrap identitycomponentsScope— The agent'scomponentsfilesystem scope, which it reads and, withallowDestructive, writes; absolute or relative torootPath. Read at startup only; Default: the fullcomponentsRootconfigScopeAdded in: v5.4.0 — The agent's read-onlyconfigfilesystem scope: a file or a directory, absolute or relative torootPath. Read at startup only. See Filesystem scopes; Default: the Harper config file onlyhttpFetchAdded in: v5.4.0 — Whether the agent has itshttp_fetchtool, and which hosts it may reach:true,false, or{ allow: [...] }. Read at startup only. See Restrictinghttp_fetch; Default:truesystemPromptAppend— Operator text appended to the agent's system prompt
enabled, provider, model, maxTurns, maxTokens, maxToolResultBytes, maxSessionTokens, maxCostUsd, autoApprove, allowDestructive, and systemPromptAppend can also be changed at runtime with set_agent_config, which applies in memory only. enabled is the exception worth knowing: it cannot switch the agent on, because with the agent disabled at startup no agent operation is registered at all. httpFetch, componentsScope, and configScope are read at startup only, and set_agent_config rejects them.
Filesystem scopes
Changed in: v5.4.0The agent's read tools (read_file, list_dir, grep_files, and tail_file) take a root naming one of three scopes. write_file, available when allowDestructive is on, takes no root and always writes to components.
| Scope | Reaches | Access |
|---|---|---|
components | componentsRoot, or componentsScope when set | read and write |
logs | the log directory | read |
config | the Harper config file, or configScope when set | read |
Before v5.4.0, config was the directory holding harper-config.yaml. On a default install that is rootPath itself, so the agent could read keys/, database/, and the rest of the root. It is now the config file alone, and list_dir shows only that file.
To point the agent somewhere else, such as a directory of extra configuration, set configScope to a file or a directory, absolute or relative to rootPath. It replaces the config file as the scope: the config file stays readable only if the new target contains it, as rootPath does. It is read at startup only. A path that names no file or directory at startup leaves the config scope unavailable and logs an error.
agent:
enabled: true
configScope: /etc/harper/extra
Whatever componentsScope and configScope say, every scope refuses:
- Harper's key directories,
<rootPath>/keys(TLS and JWT keys) and<rootPath>/ssh(git deploy keys): nothing in them is read, listed, or written. Paths are compared after resolving symlinks. - Key file names,
*.pem,*.key, and.jwtPass: not read.list_dirstill shows the names, andwrite_filecan still create such a file outside the key directories. - Text holding a PEM private key (
-----BEGIN ... PRIVATE KEY-----), such as an inlinetls.privateKeyin the config file:read_filerefuses a page that holds one or starts partway through one (it looks back 128 KiB for the BEGIN line, more than any PEM key spans, post-quantum ones included),tail_filerefuses lines that hold one or a file that ends partway through one, andgrep_filesskips the file.
A key with neither a key file name nor PEM armor, such as a .p12 bundle or raw DER, is not recognized outside the key directories.
Setting configScope to rootPath also exposes the rest of rootPath, though a file over 256 KiB or not UTF-8, such as a database file or a backup, is refused (see Masked credential values).
Masked credential values
Changed in: v5.4.0The config file can hold plaintext credentials: a model apiKey passed through HARPER_SET_CONFIG is written there. Logs hold whatever a component logged. When the agent reads the logs or config scope, the value of every credential-named key comes back as *s:
models:
generative:
default:
apiKey: **************
model: gpt-5
- Which keys. A key that ends in
key,token,cookie, orauthorization, or containssecret,password,passwd,passphrase, orcredential, andaccessKeyIdin any spelling. The whole key must match, so settings such asmaxTokensandoperationTokenTimeoutare shown. A value that is exactly an environment reference, such as${OPENAI_API_KEY}, is shown as written. - Which forms.
key: valueandkey=value, with the key quoted or not, anywhere in a line: YAML, JSON (including JSON logged inside a JSON string),Authorization: Bearer ...headers, and?api_key=...query strings. A quoted value is masked up to its closing quote, even on a later line, and an unquoted one to the end of the line. A YAML value that continues on more-indented lines, such as a long folded value or a|block, is masked on every line, as are the-items under a key with no value of its own. In theconfigscope each file is also parsed as YAML, so a key written with escapes, such as"api\u004Bey", and a value reached through an alias are masked too. A config file that does not parse shows only its keys, with every value masked. A config file over 256 KiB or not UTF-8 is refused. - Positional reads. The mask keeps each value in place (in a config file, one
*per byte), so aread_fileoffsetinto a value, the later parts of a long line, and the end of a line thattail_filereturns are masked too.sizeandnextOffsetstill count the file's bytes, so a masked value's length is visible; its content is not. - Searches.
grep_filessearches the masked text, so a pattern that matches only a credential's value finds nothing.
The agent's read_log tool is masked the same way, in every text field of each entry. An entry holding a PEM private key is replaced by a refusal. The tool refuses read_log's filter argument, because filter matches the unmasked log. read_log called through the operations API or an MCP client returns the log unmasked.
The components scope is not masked, so a file the agent reads and writes back is unchanged. A credential that is not the value of a credential-named key, such as a password inside a URL, is not masked.
Agent spend caps
Added in: v5.4.0maxSessionTokens and maxCostUsd bound a whole session, across every run in it. Approving a paused action or sending a follow-up prompt starts a new run, but does not reset a cap the way it resets maxTurns. Each assistant message records the usage of the model call that produced it, and get_agent_session reports the session's totals as usage.
A cap is checked when a run starts, before it executes an approved action, and after every model call:
- At or over a cap, the session makes no further model call and ends
error, withlastErrornaming the cap, for exampleagent.maxSessionTokens=200000 reached: this session used 201450 tokens. Tool calls the model had just requested are answered with abudget_exceededresult rather than run, so the transcript stays valid if you continue the session. A final answer that crosses the cap is still delivered: the run endscompleted, withlastErrornaming the cap. - A cap that cannot be measured stops the session too.
maxCostUsdneeds every call priced, andmaxSessionTokensneeds every call's token counts. A model with nopricing, or a provider that reports no usage, stops the session after its first call rather than letting it run uncapped: it endserror, orcompletedif that call was the final answer, withlastErrorsaying why either way. The unmeasured call stays on the session, so the session's total stays unmeasured and later runs stop before making any call, and raising the cap or addingpricingdoes not clear it: start a new session, or clear the cap.set_agent_configwithnullclears it until the next restart; to clear it for good, remove the key from the configuration file. A session created before v5.4.0, whose earlier calls recorded no usage, is in the same state. - Pending approvals are closed too. A run stopped before it executes an approved action answers that action, and any approval still awaiting a decision, with
budget_exceeded, so nothing is left pending. The action does not run later, even once the cap is raised; the model sees the refusal and can ask again. - A capped session stays capped. Raise the cap with
set_agent_configor start a new session.
A cap is a threshold checked after each call, not a guarantee: the call that reaches it can overshoot it. Every call that returns is counted, including a reply the agent rejects as cut off or empty: that reply stays out of the transcript, but its usage is kept on the session. A call that fails, for example on a timeout or a provider error and whether or not another model then serves it, is not counted, even if the provider billed it. The caps bound one session only: there is no cross-session or per-user budget, so a caller allowed to run agent_prompt can keep spending by starting new sessions.
A cap, or a maxTurns, that is not a usable number fails the agent's start: Harper logs the error and registers no agent operation. Through set_agent_config the same value is rejected with a 400. A bare maxSessionTokens: or maxCostUsd: with no value leaves that cap unset, as null does through set_agent_config; a bare maxTurns: fails, because maxTurns cannot be unset.
If you upgrade with maxCostUsd already set in your configuration, it starts being enforced, and needs pricing on the generative entry the agent uses.
Restricting http_fetch
Added in: v5.4.0
http_fetch lets the agent send HTTP requests from the Harper process: to its own REST endpoints while it builds an app, or to pull a reference page. The same agent reads table rows, logs, and component source, and text in any of them can steer it, so an enabled agent has a read path and an egress path in one toolset. agent.httpFetch bounds that tool. It is not a bound on every way the agent can reach the network: with allowDestructive on, for example, the agent can write component code, and the Harper process runs that code.
Remove the tool when the agent has no reason to reach the network, for example an investigation or operations-assistant role:
agent:
enabled: true
httpFetch: false
Or keep it and allow only named hosts:
agent:
enabled: true
httpFetch:
allow:
- localhost:9926
- '*.example.com'
| Value | Effect |
|---|---|
true (default) | The tool is available. Cloud-metadata hosts and the IPv4 link-local range are refused. |
false | The tool is removed from the agent's toolset. The model never sees it, and the system prompt stops suggesting HTTP checks. |
{ allow: [...] } | The tool is available, and a request is refused unless its host matches an entry. Its description lists the entries, so the model knows where it can go. |
Allow-list entries take these forms:
api.example.com— that host, on any portlocalhost:9926— that host on that port only. A URL without a port uses 80 forhttpand 443 forhttps.*.example.com— any subdomain, at any depth. It does not matchexample.comitself; list that separately.[::1]or[::1]:9926— an IPv6 address, in brackets
Entries and request URLs are compared in canonical form, so letter case, internationalized names, and shorthand spellings of an IPv4 address all match the same entry. An IPv4 entry does not admit the IPv4-mapped IPv6 form of its address ([::ffff:...]); list that separately if you need it.
Some behavior worth knowing:
- Every redirect hop is checked. A redirect to a host the policy refuses fails the request before that host is contacted. This applies in every mode, so with the default
true, a redirect into the cloud-metadata range fails too. - The metadata block comes first. Listing a cloud-metadata or link-local address does not make it reachable.
- Matching is by host name, not address. An allowed name whose DNS resolves to an internal address is still reached. List names whose DNS you control.
- An empty list removes the tool, the same as
false. A value Harper cannot read also removes the tool, and logs an error naming it, while the rest of the agent stays available. - It is read at startup only. Set it in
harper-config.yaml, withset_configuration, throughHARPER_CONFIG, or as an environment variable or CLI argument (AGENT_HTTPFETCH=false, orAGENT_HTTPFETCH_ALLOW='["localhost:9926"]'as a JSON array), then restart. A comma-separated string is not a list: it is rejected, and the tool is removed.set_agent_configrejects it at runtime. Becauseset_configurationcan write it andrestartapplies it, the policy only holds against the agent itself while those two operations stay out of its toolset (they are not in the default operations allow list) or behind approval.
deployment
Retention for what deploy_component leaves behind, and how long startup waits for component installs. See deploy_component.
deployment:
startupInstallTimeout: 600000
payloadRetention:
maxSize: 10485760
failedMaxAge: 7d
failedMaxCount: 2
stagingRetention:
maxCount: 5
previews:
sweepInterval: 600000
stagingRetention:
maxCount: 1
startupInstallTimeoutAdded in: v5.3.0 — Milliseconds. The longest startup waits for component installs before it opens listeners. While waiting, it logs every 60 seconds which ones remain outstanding — including throughout0's unbounded wait — unless every install settles first, which always ends the wait before that first log, whatever the timeout; a failed install logs immediately regardless of the timeout. A component whose install is still running at the deadline keeps installing in the background, and the node serves the version already installed meanwhile, or nothing for a component that was never installed. When that install succeeds, the swap lands on disk immediately and Harper flags a restart as needed (restartRequiredinget_statuscalled with noid) — any one component's late success sets it, even while others are still installing. Code already loaded keeps running until that restart, but some component plugins apply an added or changed file on their own rather than waiting for one; content served straight from disk, such as static files, can also reflect the new version before the restart. Outsideharper dev, which restarts automatically soon after any restart request, Harper does not restart on its own. An install that fails after the deadline does not set the flag; Harper only logs an error naming the component.0waits indefinitely, which was the behavior before v5.3.0. An empty value uses the default. A negative, non-finite, or otherwise unparseable value also falls back to it, with a warning in the log; Default:600000(10 minutes)payloadRetention.maxSizeAdded in: v5.1.15 — Bytes. After a successful deploy, a payload larger than this has its stored tarball (payload_blob) dropped from thehdb_deploymentrow; the row and its metadata stay. Set it very high to retain every payload; Default:10485760(10 MiB)payloadRetention.failedMaxAgeAdded in: v5.4.0 — A failed deploy keeps its payload, since that is what you retry or debug with. A failed payload older than this is dropped, asdelete_deployment_payloaddrops one, keeping the record; each project's newest failed payload is always kept. A number of seconds, or a number with a unit:m,h,d,M(30 days) ory. Any other value uses the default, and0turns the bound off; Default:7dpayloadRetention.failedMaxCountAdded in: v5.4.0 — How many failed payloads a project keeps, newest first, counting every node's deploys; older ones are dropped the same way.0turns the bound off. Both bounds apply after a deploy fails, for its project, and once at each startup. Each node drops only the payloads of deploys it received, so a deploy received by a node that has since left the cluster keeps its payload; Default:2stagingRetention.maxCountAdded in: v5.3.0 — How many complete builds that are not live may remain per component under<componentsRoot>/.deploy-staging: builds staged withactivate: false, and releases replaced by a later deploy, whichdeployment_idcan make live again. The newest survive; older ones are removed when that component next deploys and at startup.drop_componentremoves all of them. Each is a full installed copy of the component,node_modulesincluded.0keeps none, and no replaced release is kept. This bounds dormant builds only, not a disk quota: staging directories that belong to an in-flight or unsettled deploy are never touched; Default:5previews.maxPerApplicationAdded in: v5.4.0 — How many live preview deployments of one application a node keeps. A deploy of another is refused with409, naming the oldest; a redeploy of a live preview does not count. Only the node that receives a deploy checks. A role'scomponent_deploygrant can set its ownmaxPreviews, which takes the place of this for that role's deploys. A value that is not a positive whole number is ignored; Default: none, no limitpreviews.sweepIntervalAdded in: v5.4.0 — Milliseconds. How often the cluster looks for expired previews and drops them. Only a node that holds a preview takes part, and only the scheduler's leader looks. A value that is not a whole number of milliseconds, such as10m, uses the default; Default:600000(10 minutes)previews.stagingRetention.maxCountAdded in: v5.4.0 —stagingRetention.maxCountfor the component of a preview deployment, in its place.0keeps none; Default:1
applications
Added in: v5.0.0
Controls how application modules are loaded and isolated. See Module Loading for the full reference.
applications:
lockdown: seal-after-load
moduleLoader: vm-current-context
dependencyLoader: auto
allowedDirectory: app
allowedSpawnCommands:
- npm
- node
# allowedBuiltInModules: [] # if omitted, all Node.js built-ins are allowed
lockdownChanged in: v5.4.0 — Whether and when Harper locks the JavaScript intrinsics (see What is locked), which makes prototype pollution attacks harder. The default for new installs isseal-after-load(since v5.4.0), which seals them once all components have loaded, so component initialization can still extend them. Sealing blocks adding or deleting a property, but still allows assigning to an existing writable one. This can also be set toseal(seal before any application code loads; added in v5.4.0),freeze-after-load(the default from v5.0.2 through v5.3; freezes once all components have loaded, so existing properties cannot be changed either),freeze(freeze before any application code loads),ses(lockdown provided by thesespackage, which is more strict), ornone. An upgrade keeps the value already inharper-config.yaml, and with no value there Harper does not lock the intrinsics. Before downgrading an instance that uses a seal mode to v5.0.1 through v5.3, setfreeze-after-load: those releases treat the seal modes asfreeze. See Intrinsic Lockdown.moduleLoader— The method used to load modules (and isolate the application). The default isvm-current-context(since v5.1.0; v5.0.x defaulted tovm), which uses Node's VM module loader in Harper's own context so applications share JavaScript intrinsics. This can also be set tovm(VM loader with a separate context and its own intrinsics per application),native(standard Node module loader), orcompartment, which uses thesesimplementation of the proposedCompartmentfunctionality. See Module Loader Modes.dependencyLoader— The application module loader can be used to load packages/dependencies (installed asdependenciesfrom the package.json). The default is 'auto', which only use the VM module loader if the package specifiesharperas a dependency. This can also be set toappto always use the application module loader ornativeto always native module loader for packages. See Dependency Loading.allowedDirectory— Added in v5.0.4. Restricts where an application may load modules from. The default isapp, which permits only modules within the application's own directory tree; loading from outside it throws. Set toanyto remove the restriction. Dev-mode installs default toany. Applies to imports the application module loader handles, so treat it as a configuration guardrail rather than a security boundary. See Allowed Directory.allowedBuiltInModules— An allowlist of the Node.js built-in modules applications may import. If omitted, all built-ins are allowed. Matching strips anode:prefix and compares the first path segment, sofsalso permitsnode:fs/promises. LikeallowedDirectory, it applies to imports the application module loader handles. See Allowed Built-in Modules.allowedSpawnCommands- This lists the specific commands that can be spawned by the application (usingchild_process'sspawn()andexecFile()functions). You can add commands that your application will need to launch (this is to protect against malicious code spawning processes). Only the first token of the command is matched, spawning also requires a mandatorynameoption, and the call is subject to a node-wide single-process lock — see Child Processes for the full contract.
Component Configuration
Installed components are configured directly at the root of harper-config.yaml using the component name as the key — not nested under a components: section. See Components.
my-component:
package: 'HarperDB-Add-Ons/my-component'
port: 4321
<component-name>.package— NPM package name, GitHub repo (user/repo), or local path<component-name>.port— Port for the component; Default: value ofhttp.port