Skip to content

Configuration

iiiris is configured in three layers, each overriding the previous:

  1. Built-in defaults (config.Default()) — what you get when no flag, file, or env var is supplied.
  2. YAML file (optional, via -config path.yaml) — overlays defaults.
  3. Environment variables — overlay everything else; IIIRIS_* prefix.

Zero-config operation is a project commitment: ./bin/iiirisd works with nothing set, serving ./testdata/ on :8080.

A running server can edit a curated subset of these fields from the admin UI (/admin/config) when started with -config: the edit is written back to the YAML file, and log.level / image.max_pixel_area / image.max_concurrent apply without a restart. See admin.md for the editable subset and the live-vs-restart split.

Command-line flags

The flag surface describes the binary, not the configuration: there is no -addr or -fs-root, because those belong to the three layers above. The full contract — including exit codes — is specs/cli.md.

Flag Effect
-config <path> Overlay the YAML file at path (layer 2 above).
-version Print build identity and exit.
-check-config Load + validate the configuration and exit.
-print-config Print the effective configuration as YAML and exit.

The last three are one-shot: they report and exit without binding a listener. Everything else is configuration.

$ iiirisd -version
iiirisd v0.9.1 (linux/amd64, go1.25.0, libvips 8.15.1)

$ iiirisd -check-config -config /etc/iiiris/iiiris.yaml
config OK: /etc/iiiris/iiiris.yaml

$ iiirisd -check-config -config /etc/iiiris/broken.yaml
config: image.supersample: 3 unsupported (want 1, 2, or 4)
$ echo $?
2

-check-config as a deploy preflight. It runs exactly the load and validation startup runs — same defaults, same file, same IIIRIS_* overrides, same Validate — so anything it rejects would have stopped a restart with the same message and the same exit code.

It is a config check, not a full dress rehearsal. It does not build subsystems, so the boot-time failures listed under Validation below — unknown cache backend, a filesystem backend with no path, auth.rules naming a profile that doesn't exist, an unreachable Redis — are not caught by the preflight; they surface at startup, also with exit 2. Nor does it check that the filesystem root exists or that an origin answers; those are runtime conditions, reported by /ready once the server is up. See deployment.md for the systemd ExecStartPre form.

-print-config for "what is this box actually running?" It prints the merged result of all three layers, which is otherwise a matter of reading three sources and applying the precedence rules by hand. Secrets are masked — sentry.dsn, metrics.password, content_state.store.write_token, hook.webhook.auth_header, every redis.password, and the inline auth.profiles.*.users hashes each print as ***. An empty secret stays empty, so the dump never implies a credential is set when it isn't.

Because secrets are masked, the output is a diagnostic, not a config file: feeding it back to -config would install *** as a literal password. Read it, diff it between hosts, paste it into a bug report.

YAML shape

server:
  addr: ":8080"
  read_timeout: 30s
  write_timeout: 60s
  shutdown_timeout: 10s

log:
  level: info     # debug | info | warn | error
  format: text    # text | json
  file: ""        # path to rolling log file; empty = stderr only.
                  # Size-based rotation: 100 MB per file, 5 backups.
                  # Windows service mode writes here AND to Event Log.

sources:
  default: filesystem    # which backend serves identifiers by default
  filesystem:
    root: ./testdata
  http:
    base_url: ""         # if non-empty, registers an HTTP source
    timeout: 30s
  s3:
    bucket: ""           # if non-empty, registers an S3 source
    region: ""
    prefix: ""
    endpoint: ""         # optional, for MinIO and other S3-compatibles

caches:
  render:
    backend: heap        # none | heap | filesystem | s3 | redis (none = slot disabled)
    max_bytes: 268435456 # 256 MiB; 0 = unbounded for heap/filesystem; required 0 for s3 / redis
    ttl: 0               # 0 = no TTL; honored by redis backend (per-entry SET EX)
    path: ""             # filesystem only
    s3:                  # s3 only
      bucket: ""
      region: ""
      prefix: ""
      endpoint: ""
    redis:               # redis only
      addr: ""             # e.g. redis.internal:6379
      password: ""         # inline (prefer password_env)
      password_env: ""     # env var name holding the password
      db: 0
      tls: false
      prefix: ""           # namespace for shared-Redis deployments
    coordinator:         # filesystem only; promotes to distributed mode
      redis:               # required: Redis as the shared-index coordinator
        addr: ""             # set to enable distributed mode
        password_env: ""
        db: 0
        tls: false
        prefix: ""
  info:
    backend: heap
    max_bytes: 16777216
  origin:
    backend: heap
    max_bytes: 268435456
  manifest:            # IIIF Presentation documents (manifests + collections)
    backend: heap
    max_bytes: 16777216

