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.

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
  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)

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 | lua | 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)
  lua:                        # used when type: lua — DEPRECATED, removed in 1.0; use js
    path: ""                  # path to .lua file
    script: ""                # OR inline script (one or the other)
    watch_interval: 5s        # hot-reload poll cadence (path mode); 0=default, <0=disabled
  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
    ttl: 10m                            # sweep interval for expired sessions (heap only)

  token_ttl: 5m                         # global Bearer-token lifetime
  session_ttl: 24h                      # global server-side session lifetime
  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.

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_VIPS_CONCURRENCY image.vips_concurrency
IIIRIS_VIPS_MAX_CACHE_MEM image.vips_max_cache_mem
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