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 credsGET /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 unlesspresentation.enabled: false. Seepresentation.md.GET /view/{rest...}— bundled IIIF viewer (Universal Viewer / Mirador) for a manifest; mounted with the Presentation API. Reads theiiif-contentparameter (Content State) whencontent_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 unlesscontent_state.enabled: false. Non-/iiif/namespace signals these are iiiris conveniences, not a standard server API. Seecontent-state.md.POST /content-state,GET /content-state/s/{id}— Content State bookmark store (Form-B hosting), off by default. Mounted only whencontent_state.store.enabled; writes gated by a Bearer token, reads public. Backed byinternal/annostore./mcp— Model Context Protocol server (Streamable HTTP; read-only IIIF tools for AI agents). Mounted only whenmcp.enabled: true(off by default). Seemcp.md.* /iiif/auth/...— IIIF Authorization Flow 2.0 services (probe, login, token, callback, logout); mounted only when anauth: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. Seeiiif-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 viaimage.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) thatcmd/iiirisdwires when metrics are enabled. - Sources return
source.ErrNotFoundfor missing identifiers; the server maps that to 404. Caches returncache.ErrMissfor 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 inv{2,3}Handler.dispatchby splittingrestand treating the last four (or one, forinfo.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:
RenderCacheusesauth.RenderKey(r.URL.Path, tier);InfoCacheusesauth.InfoKey(r.Host, r.URL.Path, tier)(the host is included because the renderedidfield embeds it);OriginCacheusessourceName + ":" + identifier. Thepublictier preserves the legacy unsalted key shape, so introducing auth to a deployment does not churn unrelated cache entries. Seeiiif-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.