Web annotations versioned like code, served like a database.
Setup and deployment: start with doc/installation.md.
Store annotations in Git, serve them from an optimized read-only store. Use familiar version control workflows without any database setup.
- Git-based workflow: Familiar version control for annotations
- No database required: Just files and Git
- Optimized serving: Read-optimized store for fast, efficient HTTP API
- Stable IDs: Generated at serve-time from filesystem structure
- Horizontal scaling: Serve millions of annotations across multiple instances
- Separation of concerns: Storage/serving decoupled from search
Option A: Clone from Git (recommended)
Use miiify-clone to get started with sample annotation data:
miiify-clone https://github.com/jptmoore/miiify-sample-data.git --git ./git_storeUsing Docker? Click to see Docker commands
# Clone using Docker (using pre-built image from GHCR)
docker run --rm -v $(pwd)/git_store:/home/miiify/git_store ghcr.io/nationalarchives/miiify:latest \
/home/miiify/miiify-clone https://github.com/jptmoore/miiify-sample-data.git --git ./git_storeOption B: Create from filesystem
Create annotation files manually:
# Create directory structure
mkdir -p annotations/my-canvas
# Create an annotation
cat > annotations/my-canvas/highlight-1.json << 'EOF'
{
"type": "Annotation",
"motivation": "highlighting",
"body": {
"type": "TextualBody",
"value": "Important passage",
"purpose": "commenting"
},
"target": "https://example.com/iiif/canvas/1#xywh=100,100,200,50"
}
EOF
# Create another annotation
cat > annotations/my-canvas/comment-1.json << 'EOF'
{
"type": "Annotation",
"motivation": "commenting",
"body": {
"type": "TextualBody",
"value": "This is a fascinating detail",
"purpose": "commenting"
},
"target": "https://example.com/iiif/canvas/1#xywh=300,150,100,75"
}
EOFThen import them into Git storage:
miiify-import --input ./annotations --git ./git_storeUsing Docker? Click to see Docker commands
# Import using Docker (using pre-built image from GHCR)
docker run --rm \
-v $(pwd)/annotations:/home/miiify/annotations \
-v $(pwd)/git_store:/home/miiify/git_store ghcr.io/nationalarchives/miiify:latest \
/home/miiify/miiify-import --input ./annotations --git ./git_storeCompile Git storage to optimized pack storage for serving (required for both options):
miiify-compile --git ./git_store --pack ./pack_storeUsing Docker? Click to see Docker commands
# Compile using Docker (using pre-built image from GHCR)
docker run --rm \
-v $(pwd)/git_store:/home/miiify/git_store \
-v $(pwd)/pack_store:/home/miiify/pack_store ghcr.io/nationalarchives/miiify:latest \
/home/miiify/miiify-compile --git ./git_store --pack ./pack_store# Start the HTTP API server
miiify-serve --repository ./pack_store --port 10000Using Docker? Click to see Docker commands
# Serve using Docker (using pre-built image from GHCR)
docker run --rm -p 10000:10000 \
-v $(pwd)/pack_store:/home/miiify/pack_store ghcr.io/nationalarchives/miiify:latest \
--repository ./pack_store --port 10000 --base-url http://localhost:10000
# Or use Docker Compose
docker compose up -d# Get annotation
curl http://localhost:10000/my-canvas/highlight-1
# List all annotations (AnnotationCollection)
curl http://localhost:10000/my-canvas/
# Get specific page (AnnotationPage)
curl http://localhost:10000/my-canvas/?page=0Miiify injects stable, URL-based annotation id fields at serve time. IDs are derived from filesystem structure and deployment configuration—never stored in your JSON files.
How it works:
- IDs follow the pattern:
<base-url>/<container>/<slug> - Container = directory name (e.g.,
my-canvas) - Slug = filename without
.jsonextension (e.g.,highlight-1) - Base URL is configurable via
--base-urlflag
Example:
# Your file: annotations/my-canvas/highlight-1.json
# Served with ID: "http://localhost:10000/my-canvas/highlight-1"Deployment flexibility:
# Development
miiify-serve --base-url http://localhost:10000
# Production
miiify-serve --base-url https://annotations.example.orgSame JSON files, different IDs based on deployment. No database updates, no file edits—just change the flag.
Miiify implements a separation of concerns: it provides storage and serving infrastructure, while search functionality is delegated to solutions such as annosearch. This architectural decision enables annotations to be organized by canvas but indexed at IIIF Collection level. Storage and search strategies evolve independently.
See doc/api.md for the full HTTP API reference.
See doc/scaling.md for horizontal scaling strategies from single projects to institution-wide deployments serving millions of annotations.
See doc/commands.md for the full command reference.