image:
  max_pixel_area: 100000000     # source area guard (rejects oversized originals up front)
  max_output_area: 100000000    # output area guard (rejects huge upscales; 0 = unbounded)
  max_output_width: 0           # output width cap; 0 = unbounded
  max_output_height: 0          # output height cap; 0 = unbounded
  max_concurrent: 0             # in-flight render cap; explicit 0 = unbounded; omit the field → defaults to host CPU count
  max_concurrent_per_ip: 0      # in-flight IIIF requests per client IP; 0 = disabled (opt-in)
  tile_size: 512                # info.json tiles edge length; 0 = no tiles array
  jpeg_progressive: false       # progressive JPEG output; false = baseline (~3x faster encode)
  metadata: strip               # strip = remove EXIF/XMP/IPTC + normalize colour/orientation; preserve = byte-faithful passthrough
  color: ""                     # srgb | respect-icc; "" (unset) = derive from metadata (strip->srgb, preserve->respect-icc)
  vips_concurrency: 0           # libvips worker threads per op; 0 = default (1)
  vips_max_cache_mem: 0         # libvips operation-cache cap in bytes; 0 = default (50 MiB)
  sharpen: 0.0                  # unsharp mask on downscaled renders; 0 = off (default), 0.0–1.0 (0.5 = libvips' own default strength)
  sharpen_sigma: 0.0            # unsharp radius; 0 = libvips default (1.5). Advanced
  sharpen_x1: 0.0               # unsharp flat/jagged threshold; 0 = libvips default (2.0). Advanced
  supersample: 1                # minimum linear decode headroom above the output before the residual resize; 1 = off (default) | 2 | 4

presentation:                   # IIIF Presentation API (manifests + collections)
  enabled: true                 # default-on (stateless, part of the zero-config path); false = image-only
  default_viewer: mirador       # viewer at /view/{id}: mirador (self-hosted) | uv (Universal Viewer, from CDN); ?viewer= overrides per-request
  validate: false               # warn-only structural conformance checks on served/derived docs (logs problems; never rejects)

content_state:                  # IIIF Content State API (shareable "open here" links)
  enabled: true                 # default-on (stateless); mounts /content-state/ mint+resolve and makes /view read iiif-content. Needs presentation.enabled
  store:                        # bookmark store (Form-B hosting) — OFF by default
    enabled: false              # true mounts POST /content-state + GET /content-state/s/{id}
    dir: ""                     # durable filesystem dir (required when store.enabled); never evicted
    write_token: ""             # POST requires `Authorization: Bearer <token>`; empty = writes refused

mcp:                            # Model Context Protocol server (read-only IIIF tools for AI agents)
  enabled: false                # OFF by default; true mounts the /mcp Streamable HTTP endpoint. See docs/mcp.md

metrics:                        # Prometheus exposition (opt-in)
  enabled: false                # master switch; everything below ignored when false
  path: /metrics                # route on the main listener
  addr: ""                      # non-empty (e.g. ":9090") = dedicated listener instead of the main one
  username: ""                  # both set = HTTP Basic required on the endpoint
  password: ""                  #   (Prometheus scrape configs support this via basic_auth)

sentry:                         # error tracking via the Sentry Go SDK (opt-in)
  enabled: false                # master switch; an empty dsn also disables the subsystem
  dsn: ""                       # Sentry project ingestion endpoint; empty = disabled
  environment: production       # tags every event (e.g. production | staging)
  release: ""                   # build attribution; empty derives it from the binary version
  sample_rate: 1.0              # fraction of error events sent (0.0–1.0)
  send_default_pii: false       # false = strip client IP / cookies / auth headers / query strings
  capture:                      # which error sources are reported (all default true)
    panics: true                # recovered panics (with stack traces) from the recover middleware
    server_errors: true         # requests ending in a 5xx status
    internal_errors: true       # source / decode / cache errors captured at the error site

