Dit document beschrijft een op Docker gebaseerde systeemarchitectuur voor het Jottem-platform: een participatief digitaal erfgoedplatform waarin gebruikers media (jottems) uploaden, moderatoren deze beoordelen en publiceren, en annoteerders verrijkingen toevoegen. Het geheel wordt via subdomeinen onder iotm.nl ontsloten en implementeert de in het designdocument genoemde standaarden: IIIF Image API 3.0, IIIF Presentation API 3.0, IIIF Change Discovery, W3C Web Annotations, schema.org AP NDE, ARK en RSS.
1. Uitgangspunten
-
Open source first: elk component is een bewezen open-source product of een dunne maatwerklaag daaromheen. Succesvolle onderdelen zijn herbruikbaar tussen Time Machines.
-
Standaarden aan de buitenkant: alle publieke interfaces zijn standaard-API’s (IIIF, W3C Annotation Protocol, SPARQL/RDF, RSS), zodat harvesters en aggregators (o.a. NDE) zonder maatwerk kunnen aansluiten.
-
Duurzame opslag identificatie: elke gepubliceerde jottem krijgt een duurzame platform-URL met content negotiation naar HTML of RDF. Besluit: ARK-minting (NOID/UUID-objectnaam, resolver op ark.iotm.nl) is uitgesteld naar een latere fase - zie het keuzedocument in het designdocument; URL’s worden zo gekozen dat latere ARK-koppeling zonder linkbreuk kan.
-
Multi-tenant: een platforminstallatie bedient meerdere organisatiejottems (Streekarchief Midden-Holland, historische verenigingen, andere Time Machines), elk met eigen huisstijl, NAAN/shoulder en gebruikers.
-
Beheersbaarheid: het geheel draait als een docker-compose-stack op een enkele VPS (of dedicated server) en kan later doorgroeien naar Kubernetes zonder architectuurbreuk.
-
Getest en gemonitord: elk component wordt geautomatiseerd getest (unit, integratie, contract op de standaard-API’s en end-to-end over de stack) en draait onder centrale monitoring, logging en alerting, zodat regressies en storingen vroeg en aantoonbaar zichtbaar worden.
2. Contextdiagram
Het platform staat tussen vijf rollen (bezoeker, gebruiker/uploader/annoteerder, moderator, beheerder, API-gebruiker) en een aantal externe systemen: social login providers, het Internet Archive voor duurzame opslag (latere fase), het NDE Datasetregister, tijdmachines als kaart-/tijdinterface, een SMTP-provider en het NDE Termennetwerk (GraphQL) als toegang tot terminologiebronnen (Cultuurhistorische Thesaurus, Wikidata, GTAA, ...) voor URI’s in metadata en annotaties.
3. Componentenoverzicht en subdomeinen
| Subdomein | Component | Open-source suggestie | Alternatieven |
|---|---|---|---|
www.iotm.nl
| Webfrontend | Next.js of SvelteKit + Mirador/Tify (IIIF-viewer), Annotorious (annoteren), MapLibre GL (kaart) | Nuxt, Universal Viewer, OpenSeadragon + eigen UI |
api.iotm.nl
| Backend-API (REST, IIIF Presentation, Change Discovery, RSS, zoeken) | FastAPI (Python) met iiif-prezi3 | NestJS, Laravel, Django REST Framework |
auth.iotm.nl
| Identity provider (OIDC, social login, 2FA/passkeys) | Authentik (gekozen) | Keycloak, Ory Kratos/Hydra |
iiif.iotm.nl
| IIIF Image API 3.0 + HTTP-cache | Cantaloupe achter Varnish | iipsrv, SIPI, go-iiif; nginx proxy_cache |
anno.iotm.nl
| W3C Web Annotation server | miiify (expliciet genoemd in het designdocument) | SimpleAnnotationServer, Annotot |
ark.iotm.nl
| ARK-resolver en -minter | Arklet (Internet Archive) | eigen microservice + noid-bibliotheek (pynoid) |
data.iotm.nl
| RDF-publicatie (schema.org AP NDE), SPARQL, datadumps, datasetbeschrijvingen | Apache Jena Fuseki | Oxigraph, QLever, Virtuoso OS |
| (intern) | Zoekmachine | Elasticsearch (genoemd in requirements) | OpenSearch |
| (intern) | Relationele database | PostgreSQL | MariaDB |
| (intern) | Cache en taakwachtrij | Valkey (gekozen; Redis-compatibel) | Redis, RabbitMQ (queue) |
| (extern) | Mediaopslag (S3) | Externe Object Storage-dienst (gekozen) | MinIO (zelf gehost) |
| (intern) | Detectie herkenbare personen | Herkenbaar API (eigen dienst: FastAPI + YOLO-pose) | cloud vision API’s (vallen af: beelden moeten on-premise blijven) |
| (intern) | Async workers | Celery (bij FastAPI) of BullMQ (bij NestJS) | RQ, Sidekiq |
| (edge) | Reverse proxy, TLS | Traefik | Caddy, nginx-proxy + acme-companion |
De volgende paragrafen lichten de belangrijkste componenten toe.
3.1. Reverse proxy (Traefik)
Traefik termineert TLS (automatisch via Let’s Encrypt), routeert op subdomein naar de juiste container via Docker-labels, en voegt security headers en rate limiting toe. Alle publieke componenten hangen aan het edge-netwerk; datastores zijn uitsluitend intern bereikbaar.
De rate limiting is per IP-adres met burst, het strengst op de endpoints die dure backends raken. Indicatieve startwaarden (bij te stellen op basis van monitoring): zoek-API en SPARQL 5 req/s (burst 15), beheer-API en meldingen/rapporteren 2 req/s (burst 5), annotatie- en metadata-reads 30 req/s, IIIF-tiles en thumbnails ruim (die komen vrijwel altijd uit de Varnish-cache). Overschrijding levert 429 Too Many Requests met Retry-After; bulk-afnemers worden naar de datadumps en feeds verwezen.
3.2. Authenticatie en autorisatie (Authentik)
Authentik fungeert als centrale OIDC-provider op auth.iotm.nl en doet uitsluitend authenticatie: registratie voor bezoekers, social login (Google, Facebook, Apple, Microsoft) via federatie, wachtwoord-login, TOTP-2FA en passkeys (WebAuthn). Voor accounts die via een beheeruitnodiging binnenkomen dwingt de enrollment-flow het instellen van een wachtwoord én een tweede factor (TOTP of passkey) af.
Autorisatie: de database is leidend. Rollen staan niet in de IdP maar in GebruikerRol (per organisatie, meerdere rollen per gebruiker - zie het ERD in de data-architectuur). De backend koppelt het access-token via de sub-claim aan een Gebruiker-rij en bepaalt daarna zelf de rechten (bijv. "moderator van organisatie X mag alleen jottems van X beoordelen"). Zo blijven de uitnodigings- en beheerflows in de applicatie-UI, is er geen groepensynchronisatie met de IdP nodig en is er één bron van waarheid voor rollen.
De werking in de praktijk: een FastAPI-dependency (auth-middleware) valideert per request het access-token (handtekening, expiry, amr), zoekt via de sub-claim de gebruiker en diens GebruikerRol-rijen op en hangt die als principal aan de request. De rollen-lookup wordt per sub kort gecachet in Valkey (TTL orde van een minuut); bij elke rolwijziging via de beheer-API wordt die cache-entry direct geïnvalideerd, zodat intrekking vrijwel meteen effect heeft zonder dat elke request de database raakt.
Het JWT-claims-contract dat de backend van Authentik nodig heeft, is daarmee klein:
| Claim | Gebruik |
|---|---|
sub
| stabiele koppeling token → Gebruiker (bij eerste login wordt de rij aangemaakt of aan de uitgenodigde rij gekoppeld)
|
email, email_verified
| e-mailadres (uitnodigingen, notificaties) |
name / preferred_username
| weergavenaam (zie Gebruiker.naamPubliek voor publicatie)
|
amr (auth-methode)
| de backend eist een sterke factor (TOTP of passkey) voor endpoints die een beheer- of moderatierol vragen |
De uitnodigingsflow: de beheer-API verstuurt de uitnodigingsmail met bevestigingslink en zet de GebruikerRol klaar; de link leidt naar een Authentik-enrollment-flow (wachtwoord + verplichte tweede factor: TOTP of passkey); bij de eerste login koppelt de backend de sub aan de uitgenodigde Gebruiker-rij.
Fijnmazige autorisatie op objectniveau (eigen afgekeurde jottems verwijderen, goedgekeurde niet) hoort in de backend thuis; wie dit declaratief wil, kan een policy engine als Casbin of OpenFGA toevoegen, maar voor deze schaal volstaat autorisatielogica in de API-laag.
3.3. Backend-API en frontend
De backend-API op api.iotm.nl is het maatwerkhart van het platform. Hij implementeert het datamodel uit het ERD (organisatie, project, gebruiker, media, metadata, favoriet) op PostgreSQL en biedt:
-
REST-endpoints voor upload (via presigned S3-URL’s, zodat grote bestanden niet door de API stromen), moderatieworkflow, projecten, favorieten, materiaaltypes/genres en koppelingen naar externe archiefbronnen (label + URI);
-
IIIF Presentation API 3.0: per jottem een manifest, per organisatie/project een collection, gegenereerd uit de database met een bibliotheek als iiif-prezi3;
-
IIIF Change Discovery 1.0: een ActivityStreams-feed per organisatie met create/update-activiteiten (inclusief nieuwe annotaties) voor harvesters;
-
RSS-feeds per organisatie met nieuwe jottems;
-
zoek-endpoints die Elasticsearch bevragen (fulltext, facetten op genre, organisatie, periode, plaats);
-
een proxy op de GraphQL API van het NDE Termennetwerk voor term-lookups (genres via de Cultuurhistorische Thesaurus, term-URI’s), beperkt tot de per project ingestelde terminologiebronnen;
-
statistieken voor beheerders en moderatoren.
De frontend op www.iotm.nl is een aparte container (Next.js of SvelteKit) met de publieke site, de HTML-weergave per jottem (IIIF-viewer + metadata + annotaties), de upload- en moderatie-omgeving, en de interactieve kaart. Voor het tekenen van vlakken op afbeeldingen en het vastleggen van geo-annotaties (standpunt fotograaf, zichtveld als WKT/GeoJSON) is Annotorious in combinatie met OpenSeadragon/Mirador en MapLibre een beproefde keuze; de kaartweergave kan via IIIF en de API gekoppeld worden aan tijdmachines (zoals de Gouda Tijdmachine).
3.4. IIIF Image API (Cantaloupe + Varnish)
Cantaloupe serveert op iiif.iotm.nl de Image API 3.0 (info.json, tiles, regio’s, formaten) en leest bronbestanden rechtstreeks uit de S3-opslag (S3Source). Bij goedkeuring van een jottem maakt een worker een pyramidal TIFF of JPEG2000 als afgeleide, wat tiling zonder zware rekenlast mogelijk maakt. Varnish (of nginx proxy_cache) staat ervoor en cachet tiles en info.json agressief; gepubliceerde beelden zijn immutable, dus lange TTL’s zijn veilig. Zo blijft de image server ook bij piekverkeer (bijv. na een oproep in lokale media) responsief.
3.5. Annotaties (miiify)
miiify, de annotatieserver van The National Archives (UK) die ook in het designdocument wordt genoemd, implementeert het W3C Web Annotation Protocol en slaat annotaties versiebeheerd op (git-gebaseerde backend), wat goed past bij de wens om herinneringen, aanvullingen en correcties herleidbaar te houden. De frontend schrijft annotaties (vlak-identificaties van personen/gebouwen/bedrijven met naam + URI, plaatsnamen, gebeurtenissen, archiefbronkoppelingen, vrije tekst en reacties) via de backend-API naar miiify; de API voegt daarbij de geauthenticeerde gebruiker als creator toe en bewaakt autorisatie (bewerken/verwijderen van eigen annotaties). Lezen kan publiek en standaardconform via anno.iotm.nl, zodat externe partijen annotaties per jottem kunnen ophalen en doorzoeken.
3.6. Detectie herkenbare personen (Herkenbaar API)
De Herkenbaar API is een eigen dienst (FastAPI met een YOLO-pose-model via ultralytics) die bepaalt of op een afbeelding herkenbare personen staan, ten behoeve van het portretrecht en de privacy-eisen uit het designdocument. De dienst draait als interne container op het app-netwerk en is niet publiek bereikbaar.
De backend-API roept de dienst synchroon aan bij het aanmaken van een jottem: na de upload (via presigned URL naar de mediaopslag) haalt de backend het detectieresultaat op en slaat dit op bij de media (herkenbaar ja/nee plus betrouwbaarheid). Bij "herkenbaar: ja" vraagt de frontend de uploader direct om een toestemmingsverklaring van de afgebeelde personen; de moderator ziet het detectiesignaal en de verklaring bij de kwaliteitscontrole. De detectie is een hulpmiddel - de moderator beslist.
Privacy-uitgangspunt: de inferentie draait volledig op de eigen server; beelden verlaten het platform niet en er worden geen gezichtsherkennings- of identificatietechnieken gebruikt, uitsluitend detectie van de aanwezigheid van herkenbare personen.
3.7. RDF-publicatie en datasetbeschrijvingen (Fuseki)
Elke publicatie of wijziging triggert een worker die de jottem-metadata mapt naar RDF conform het schema.org-profiel van NDE (schema.org AP NDE) en deze upsert in Apache Jena Fuseki op data.iotm.nl. Daarmee ontstaat:
-
een resolvebare RDF-representatie per jottem (JSON-LD, Turtle, RDF/XML via content negotiation);
-
een SPARQL-endpoint per organisatie (aparte named graphs);
-
periodieke datadumps (N-Triples/Turtle, gzip) als distributie;
-
per organisatie een datasetbeschrijving als
schema:Datasetconform de NDE-requirements voor datasets, met de dump en het SPARQL-endpoint alsschema:distribution, aangemeld bij het NDE Datasetregister.
De mapping zelf kan in applicatiecode (rdflib) of declaratief met RML/CARML; voor een overzichtelijk model als dit is applicatiecode het eenvoudigst te onderhouden.
3.8. ARK-resolving (latere fase)
*In de MVP is de duurzame link een platform-URL met content negotiation (zie de URI-strategie in de data-architectuur); ARK-minting en -resolving zijn uitgesteld naar een latere fase (zie het keuzedocument). Onderstaande beschrijving legt vast hoe die fase eruitziet, zodat URL’s nu al ARK-klaar gekozen worden.*
Bij goedkeuring mint de backend een objectnaam (NOID via pynoid, of UUID) onder de NAAN en shoulder van de organisatie en registreert deze in de resolver op ark.iotm.nl. Arklet van het Internet Archive is hiervoor een lichtgewicht, S3- en Postgres-vriendelijke keuze; een eigen microservice van enkele tientallen regels is een reeel alternatief omdat de resolvelogica beperkt is. De resolver ondersteunt content negotiation en ARK-conventies zoals ?info voor metadata over het object zelf:
Belangrijk duurzaamheidsprincipe: de ARK-registry (koppeling ARK naar actuele locaties) staat in PostgreSQL en wordt meegenomen in de backup en in de metadata die naar het Internet Archive gaat, zodat resolving ook bij migratie van het platform herstelbaar blijft.
3.9. Dataopslag, caching en mediaopslag
PostgreSQL is de primaire bron van waarheid voor het volledige ERD en de moderatiestatussen (een ARK-registry volgt pas in de latere ARK-fase). Valkey (Redis-compatibel) dient drie doelen: sessie-/API-cache, rate limiting en de taakwachtrij voor de workers. De mediaopslag is een externe Object Storage-dienst (S3) - geen eigen opslagbeheer en de capaciteit groeit mee zonder serverwijziging - met gescheiden buckets: originals (niet publiek, alleen via API en workers), derivatives (pyramidal TIFF’s voor Cantaloupe), thumbs (publiek, lange cache-headers), audio voor audio-jottems zoals interviews (fase 2, zie de uploadscope in de requirements) en exports (e-depot-exportpakketten, beperkte bewaartermijn). Alle componenten spreken uitsluitend het S3-protocol, dus een zelf-gehost alternatief (MinIO) blijft transparant inwisselbaar; de leverancierskeuze is een exploitatiebesluit.
Caching gebeurt op drie niveaus: Varnish voor IIIF-tiles en manifests, Valkey voor API-responses (zoekresultaten, collections, statistieken) en HTTP-cache-headers plus ETags op alle publieke endpoints zodat ook harvesters efficient werken.
3.10. Async workers en Internet Archive
Een workerpool (Celery met Valkey als broker) handelt alles af wat niet in de request-cyclus hoort: genereren van derivatives en thumbnails, EXIF-extractie, e-mailnotificaties (goedkeuring, afkeuring met reden, uitnodigingen), Elasticsearch-indexering, RDF-synchronisatie naar Fuseki en e-depot-exports per project (BagIt + RO-Crate, zie de data-architectuur). In fase 2 komen daar de conversiepipelines voor PDF (pagina’s naar afgeleiden voor Cantaloupe, multi-canvas Manifest) en audio (transcodering plus duration voor het IIIF-audio-canvas) bij. De Internet Archive-doorzetting van goedgekeurde media (via de internetarchive Python-client, met de IA-itemidentifier terug in de database en de RDF, bijv. als schema:archivedAt) behoort niet tot de MVP en volgt in een latere fase (zie het keuzedocument).
Betrouwbare doorwerking van mutaties (outbox). Het Gebeurtenislog uit het ERD fungeert als transactional outbox: elke mutatie die outputs raakt (goedkeuring, metadata-wijziging, annotatiemutatie, depublicatie) schrijft binnen dezelfde databasetransactie een logregel. De workers verwerken die regels asynchroon en idempotent naar Elasticsearch, de Fuseki named graph en een Varnish-purge/ban van de geraakte URL’s; pas na succes wordt de regel als verwerkt gemarkeerd, en bij falen wordt opnieuw geprobeerd met backoff. Een nachtelijke reconciliatiejob vergelijkt PostgreSQL met de zoekindex en de triplestore en repareert verschillen, zodat de outputs ook na een crash of storing aantoonbaar bijlopen.
4. Deployment met Docker
De stack draait met docker compose op een enkele host, met drie netwerken: edge (publiek via Traefik), app (interne servicecommunicatie) en data (datastores, niet publiek routeerbaar). Een indicatieve docker-compose.yml:
services : traefik : image : traefik:v3ports : ["80:80" ,"443:443" ]volumes : - /var/run/docker.sock:/var/run/docker.sock:ro - traefik_acme:/acmenetworks : [edge ]frontend : build : ./frontendlabels : -"traefik.http.routers.www.rule=Host(`www.iotm.nl`)" networks : [edge ,app ]api : build : ./apienvironment : DATABASE_URL : postgres://jottem@postgres/jottemREDIS_URL : redis://valkey:6379S3_ENDPOINT : https://s3.example-objectstorage.eu# externe Object Storage-dienst OIDC_ISSUER : https://auth.iotm.nl/application/o/jottem/ES_URL : http://elasticsearch:9200labels : -"traefik.http.routers.api.rule=Host(`api.iotm.nl`)" networks : [edge ,app ,data ]worker : build : ./apicommand : celery -A jottem workerdeploy : {replicas : 2 }networks : [app ,data ]authentik : image : ghcr.io/goauthentik/server:latestcommand : serverlabels : -"traefik.http.routers.auth.rule=Host(`auth.iotm.nl`)" networks : [edge ,data ]cantaloupe : image : uclalibrary/cantaloupe:latestenvironment : SOURCE_STATIC : S3Sourcenetworks : [app ,data ]varnish : image : varnish:stablelabels : -"traefik.http.routers.iiif.rule=Host(`iiif.iotm.nl`)" networks : [edge ,app ]miiify : image : nationalarchives/miiify:latestlabels : -"traefik.http.routers.anno.rule=Host(`anno.iotm.nl`)" volumes : [miiify_data :/data ]networks : [edge ,app ]ark : image : internetarchive/arklet:latestlabels : -"traefik.http.routers.ark.rule=Host(`ark.iotm.nl`)" networks : [edge ,data ]fuseki : image : stain/jena-fuseki:latestlabels : -"traefik.http.routers.data.rule=Host(`data.iotm.nl`)" volumes : [fuseki_tdb2 :/fuseki ]networks : [edge ,app ]postgres : image : postgres:16volumes : [pg_data :/var/lib/postgresql/data ]networks : [data ]valkey : image : valkey/valkey:8networks : [data ]elasticsearch : image : elasticsearch:8.14.0environment : {discovery.type : single-node }volumes : [es_data :/usr/share/elasticsearch/data ]networks : [data ]herkenbaar : build : ./herkenbaar-apinetworks : [app ]prometheus : image : prom/prometheus:latestvolumes : [prometheus_data :/prometheus ]networks : [app ,data ]alertmanager : image : prom/alertmanager:latestnetworks : [app ]grafana : image : grafana/grafana:latestenvironment : GF_AUTH_GENERIC_OAUTH_ENABLED : "true" # SSO via Authentik labels : -"traefik.http.routers.status.rule=Host(`status.iotm.nl`)" volumes : [grafana_data :/var/lib/grafana ]networks : [edge ,app ]loki : image : grafana/loki:latestvolumes : [loki_data :/loki ]networks : [app ]promtail : image : grafana/promtail:latestvolumes : - /var/run/docker.sock:/var/run/docker.sock:ro - /var/lib/docker/containers:/var/lib/docker/containers:ronetworks : [app ]blackbox : image : prom/blackbox-exporter:latest# uptime/TLS/inhoud van publieke endpoints networks : [app ,edge ]cadvisor : image : gcr.io/cadvisor/cadvisor:latestvolumes : [/ :/rootfs :ro ,/var/run :/var/run :ro ,/sys :/sys :ro ]networks : [app ]node-exporter : image : prom/node-exporter:latestnetworks : [app ]volumes : traefik_acme : {}pg_data : {}es_data : {}fuseki_tdb2 : {}miiify_data : {}prometheus_data : {}grafana_data : {}loki_data : {}networks : edge : {}app : {internal : true }data : {internal : true }
Aanvullend op de kernstack: Prometheus + Grafana + Loki + Alertmanager voor monitoring, logging en alerting (met exporters per datastore en een blackbox-exporter voor de publieke endpoints), pgBackRest voor database-backups en restic voor nachtelijke offsite backups van alle volumes naar een tweede opslaglocatie; de externe mediabuckets worden separaat geback-upt (bijv. met rclone naar diezelfde backup-opslag). De Internet Archive-kopie van gepubliceerd materiaal (latere fase) voegt daar te zijner tijd een tweede duurzaamheidslaag aan toe. De inrichting van testen en monitoring is uitgewerkt in paragraaf 5.
5. Doorsnijdende aspecten
Privacy en auteursrecht. Persoonsgegevens staan uitsluitend in PostgreSQL en Authentik; verwijderingsverzoeken worden ondersteund doordat originelen alleen via de API bereikbaar zijn en gepubliceerde jottems een depubliceer-workflow kennen (verwijderen uit index, cache-purge in Varnish, tombstone op de duurzame URL). Toestemming voor herkenbare personen en het onderscheid herinnering versus verifieerbaar feit zijn metadatavelden in de moderatieworkflow, geen infrastructuurcomponent.
Multi-tenancy. Organisatiejottems delen dezelfde stack; scheiding gebeurt via organisatie-id in de database, named graphs in Fuseki, prefixen in S3 en Elasticsearch, identiteiten in Authentik (rollen in de database) en shoulders binnen de ARK NAAN. Huisstijl (logo, kleurenpalet, favicon) is configuratie in de database die de frontend per organisatie-slug toepast.
Geautomatiseerd testen. Het testen volgt de testpiramide en draait volledig in CI (GitHub Actions of Forgejo Actions), met een gate op elke pull request en een nachtelijke volledige run:
-
Unit- en integratietests op de maatwerkcomponenten: pytest (met httpx/testcontainers voor Postgres, Valkey, een S3-testcontainer en Elasticsearch) voor de backend-API en workers, Vitest/Jest plus Testing Library voor de frontend. Doelstelling is dekking op de bedrijfslogica (moderatieworkflow, autorisatie op objectniveau, ARK-minting, RDF-mapping), niet op boilerplate.
-
Contracttests op de publieke standaarden, zodat "standaarden aan de buitenkant" ook aantoonbaar blijft: de IIIF Presentation- en Image API-validators, de W3C Web Annotation Protocol-testsuite tegen
anno.iotm.nl, SHACL-validatie van de gegenereerde RDF tegen het schema.org AP NDE-profiel, smoke-queries op het SPARQL-endpoint en schemavalidatie van de RSS- en ActivityStreams-feeds. Deze tests bewaken dat harvesters (o.a. NDE) zonder maatwerk blijven aansluiten. -
End-to-end tests met Playwright over de docker-compose-stack, die de kritieke ketens doorlopen: registratie/inlog via Authentik → upload via presigned URL → moderatie → publicatie → duurzame link met content negotiation → IIIF-manifest en tiles → annotatie → verschijnen in zoekindex, RDF en Change Discovery-feed. Dezelfde suite draait als synthetische monitoring in productie (zie hieronder).
-
Third-party open-source componenten (Authentik, Cantaloupe, miiify, Fuseki) testen we niet zelf op unitniveau, maar dekken we af met health-/smoke-tests, versie-pinning en gecontroleerde updates via Renovate/Dependabot; de contract- en e2e-suite fungeert als regressienet bij elke image-update. Security scanning (Trivy op images, dependency-audit) hangt aan dezelfde pipeline.
Monitoring en observability. De observability-stack (Prometheus, Grafana, Loki, Alertmanager) uit paragraaf 4 wordt als volgt ingericht:
-
Metrics. De maatwerkcomponenten exposen Prometheus-metrics via de OpenTelemetry/Prometheus-client (request-latency en foutratio per endpoint, Celery-queuediepte en taakduur/faalratio, upload- en derivative-doorlooptijd). Voor de overige componenten leveren exporters de systeem- en dienststatistieken: node_exporter en cAdvisor (host en containers), postgres_exporter, redis_exporter, elasticsearch_exporter en de metrics-endpoints van Traefik, Cantaloupe/Varnish en Authentik.
-
Uptime en synthetische checks. Een blackbox-exporter controleert elk publiek subdomein op bereikbaarheid, TLS-geldigheid en inhoudelijke correctheid (bijv. een valide
info.json, een resolvebare duurzame link en een lopend SPARQL-endpoint). De kritieke e2e-flow draait periodiek als synthetische test, zodat een gebroken keten opvalt vóór een gebruiker het meldt. Uptime Kuma is een lichtgewicht alternatief voor een los statuspagina/uptime-dashboard. -
Logging en tracing. Promtail verzamelt container- en Traefik-accesslogs naar Loki; een doorlopend request-id (gezet in Traefik, doorgegeven aan API en workers) maakt correlatie tussen logs, metrics en traces mogelijk. Distributed tracing is optioneel via OpenTelemetry naar Tempo of Jaeger, nuttig voor het volgen van een upload door API, workers en Cantaloupe.
-
Alerting. Alertmanager stuurt meldingen (e-mail/Slack) op vooraf gedefinieerde SLO’s en foutcondities: endpoint down of trage respons, verhoogde 5xx-ratio, oplopende worker-backlog, volraken van disk of S3, opvallend snel groeiend opslag- of ingestgebruik per organisatie (signaal in plaats van hard quotum, zie de requirements), gefaalde nachtelijke backups en naderend TLS-verloop. Grafana-dashboards per component en per organisatiejottem geven beheerders en moderatoren inzicht, aansluitend op de statistieken die de backend-API al levert.
-
Deployment. De monitoringcomponenten draaien als eigen services in de compose-stack op het
app- endata-netwerk; alleen Grafana wordt via Traefik ontsloten (bijv.status.iotm.nl) achter Authentik-authenticatie, de overige endpoints blijven intern.
Schaalpad. De eerste jaren volstaat een enkele VPS (8 vCPU, 16-32 GB RAM). Groeit het platform, dan schalen frontend, API, workers en Cantaloupe horizontaal het eenvoudigst; PostgreSQL en Elasticsearch verhuizen dan naar managed diensten of eigen nodes. Omdat alle koppelingen via standaardprotocollen lopen (HTTP, S3, OIDC, SPARQL) is die migratie een deployment-vraagstuk, geen architectuurwijziging.
VPS/dedicated server Bij Hetzner (DE) biedt de CCX33 voor €139 per maand (€1.668 per jaar) een VPS met gestelde specificaties. Een dedicated server met betere specs heb je via Hetzner’s "server auction" overigens al voor de helft van dat bedrag.
Zoals eerder gezegd, doordat alle koppelingen standaardprotocollen zijn (HTTP, S3, OIDC, SPARQL) en alles in Docker draait, blijft de overstap naar grotere of meerdere dedicated servers een deployment-vraagstuk, geen architectuurwijziging. Een dedicated server sluit het groeipad dus niet af, hij maakt de eerste stap alleen minder elastisch. Er is wel een kanttekening te maken ten aanzien van beschikbaarheid: bij één dedicated box betekent hardwarefalen downtime tot restore; een cloud-VM kan de provider naar gezonde hardware verplaatsen. Je backups (restic/pgBackRest/IA) dekken dataduurzaamheid, niet uptime. Server-auction-machines zijn bovendien gebruikte hardware.
Voor Jottem is een dedicated server een prima, kostenefficiënte keuze met veel headroom.
6. Samenvatting
| Requirement uit het designdocument | Invulling |
|---|---|
| Laagdrempelig inloggen incl. social login | Authentik met social-login-federatie |
| 2FA/passkeys voor beheerders | Authentik TOTP-/WebAuthn-enrollment + amr-controle in de backend |
| Duurzame ARK-links met NOID/UUID | Arklet + pynoid, registry in PostgreSQL |
| HTML- en RDF-weergave via content negotiation | ARK-resolver met 303-redirects naar frontend resp. Fuseki |
| IIIF Image API (info.json) | Cantaloupe + Varnish op iiif.iotm.nl |
| IIIF Presentation API (manifest/collection) | Backend-API met iiif-prezi3 |
| IIIF Change Discovery | ActivityStreams-feed in backend-API |
| W3C Web Annotations / Annotation Protocol | miiify op anno.iotm.nl |
| Elasticsearch-zoekfunctie | Elasticsearch, geindexeerd door workers |
| RSS per organisatie | Backend-API |
| schema.org AP NDE + datasetbeschrijvingen | Fuseki op data.iotm.nl + NDE Datasetregister |
| Internet Archive integratie | Workerpool met internetarchive-client |
| Mediaopslag (S3) | externe Object Storage-dienst (S3); MinIO als zelf-gehost alternatief |
| Geo-annotaties (WKT/GeoJSON) | Annotorious + MapLibre, opslag in miiify/metadata |
| Termen en vocabulaires (CHT, term-URI’s) | NDE Termennetwerk GraphQL API, via backend-proxy met bronnenselectie per project |
| Detectie herkenbare personen (portretrecht) | Herkenbaar API, synchroon aangeroepen bij upload |
| Geautomatiseerd testen | CI-pipeline: unit/integratie (pytest, Vitest), contracttests op IIIF/W3C/SHACL/SPARQL, e2e met Playwright |
| Monitoring, logging en alerting | Prometheus + exporters, Grafana, Loki/Promtail, Alertmanager, blackbox-/synthetische checks |
| Subdomeinen | Traefik-routing: www, api, iiif, ark, anno, auth, data, status |