Skip to content

Scaffold

The durable foundation of iiiris — the invariants every later feature must respect. This spec replaces docs/briefs/SCAFFOLD.md now that the scaffold has shipped end-to-end and been validated by every subsequent phase of work.

Project priorities

Trade-offs in the codebase resolve toward these, in order. When two priorities conflict, the higher-numbered one yields.

  1. Complete IIIF Image API compliance. Level 2 of IIIF Image API 3.0 and 2.x both, validated by the official IIIF validators in CI. No "almost compliant" shortcuts.
  2. Rich feature set, comprehensive caching. Three caches (RenderCache, InfoCache, OriginCache) with pluggable backends. Correct HTTP cache semantics on every response.
  3. Easy deployment. Single binary. Zero-config startup works (no flags, no file, no env vars). Container image is first-class. Env-var-only operation supported; YAML config file is opt-in.
  4. Popular storage backends from day one. Filesystem, HTTP, S3 all first-class.
  5. Highest image-processing performance. libvips throughout, streaming where possible, no avoidable re-decodes, benchmark suite present.

Module + binary

  • Module path: git.iiiris.org/iiiris.
  • Single binary: cmd/iiirisd. New main packages are added cautiously; the project's centre of gravity is one server, not a toolkit.
  • Go version: modern stable (Go 1.25+ at time of writing; the floor moves with security-supported releases).

Package boundaries

Standard cmd/ + internal/ Go layout. The interior boundary set is described in docs/architecture.md; the durable invariants are:

  • internal/image is the only consumer of govips. Other packages probe via image.Probe / image.Pipeline. This keeps govips-aware code in one place if a second image backend is ever needed (no premature Processor interface today).
  • internal/iiif owns IIIF route grammar. v2/ParsePath and v3/ParsePath are authoritative; handlers reconstruct the IIIF tail from r.PathValue(...) and call the parser. Grammar is not duplicated in handlers.
  • Source / cache / hook / auth are interface-first. Each has at least two production impls (e.g. Filesystem + Heap for caches; Filesystem + HTTP + S3 for sources) so the interface stays honest. New backends plug in without touching call sites.
  • internal/observability owns logger construction and the /health + /ready endpoints. /health is liveness (always 200 if the process is alive); /ready runs every source's Healthchecker in parallel with a bounded timeout.
  • internal/metrics owns the optional /metrics endpoint (off by default) and is the only consumer of prometheus/client_golang. Instrumented packages expose observer hooks and stay metrics-library-free. Contract: docs/specs/metrics.md.

Configuration contract

  • Zero-config is non-negotiable. ./bin/iiirisd with no flags, no file, and no env vars must boot a working IIIF server against the bundled testdata/sample.jpg. Don't add a required config field without supplying a default in config.Default().
  • YAML is optional. -config <path> enables file-driven configuration; absence falls through to defaults.
  • Env-var overrides with the IIIRIS_* prefix. See internal/config/load.go for the full table; docs/configuration.md for the user-facing reference.

Cache contract

  • Three caches, distinct keys. RenderCache keyed by auth.RenderKey(r.URL.Path, tier, overlay); InfoCache by auth.InfoKey(r.Host, r.URL.Path, tier); OriginCache by sourceName + ":" + identifier. The public tier with no overlay preserves the legacy unsalted key shape; gated tiers append |v1|<tier>, and an applied overlay appends |o=<hash> (append-only — see docs/specs/hooks.md "Render-cache keying folds in the overlay").
  • Tier vocabulary changes are cache-key version bumps. Bumping cacheKeyVersion in internal/auth/cachekey.go cleanly invalidates existing entries. Adding a new tier value counts as a vocabulary change — bump.
  • Eviction is policy-specific. Heap + Filesystem use size-bounded LRU. S3 delegates to bucket lifecycle. Redis uses MAXMEMORY policy
  • optional per-entry TTL.
  • max_bytes != 0 with backend: s3 or redis errors at boot. Those backends don't enforce in-app size caps.
  • Build via the cache.BuildRender / BuildInfo / BuildOrigin factories — never instantiate concrete backends in handlers.

Source contract

  • Three first-class sources: Filesystem, HTTP, S3.
  • HTTP and S3 are fronted by OriginCache. Wrap via source.Cached in cmd/iiirisd/main.go; filesystem is bypassed (it is itself an on-disk store).
  • Missing identifiers return source.ErrNotFound. The server maps that to HTTP 404. Don't propagate raw filesystem errors.
  • Optional capability interfaces. Lister (filesystem + S3) and Healthchecker (all three) are discoverable via source.AsLister / source.AsHealthchecker through the Cached.Unwrap chain. Future backends opt in as relevant.

