Skip to content

Architecture

Standard cmd/ + internal/ layout. Single binary: cmd/iiirisd.

URL surface

  • GET / — minimal landing page; renders the iiiris wordmark on a blank canvas. Confirms the server is reachable without leaking any operational detail.
  • GET /logo.svg, GET /favicon.svg — public-facing brand assets, embedded into the binary.
  • GET /health — liveness (always 200 if process is alive)
  • GET /ready — readiness (200 + per-source JSON if all healthy, 503 otherwise)
  • * /admin/... — operator UI; HTTP Basic gated on env creds
  • GET /iiif/{2,3}/{rest...} — IIIF Image API endpoints (catchall route)
  • GET /iiif/presentation/{2,3}/{rest...} — IIIF Presentation API (manifests + collections; serve stored or auto-derive from a directory); mounted unless presentation.enabled: false. See presentation.md.
  • GET /view/{rest...} — bundled IIIF viewer (Universal Viewer / Mirador) for a manifest; mounted with the Presentation API. Reads the iiif-content parameter (Content State) when content_state.enabled.
  • GET /content-state/mint, GET|POST /content-state/resolve — IIIF Content State helpers (mint a shareable view state; decode/inspect one). Mounted with the Presentation API unless content_state.enabled: false. Non-/iiif/ namespace signals these are iiiris conveniences, not a standard server API. See content-state.md.
  • POST /content-state, GET /content-state/s/{id} — Content State bookmark store (Form-B hosting), off by default. Mounted only when content_state.store.enabled; writes gated by a Bearer token, reads public. Backed by internal/annostore.
  • /mcp — Model Context Protocol server (Streamable HTTP; read-only IIIF tools for AI agents). Mounted only when mcp.enabled: true (off by default). See mcp.md.
  • * /iiif/auth/... — IIIF Authorization Flow 2.0 services (probe, login, token, callback, logout); mounted only when an auth: block with profiles is configured.
  • * /iiif/auth/v1/... — IIIF Authentication API 1.0 surface (login, token, logout — no probe in 1.0); mounted alongside the 2.0 services for legacy-viewer compatibility. The 1.0 login and logout share the 2.0 handlers; only the token-response shape and the advertised service tree differ. See iiif-auth.md.

Request flow (image)

HTTP route ─► collapseSlashes middleware (fold runs of "/" before routing)
              ─► server middleware (count + per-route stats, log, optional per-IP cap, recover)
                 ─► hook.Hook.Redirect(ctx, identifier)
                    ├─ non-empty URL → 303 (request stops here)
                    └─ empty         → continue
                 ─► IIIF parser (v2 or v3) ─► iiif.Request
                    ─► auth.Authorizer.Authorize(ctx, r, identifier)
                       ├─ Allow         ─► tier = public | full
                       ├─ Deny+Substitute → rewrite identifier/size, tier = substitute
                       ├─ Deny+Redirect  → 303
                       └─ Hard deny      → write Decision.Status + Body
                    ─► RenderCache lookup (key: auth.RenderKey(r.URL.Path, tier))
                       on miss ─► hook.Resolve(identifier) ─► (backend, key)
                                  ─► source.Source.Open(key)  (filesystem | http | s3)
                                     └── HTTP/S3 wrapped in source.Cached → OriginCache
                                     ─► image.Pipeline.Execute (govips)
                                        (semaphore + source/output-area guards)
                                        ─► RenderCache.Put + serveBytes
                                           (ETag, 304, CORS, JSON-LD for info.json)

Request flow (info.json)