tracing:                        # distributed tracing via OpenTelemetry (opt-in)
  enabled: false                # master switch; everything below ignored when false
  otlp_endpoint: ""             # OTLP/gRPC collector, e.g. "localhost:4317"; empty = sample but don't export
  insecure: false               # true = plain HTTP/2 (no TLS) to the collector
  sample_rate: 0.0              # head-sampling probability for root spans (0.0–1.0); 0 = trace nothing
  propagate_upstream: false     # true = inject W3C traceparent on outbound HTTP source fetches (not S3)

hook:
  type: noop                  # noop | lookup | js | webhook
  lookup:                     # used when type: lookup
    backend: filesystem
    template: "{{.Identifier}}"
  js:                         # used when type: js (recommended scripting engine)
    path: ""                  # path to .js file
    script: ""                # OR inline script (one or the other)
    watch_interval: 5s        # hot-reload poll cadence (path mode); 0=default, <0=disabled
    timeout: 1s               # per-call execution budget; 0=default (1s)
  webhook:                    # used when type: webhook
    url: ""
    timeout: 2s
    auth_header: ""

auth:                         # absent or empty profiles → AllowAll, no auth services
  profiles:
    reading-room:                       # profile name (used by rules: and the login route)
      pattern: clickthrough             # clickthrough | active | kiosk | external
      label:                            # BCP-47 language map (per IIIF JSON-LD)
        en: ["Reading Room"]
      heading:                          # optional; shown on the login page and probe denials
        en: ["Restricted material"]
      description:                      # optional; longer note on probe denials
        en: ["You must agree to view this material."]
      confirm_label: "I agree"          # CTA on the built-in clickthrough button
      logo_url: ""                      # optional <img src> in the built-in login UI
      access_service:
        backend: builtin                # builtin | header | external | oidc
        template: ""                    # path to override the built-in login HTML (builtin only)
        header: X-Forwarded-User        # request header to trust (header backend)
        url: ""                         # operator's auth page URL (external backend)
        callback_secret_env: ""         # env var holding HMAC-SHA256 secret (external)
        issuer: ""                      # OIDC issuer URL (oidc backend)
        client_id: ""                   # OIDC client_id
        client_secret_env: ""           # env var holding OIDC client_secret
        scopes: []                      # OIDC scopes; defaults to [openid, profile, email]
        user_claim: sub                 # OIDC claim used as session user; defaults to "sub"
      token_ttl: 5m                     # per-profile override for the global default
      session_ttl: 24h                  # per-profile override for the global default
      substitute:
        max_size: ""                    # IIIF size string for degraded view on deny ("!400,400")
      users:                            # inline bcrypt/apr1 map (active pattern + builtin backend)
        alice: "$2y$10$..."
      htpasswd_file: ""                 # path to htpasswd; mtime-watched; beats users when both set
      kiosk:
        allowed_ips: []                 # IPs and/or CIDRs (v4 or v6); kiosk pattern only

  rules:                                # first match wins; identifiers matching no rule are public
    - match: "special/*"                # exact | "prefix/*" | "*"
      profile: reading-room

  cookie:
    name: ""                            # session cookie name; defaults to "iiiris_session"
    path: ""                            # auto-derived from X-Forwarded-Prefix when empty

  cors:
    probe_origins: []                   # empty = wildcard ("*")
    token_origins: []                   # REQUIRED list for the postMessage token flow

  session:
    backend: heap                       # heap | filesystem | s3 | redis
    path: ""                            # required when backend=filesystem
    s3:                                 # required when backend=s3
      bucket: ""
      region: ""
      prefix: ""
      endpoint: ""                      # optional, for MinIO et al.
    redis:                              # required when backend=redis
      addr: ""                          # e.g. redis.internal:6379
      password: ""                      # inline (prefer password_env)
      password_env: ""                  # env var name
      db: 0
      tls: false
      prefix: ""                        # namespace for shared-Redis deployments
    sweep_interval: 10m                 # how often expired sessions/tokens are pruned
                                        # (0 = default 10m; negative disables)
                                        # NOTE: a cadence, not a lifetime.
                                        # `ttl:` is the deprecated old name.

  token_ttl: 5m                         # global Bearer-token lifetime
  session_ttl: 24h                      # global server-side session LIFETIME
                                        # (not to be confused with
                                        #  session.sweep_interval above)
  trusted_proxies: []                   # CIDRs whose X-Forwarded-For is honored