IIIF compliance contract

  • Both versions are first-class. v2 and v3 each have their own parsers, info.json builders, and route handlers. Migration scenarios keep working as v2 is deprecated upstream.
  • Supported feature set is single-sourced. SupportedFormats, SupportedQualities, SupportedExtraFeatures are package-level slices in internal/iiif/v3/ (and the v2 equivalents). Both the parsers and the info.json builder consume them — change the slice, the advertisement and validation update together.
  • Pipeline limits and info.json caps are paired. image.max_pixel_area / max_output_area / max_output_width/height / max_concurrent drive both enforcement and the advertised maxWidth/maxHeight/maxArea. info.json never promises what the pipeline will refuse.
  • Validators gate CI. The IIIF Image API validator runs against the live build for both v2 and v3 in iiif-validate; auth conformance runs via iiif-auth-validate. Both ride the same pattern (build → run → exercise → capture log).

HTTP cache semantics contract

  • Every IIIF response carries Cache-Control and ETag. Conditional GETs (If-None-Match / If-Modified-Since) return
  • Wired via http.ServeContent in server.serveBytes.
  • CORS is open on the IIIF surface. Access-Control-Allow-Origin: * on every /iiif/... response. Content negotiation on info.json honors Accept: application/ld+json.

Architectural seams (reserved at scaffold)

Each was introduced as an interface with a no-op or allow-all impl so later features land without rewriting call sites. All have since shipped real implementations:

  • internal/hookHook interface (Resolve / Authorize / Redirect). Four impls: Noop, Lookup, Lua, Webhook.
  • internal/authAuthorizer interface; AllowAll + RuleAuthorizer. See docs/specs/auth.md.
  • Pipeline overlay/watermark stage — reserved insertion point in internal/image. No-op today; renderer can add a stage without reshaping the pipeline.
  • Metadata + ICC threading — shipped as the metadata-privacy feature. Default (image.metadata: strip) normalizes colour to sRGB/sGray (micro ICC profile) and orientation (autorotate), then strips EXIF/XMP/IPTC; preserve reattaches everything verbatim. See docs/iiif-compliance.md and docs/specs/image-decode.md.
  • /health + /admin/* — both wired at scaffold; admin grew into a full operator UI (docs/admin.md).

Build / test / run / CI contract

  • make build / make test / make bench / make run / make lint / make docker / make release-binary are the canonical entry points. Don't add competing top-level invocations.
  • libvips is a runtime dependency (CGO via govips). The local build path requires the host's libvips; the distroless image statically links it. See docs/deployment.md.
  • CI runs on personal runners tagged iiiris for Docker-based jobs; Windows-specific jobs use saas-windows-medium-amd64. The default: tags: [iiiris] at the top of .gitlab-ci.yml enforces this; per-job overrides are explicit.
  • The IIIF validators gate the pipeline. iiif-validate (v2 + v3) runs allow-failure; residual failures are upstream validator bugs / spec disagreements documented in docs/iiif-compliance.md. iiif-auth-validate is load-bearing — a regression in any auth pattern fails the pipeline.

Versioning + releases

The release contract — what triggers a release, what artifacts ship, versioning rules, latest semantics, rollback policy — is owned by releases.md. The operator-facing walkthrough (image-tag conventions, upgrade paths, distroless vs. debian-slim trade-offs) is in ../deployment.md.

The invariant the scaffold preserves: CHANGELOG.md is authoritative for what's shipped. Edits to [Unreleased] land in normal feature/fix MRs; the release flow flips [Unreleased] to [vX.Y.Z] at tag-cut. Don't leave WIP: entries past the change shipping.

Out of scope (deliberately)

The scaffold deliberately stops before the following; each has its own brief or spec where it landed (or stays open).

  • Premature processor abstraction — single libvips backend; no Processor interface. Reintroduce only if a second backend arrives.
  • Hook scripting beyond Noop at scaffold-time — Lua, Lookup, Webhook all shipped later. See docs/hooks.md.
  • IIIF Auth beyond AllowAll at scaffold-time — Auth 2.0 (and 1.0 dual-advertisement) shipped. See docs/specs/auth.md + docs/iiif-auth.md.
  • Helm / Kubernetes manifests — not on the active roadmap.
  • Concurrent iiirisd processes sharing one filesystem cache dir — single-process ownership is assumed; documented in docs/deployment.md.