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.
- 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.
- Rich feature set, comprehensive caching. Three caches
(
RenderCache,InfoCache,OriginCache) with pluggable backends. Correct HTTP cache semantics on every response. - 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.
- Popular storage backends from day one. Filesystem, HTTP, S3 all first-class.
- 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/imageis the only consumer ofgovips. Other packages probe viaimage.Probe/image.Pipeline. This keeps govips-aware code in one place if a second image backend is ever needed (no prematureProcessorinterface today).internal/iiifowns IIIF route grammar.v2/ParsePathandv3/ParsePathare authoritative; handlers reconstruct the IIIF tail fromr.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+Heapfor caches;Filesystem+HTTP+S3for sources) so the interface stays honest. New backends plug in without touching call sites. internal/observabilityowns logger construction and the/health+/readyendpoints./healthis liveness (always 200 if the process is alive);/readyruns every source'sHealthcheckerin parallel with a bounded timeout.internal/metricsowns the optional/metricsendpoint (off by default) and is the only consumer ofprometheus/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/iiirisdwith no flags, no file, and no env vars must boot a working IIIF server against the bundledtestdata/sample.jpg. Don't add a required config field without supplying a default inconfig.Default(). - YAML is optional.
-config <path>enables file-driven configuration; absence falls through to defaults. - Env-var overrides with the
IIIRIS_*prefix. Seeinternal/config/load.gofor the full table;docs/configuration.mdfor the user-facing reference.
Cache contract¶
- Three caches, distinct keys.
RenderCachekeyed byauth.RenderKey(r.URL.Path, tier, overlay);InfoCachebyauth.InfoKey(r.Host, r.URL.Path, tier);OriginCachebysourceName + ":" + identifier. Thepublictier with no overlay preserves the legacy unsalted key shape; gated tiers append|v1|<tier>, and an applied overlay appends|o=<hash>(append-only — seedocs/specs/hooks.md"Render-cache keying folds in the overlay"). - Tier vocabulary changes are cache-key version bumps. Bumping
cacheKeyVersionininternal/auth/cachekey.gocleanly 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 != 0withbackend: s3orrediserrors at boot. Those backends don't enforce in-app size caps.- Build via the
cache.BuildRender / BuildInfo / BuildOriginfactories — never instantiate concrete backends in handlers.
Source contract¶
- Three first-class sources:
Filesystem,HTTP,S3. - HTTP and S3 are fronted by
OriginCache. Wrap viasource.Cachedincmd/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) andHealthchecker(all three) are discoverable viasource.AsLister/source.AsHealthcheckerthrough theCached.Unwrapchain. 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,SupportedExtraFeaturesare package-level slices ininternal/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_concurrentdrive both enforcement and the advertisedmaxWidth/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 viaiiif-auth-validate. Both ride the same pattern (build → run → exercise → capture log).
HTTP cache semantics contract¶
- Every IIIF response carries
Cache-ControlandETag. Conditional GETs (If-None-Match/If-Modified-Since) return - Wired via
http.ServeContentinserver.serveBytes. - CORS is open on the IIIF surface.
Access-Control-Allow-Origin: *on every/iiif/...response. Content negotiation oninfo.jsonhonorsAccept: 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/hook—Hookinterface (Resolve / Authorize / Redirect). Four impls: Noop, Lookup, Lua, Webhook.internal/auth—Authorizerinterface; AllowAll + RuleAuthorizer. Seedocs/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;preservereattaches everything verbatim. Seedocs/iiif-compliance.mdanddocs/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-binaryare 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
iiirisfor Docker-based jobs; Windows-specific jobs usesaas-windows-medium-amd64. Thedefault: tags: [iiiris]at the top of.gitlab-ci.ymlenforces 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 indocs/iiif-compliance.md.iiif-auth-validateis 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
Processorinterface. 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.