See iiif-auth.md for what each field means in the context of the four patterns and four backends.

Render quality: sharpening + supersampling

Contracts: specs/render-quality.md (sharpening) and specs/image-decode.md (supersampling).

Four image: fields tune the perceived sharpness of derivatives. All are off by default — the defaults reproduce iiiris's existing output and performance byte-for-byte, so an upgrade changes nothing unless you opt in.

Field Env var Default Range
image.sharpen IIIRIS_IMAGE_SHARPEN 0.0 (off) 0.01.0
image.sharpen_sigma IIIRIS_IMAGE_SHARPEN_SIGMA 0.0 (= libvips default, 1.5) >= 0.0
image.sharpen_x1 IIIRIS_IMAGE_SHARPEN_X1 0.0 (= libvips default, 2.0) >= 0.0
image.supersample IIIRIS_IMAGE_SUPERSAMPLE 1 (off) 0, 1, 2, or 4

0 and 1 both mean "off" — 0 is accepted as a synonym for 1, not rejected. Any other unsupported value is a startup error, not a silent clamp: an operator who typed supersample: 3 should learn it is not a supported factor rather than have it quietly rounded.

sharpen — unsharp mask on downscaled renders

Downscaling discards high-frequency detail, so a reduced derivative reads soft even when the resample itself is clean. image.sharpen applies an unsharp mask that restores apparent acutance.

  • 0.5 reproduces libvips' own default unsharp strength. The operator-facing 0.01.0 strength maps linearly onto libvips' jagged-area slope as m2 = sharpen × 6.0, and libvips' default m2 is 3.0. That mapping is frozen: changing it would silently alter the output of every deployment that has set image.sharpen. Start at 0.5 and adjust from there.
  • sharpen_sigma (radius) and sharpen_x1 (the flat/jagged threshold) are advanced knobs; 0 on either means "use libvips' default". Most operators only need sharpen.

When sharpening applies. Only when neither axis was enlarged and at least one axis shrank — i.e. detail was genuinely discarded and none was interpolated up:

Request Sharpened?
Plain downscale (pct:25, !400,400, w, smaller than source) yes
max / full, or a 1:1 crop (nothing shrank) no
Any upscale no
A distorting forced size that enlarges one axis while shrinking the other (e.g. ^2000,100 on an 800×600 region) no

Sharpening an enlarged (interpolated) axis amplifies resample ringing, and sharpening a full-resolution tile amplifies sensor noise and JPEG artifacts on exactly the pixels a deep-zoom user is inspecting most closely — so those cases are left alone.

Two things are never sharpened, regardless of the setting:

  • Overlays. The unsharp stage runs before overlay compositing, for both target=source and target=output overlays, so a watermark stays crisp instead of acquiring halos. See hooks.md.
  • CMYK pixels. libvips' unsharp mask works on the L channel of LAB, so sharpening an image that is still CMYK when the stage runs would force a lossy CMYK→LAB→CMYK round-trip. iiiris leaves it alone rather than degrade colour to add acutance.

This is a rule about CMYK pixels, not CMYK sources — which decode path a request takes decides which it gets. An arbitrary-region or full-decode render keeps the source's colour space, so a CMYK master really does arrive at the stage in CMYK and is left unsharpened. A full-region downscale decodes through libvips' shrink-on-load thumbnail operator, which converts to sRGB on the way, so the same CMYK master arrives as sRGB and is sharpened — safely, on sRGB pixels, with no round-trip. Either way colour is never degraded for the sake of acutance; that is the guarantee.

supersample — force a real resample to run

This is the setting that fixes "why is my exact power-of-two request soft?"