HTTP route ─► hook.Hook.Redirect(ctx, identifier)  (303 short-circuit, same as image path)
              ─► auth.Authorizer.ProfileFor(identifier)
              ─► tier = public | full        (info.json doesn't degrade)
              ─► InfoCache lookup (key: auth.InfoKey(r.Host, r.URL.Path, tier))
                 on miss ─► source.Open ─► Pipeline.Probe (header-only dim
                            parse — TIFF IFD / JP2 markers / stdlib DecodeConfig
                            — falling back to a libvips decode; concurrency-
                            bounded, separate gate from Execute)
                            ─► v2.BuildInfo or v3.BuildInfo
                               (auth service tree from auth.BuildAdvertisement
                                attached when ProfileFor != nil)
                               ─► InfoCache.Put + serveBytes

Request flow (presentation)

HTTP route ─► hook.Hook.Redirect (303 short-circuit, same as image path)
              ─► ManifestCache lookup (key: host + version + identifier)
                 on miss ─► hook.Resolve(identifier) ─► (backend, key)
                            ─► directory?  (source.Lister.List succeeds)
                               ├─ no  ─► source.Open ─► serve as-is
                               │         (normalise top-level id/@id only)
                               └─ yes ─► derive:
                                         images   → Manifest (Probe each, paint
                                                    Image API service per canvas)
                                         subdirs  → Collection (member per subdir)
                                         overrides: conventions → sidecar → hook
                               ─► (optional) presentation.Validate (warn-only)
                               ─► ManifestCache.Put + serveBytes (ETag, 304, ld+json)

Packages

Package Role
cmd/iiirisd Binary entrypoint. Loads config, builds caches/sources/admin, runs *server.Server.
internal/config YAML loader + env-var overrides; Default() for zero-config startup.
internal/iiif Version-agnostic types (Request, Region, Size, Rotation, Format, Quality).
internal/iiif/v2 IIIF 2.x parsers + info.json.
internal/iiif/v3 IIIF 3.0 parsers + info.json.
internal/presentation IIIF Presentation API 3.0 + 2.1: path parsing, id normalisation, manifest/collection builders, descriptor model (sidecar + optional Describer hook capability), and Validate. Pure data → JSON; the server layer owns the listing + image.Probe I/O. Contract: specs/presentation.md · guide: presentation.md.
internal/contentstate IIIF Content State API 1.0 (stateless core): the iiif-content codec (Encode/Decode — base64url ∘ encodeURIComponent), plus Parse/Build/Validate. Pure and I/O-free; parse-only — never dereferences a target (SSRF guard). The server layer wires it into /content-state/ and /view. Guide: content-state.md.
internal/annostore Durable, non-evicting store for annotations (JSON-LD) by id — Put/Get, filesystem-backed with atomic writes. Backs Content State Form-B hosting; shaped so a Phase 3a W3C annotation store subsumes it. Contrast internal/cache (evicts). Guide: content-state.md.
internal/mcp Model Context Protocol server (read-only IIIF tools for AI agents). Only consumer of modelcontextprotocol/go-sdk. Pure protocol glue: a Streamable HTTP handler over a Backend interface (browse / get_image_info / get_region / get_manifest), which internal/server implements by reusing its resolve/probe/render/list and Presentation serve/derive primitives. Off by default (/mcp). Guide: mcp.md.
internal/source Source interface + Filesystem/HTTP/S3 impls + Cached wrapper. Optional Lister (filesystem + S3) and Healthchecker (all three) interfaces.
internal/cache RenderCache / InfoCache / OriginCache interfaces; Heap / Filesystem / S3 / Redis backends (+ None to disable a slot) via the Build* factories. Contract: specs/cache-filesystem.md · guide: caches.md.
internal/image The govips pipeline (the only govips consumer): Pipeline.Execute renders, Probe reads info.json dims. Decodes only the resolution + bytes a request needs — shrink-on-load, mmap'd source I/O, header-only dimension probe; jp2box.go wraps raw J2K codestreams for govips. Composites hook-driven overlays (watermarks) at stage 6, after rotation + quality. In strip mode (the image.metadata default) stage 7 normalizes colour to sRGB/sGray + strips EXIF/XMP/IPTC, and the decode path autorotates oriented originals (Probe reports oriented dims to match). Contract: specs/image-decode.md; behaviour: iiif-compliance.md.
internal/server net/http ServeMux routing, middleware (slash-collapse, counter, per-route stats, logging, optional per-IP concurrency cap, panic recovery), v2 + v3 handlers, Stats for the admin dashboard. serveBytes emits CORS + JSON-LD content negotiation on every IIIF response. Contract: specs/http-interface.md.
internal/admin Operator UI under /admin/ (HTTP Basic auth): live SSE dashboard, structured config editor, cache purge, file browser, IIIF viewer. Server-rendered HTML + one embedded stylesheet; vendored OpenSeadragon under internal/admin/static/.
tools/gen-sample Regenerates the bundled testdata/sample.jpg synthetic test fixture.
internal/hook Hook interface with engines: Noop, Lookup (text/template), JS (sandboxed goja, recommended), Lua (sandboxed gopher-lua, deprecated — removed in 1.0), Webhook (HTTP POST per call). Built via hook.Build. Optional capabilities (detected by type assertion): presentation.Describer, Overlayer (watermark placement).
internal/auth Authorizer interface (AllowAll default, RuleAuthorizer impl): rule engine, profiles, htpasswd, session storage, real-IP extraction, tier-keyed cache helpers, and the info.json auth advertisement. Contract: specs/auth.md · guide: iiif-auth.md.
internal/auth/service HTTP handlers for the four IIIF Auth 2.0 services (probe, access/login, access-token, logout) plus external + OIDC callback. Mounted at /iiif/auth/... from main.go when profiles are configured.
internal/auth/htpasswd bcrypt + Apache APR1-MD5 verification; inline users: map and mtime-watched htpasswd_file: sources.
internal/auth/session Opaque session + token records; in-memory heap store today.
internal/observability slog setup, /health (liveness), /ready (readiness — runs every source's Healthchecker in parallel).
internal/metrics Prometheus exposition (opt-in via metrics.enabled; the only prometheus/client_golang consumer): dedicated registry, /metrics handler with optional Basic auth, and the observer implementations fed by hooks in server / cache / source / image / auth — those packages stay metrics-library-free and cmd/iiirisd wires the observers at startup. Contract: specs/metrics.md.
internal/telemetry Error tracking (Sentry) + distributed tracing (OpenTelemetry) — the only consumer of both getsentry/sentry-go and the OTel SDK. Errors (opt-in via sentry.enabled + a DSN): SDK init with a privacy scrubber (PII off by default), plus CapturePanic / CaptureServerError plain-func hooks the server calls (panics from the recover middleware; 5xx + source/decode/cache errors, the latter attached at the handler error site via a per-request holder). Tracing (opt-in via tracing.enabled): an OTLP/gRPC tracer provider, a root-span TraceMiddleware handed to the server as an opaque func(http.Handler) http.Handler, and Traced* decorators wrapping the source/cache interfaces (parallel to the metrics Observed* wrappers). Sentry errors are stamped with the active trace ID via NewOtelIntegration. The server and source/cache packages stay free of both SDKs; cmd/iiirisd builds everything, wires the hooks/wrappers, and flushes on shutdown. Contract: specs/telemetry.md.

Conventions

  • The image package is the only consumer of govips. Other packages render and probe via image.Pipeline (Execute / Probe).
  • The metrics package is the only consumer of prometheus/client_golang. Instrumented packages expose plain-func observer hooks (cache.OpObserver, source.OpenObserver, image.Observer, auth.RuleAuthorizer.Recorder, server.Deps.ObserveRequest) that cmd/iiirisd wires when metrics are enabled.
  • Sources return source.ErrNotFound for missing identifiers; the server maps that to 404. Caches return cache.ErrMiss for misses.
  • IIIF route grammar is owned by iiif/v{2,3}.ParsePath, which takes the five components (identifier, region, size, rotation, quality.format) pre-split. Server uses a single catchall route per version (/iiif/3/{rest...}) and dispatches in v{2,3}Handler.dispatch by splitting rest and treating the last four (or one, for info.json) segments as the IIIF tail; everything before is the identifier.
  • Supported IIIF formats / qualities / extra-features are sourced from package-level slices in internal/iiif/v{2,3}/. Both parsers and info.json builder consume them — change the slice, the advertisement and validation update together.
  • Cache keys: RenderCache uses auth.RenderKey(r.URL.Path, tier); InfoCache uses auth.InfoKey(r.Host, r.URL.Path, tier) (the host is included because the rendered id field embeds it); OriginCache uses sourceName + ":" + identifier. The public tier preserves the legacy unsalted key shape, so introducing auth to a deployment does not churn unrelated cache entries. See iiif-auth.md.

Windows service lifecycle

iiirisd.exe detects Windows Service Control Manager context at startup via svc.IsWindowsService(). When launched by SCM (the MSI's service-install path), it runs through a parallel entrypoint that reuses the same setup() + serve() core as the console path; only the stop signal differs.

SCM ─► main() ─► isWindowsService() == true
              ─► runService("iiiris")
                 ─► setup(cfg, configPath, extras...)
                    (extras = Event Log handler + rolling file)
                 ─► svc.Run(name, &windowsService{bundle})
                    ─► signals StartPending → Running
                    ─► reads svc.ChangeRequest channel
                       on Stop/Shutdown ─► close(stop)
                 ─► serve(bundle, stop)
                    ─► same listen + select + Shutdown as console
                 ─► signals Stopped, returns exit code to SCM

Build-tagged files keep the Windows surface isolated: cmd/iiirisd/service_windows.go + signals_windows.go + is_service_windows.go mirror their *_unix.go counterparts. The non-Windows side is a no-op stub so the package compiles on every target; runtime dispatch happens via isWindowsService() returning false.