iiiris decodes resolution-aware: it picks the deepest reduction the source codec supports (JPEG DCT shrink-on-load, JP2 / pyramidal-TIFF level) that still covers the output, then does a residual Lanczos resize down to the exact target. When the request happens to land on a power of two — an 8192 px region rendered at 1024 px — the decode lands exactly on the target, the residual resize becomes a no-op, and the bytes you serve are raw libjpeg-DCT / pyramid-level output that no Lanczos resampling ever touched. That is visibly soft.

supersample: 2 (or 4) sets the minimum linear headroom the decode must leave above the output: the reduction backs off a level (or two), so a real resample always runs on the way down.

  • It changes pixels, never dimensions. Output W×H is identical at supersample: 1, 2, and 4, so info.json never misreports what the server produces. See specs/image-decode.md.
  • Cost. More decode work, not an N² blow-up: the reduction chosen for the decode never backs off below R=1, so supersampling can never cost more than a full-resolution decode of the region — that bound holds for both the arbitrary-region (tile) and full-region (thumbnail) paths. The full-region path adds one more clamp on top: its inflated decode target is additionally capped at the source dimensions, so the intermediate never exceeds full source resolution and a forced size can never trigger an enlarge-then-reduce round-trip. Measured on an Apple M1 Max against the bundled 800×600 sample (approximate, machine-dependent — measure on your own corpus):
Path ss=1 ss=2 ss=4
Full-region thumbnail (pct:12.5) 1.59 ms 3.18 ms 4.73 ms
Arbitrary-region tile (512×512 → w=128) 1.03 ms 1.71 ms 2.59 ms

Roughly 2.5–3× the render time from ss=1 to ss=4 on that corpus. Large masters with deep pyramids will differ; the render cache absorbs the cost for repeat requests.

supersample pairs naturally with sharpen: supersampling puts a real Lanczos reduce back in the path, and sharpening restores the acutance that any clean reduce discards.

Operational notes

  • Both settings are restart-only. They are not part of the admin config editor's live-reload subset (see admin.md); the render-cache salt below is computed once at wiring.
  • Changing either setting rekeys the render cache rather than serving stale renders. The render-quality settings are reduced to a short digest that is appended to the render cache key (|q=<hash>), so a retune (0.50.8) lands renders in a fresh key namespace and the old entries age out through normal eviction. No operator flush is needed, and nothing is ever served stale.
  • A default deployment evicts nothing on upgrade. The |q= segment is append-only: with sharpen: 0 and supersample: 1 the digest is empty and keys keep their existing bare-urlPath shape. See specs/scaffold.md "Cache contract".

Colour policy: sRGB vs ICC passthrough

Contract: specs/color-policy.md.

image.color controls the colour space of derivatives, independently of image.metadata (which only governs EXIF/XMP/IPTC and orientation). Two explicit values:

Field Env var Default Values
image.color IIIRIS_IMAGE_COLOR "" (derive from image.metadata) srgb, respect-icc
  • srgb — normalize everything to sRGB: colour sources (≥ 3 bands) → sRGB with a ~0.5 KB micro ICC profile embedded; grayscale (≤ 2 bands) → sGray, never promoted to RGB; CMYK → sRGB.
  • respect-icc — leave an RGB source's embedded ICC profile (and its pixels) untouched, so a wide-gamut viewer honours the original profile; grayscale stays untouched too. CMYK is still always converted to sRGB — that step is never negotiable, because no mainstream browser renders a CMYK JPEG correctly.

Any other value is a startup error, not a silent fallback.

Derivation when color is unset

Leaving image.color unset (the default) derives it from image.metadata, so an upgrade that never touches image.color changes nothing except the CMYK fix:

image.metadata effective color Result
strip (default) srgb today's behaviour, unchanged
preserve respect-icc today's behaviour, except CMYK now converts to sRGB

This derivation is deliberate, operator-facing magic: reading metadata: preserve in isolation does not otherwise tell you it implies a colour policy, so this table is the whole story.

Setting color explicitly overrides the derivation in either direction. The headline new combination this decoupling enables:

image:
  metadata: strip        # drop GPS / camera / serial
  color: respect-icc      # ...but keep the source's ProPhoto/AdobeRGB profile

Before this feature, the only way to keep a wide-gamut profile was metadata: preserve, which also retained camera model, serial number, and GPS — a privacy regression the metadata default exists to prevent. metadata: strip + color: respect-icc expresses "strip identifying metadata, keep colour fidelity" for the first time.

Colour policy is restart-only and, like sharpen/supersample, its effective value folds into the render-quality cache salt (|q=<hash>): srgb contributes nothing (zero churn), respect-icc makes the salt non-empty. A preserve-mode deployment therefore rekeys automatically on upgrade to this feature, so any stale CMYK-passthrough entries a warm cache holds self-heal without an operator flush. See specs/color-policy.md.

Env vars

Var Overrides
IIIRIS_ADDR server.addr
IIIRIS_LOG_LEVEL log.level
IIIRIS_LOG_FORMAT log.format
IIIRIS_LOG_FILE log.file
IIIRIS_SOURCE_DEFAULT sources.default
IIIRIS_FS_ROOT sources.filesystem.root
IIIRIS_HTTP_BASE_URL sources.http.base_url
IIIRIS_HTTP_TIMEOUT sources.http.timeout
IIIRIS_S3_BUCKET sources.s3.bucket
IIIRIS_S3_REGION sources.s3.region
IIIRIS_S3_PREFIX sources.s3.prefix
IIIRIS_S3_ENDPOINT sources.s3.endpoint
IIIRIS_MAX_PIXEL_AREA image.max_pixel_area
IIIRIS_IMAGE_METADATA image.metadata
IIIRIS_IMAGE_COLOR image.color
IIIRIS_VIPS_CONCURRENCY image.vips_concurrency
IIIRIS_VIPS_MAX_CACHE_MEM image.vips_max_cache_mem
IIIRIS_IMAGE_SHARPEN image.sharpen
IIIRIS_IMAGE_SHARPEN_SIGMA image.sharpen_sigma
IIIRIS_IMAGE_SHARPEN_X1 image.sharpen_x1
IIIRIS_IMAGE_SUPERSAMPLE image.supersample
IIIRIS_METRICS_ENABLED metrics.enabled
IIIRIS_METRICS_PATH metrics.path
IIIRIS_METRICS_ADDR metrics.addr
IIIRIS_METRICS_USER metrics.username
IIIRIS_METRICS_PASS metrics.password
IIIRIS_SENTRY_ENABLED sentry.enabled
IIIRIS_SENTRY_DSN sentry.dsn
IIIRIS_SENTRY_ENVIRONMENT sentry.environment
IIIRIS_SENTRY_RELEASE sentry.release
IIIRIS_SENTRY_SAMPLE_RATE sentry.sample_rate
IIIRIS_SENTRY_SEND_DEFAULT_PII sentry.send_default_pii
IIIRIS_SENTRY_CAPTURE_PANICS sentry.capture.panics
IIIRIS_SENTRY_CAPTURE_SERVER_ERRORS sentry.capture.server_errors
IIIRIS_SENTRY_CAPTURE_INTERNAL_ERRORS sentry.capture.internal_errors
IIIRIS_TRACING_ENABLED tracing.enabled
IIIRIS_TRACING_OTLP_ENDPOINT tracing.otlp_endpoint
IIIRIS_TRACING_INSECURE tracing.insecure
IIIRIS_TRACING_SAMPLE_RATE tracing.sample_rate
IIIRIS_TRACING_PROPAGATE_UPSTREAM tracing.propagate_upstream
IIIRIS_ADMIN_USER Admin Basic-auth user (no YAML equivalent)
IIIRIS_ADMIN_PASS Admin Basic-auth password

Env-var policy. Overrides cover the flat operational fields — bind address, logging, the default source and its roots, the image guards/knobs above, the flat metrics.* block, the sentry.* block (including the sentry.capture.* toggles), and the tracing.* block. Everything else is YAML-only: the rest of image.* (max_output_area/width/height, max_concurrent, max_concurrent_per_ip, tile_size, jpeg_progressive) and the structured caches.* / hook.* / auth.* blocks, which are too nested for env vars. For env-only deployments (Helm, 12-factor), set the flat tweaks via env on top of a mounted config file that carries the structured blocks.

AWS credentials follow the standard AWS SDK chain (AWS_ACCESS_KEY_ID/AWS_SECRET_ACCESS_KEY, ~/.aws/credentials, IMDS).

Windows-specific defaults

When iiirisd is installed via the Windows MSI (see deployment.md) the installer seeds a default config.yaml at %ProgramData%\iiiris\config\config.yaml with these Windows-aware values:

Setting Default in installed config
server.addr :8080
log.level info
log.format json (recommended for service mode — parses cleanly)
log.file C:\ProgramData\iiiris\logs\iiirisd.log
sources.filesystem.root C:\path\to\your\images (operator-editable placeholder)
caches.render.path C:\ProgramData\iiiris\cache\render
caches.origin.path C:\ProgramData\iiiris\cache\origin

When iiirisd runs under the Windows Service Control Manager (the MSI's service-install mode), an additional Windows Event Log handler writes to the Event Log source iiiris registered by the installer under HKLM\SYSTEM\CurrentControlSet\Services\EventLog\ Application\iiiris. The handler maps slog levels to Event Log severity (Info → Info, Warn → Warning, Error → Error) and truncates records at the Event Log's ~32 KB ceiling. Events appear in Event Viewer → Applications and Services Logs → iiiris. See deployment.md for operational detail.

Validation

iiiris fails fast at boot on invalid cache config:

  • Unknown backend value → unknown backend "x" (want "heap", "filesystem", "s3", or "redis")
  • backend: filesystem with empty pathfilesystem backend requires a path
  • backend: s3 with max_bytes != 0s3 backend ignores max_bytes (use S3 lifecycle policies)
  • backend: s3 with empty s3.buckets3.bucket is required
  • backend: redis with max_bytes != 0redis backend ignores max_bytes (use Redis MAXMEMORY + an eviction policy)
  • backend: redis with empty redis.addrredis backend requires redis.addr
  • backend: redis with an unreachable redis.addr → connection PING error surfaced at boot
  • backend: redis with a coordinator.redis.addr also set → backend=redis and coordinator.redis are mutually exclusive (coordinator promotes filesystem backend to distributed mode; pick one)
  • backend: filesystem with coordinator.redis.addr set and unreachable → connection PING error surfaced at boot

…and on invalid auth: config:

  • auth.rules referencing a profile name not in auth.profilesauth: unknown profile
  • access_service.backend: external without url: or callback_secret_env: → build error naming the missing field
  • access_service.backend: external with callback_secret_env: set but the named env var unset/empty → build error
  • access_service.backend: oidc missing issuer: / client_id: / client_secret_env: → build error
  • access_service.backend: <other>unknown access_service.backend
  • auth.session.backend other than heap / filesystem / s3 / redisunsupported session backend
  • auth.session.backend: filesystem with empty path:session backend=filesystem requires session.path

image.metadata does not fail fast: any value other than the exact string preserve is treated as strip. The failure mode deliberately favours privacy — a typo leaves stripping on rather than silently shipping EXIF/GPS. - auth.session.backend: filesystem with an unwritable path: → mkdir error surfaced at build time - auth.session.backend: s3 with empty s3.bucket:session backend=s3 requires session.s3.bucket - auth.session.backend: redis with empty redis.addr:session backend=redis requires session.redis.addr - auth.session.backend: redis with an unreachable address → PING error surfaced at boot - auth.kiosk.allowed_ips or auth.trusted_proxies containing an unparseable IP/CIDR → build error naming the entry - auth.profiles.<name>.substitute.max_size not parseable as an IIIF v3 size string → build error

…and on invalid image: render-quality config:

  • image.sharpen outside 0.01.0image.sharpen: <v> out of range (want 0.0–1.0, 0 = off)
  • image.sharpen_sigma below 0image.sharpen_sigma: <v> must be >= 0 (0 = libvips default)
  • image.sharpen_x1 below 0image.sharpen_x1: <v> must be >= 0 (0 = libvips default)
  • image.supersample other than 0, 1, 2, or 4 (e.g. 3 or 8) → image.supersample: <n> unsupported (want 1, 2, or 4) (0 is accepted and treated the same as 1, not rejected)
  • image.color other than "", srgb, or respect-iccimage.color: "<v>" unsupported (want "srgb" or "respect-icc")