dragoman
A web server for scholarly metadata with full DOI Resolution and DOI content negotiation. Send a DOI as the URL path; receive a redirect to the landing page or metadata in any supported format depending on the Accept header.
Installation
Prerequisites
- Rust 1.75+ (rustup.rs)
Install
This builds a release binary and installs it to ~/.cargo/bin/dragoman. Make sure ~/.cargo/bin is on your PATH (the Rust installer adds this automatically).
Local SQLite database
dragoman can serve metadata directly from a local SQLite database in the commonmeta format, bypassing the live Crossref/DataCite APIs. This dramatically reduces latency and API load for high-traffic deployments.
Database format
The database is a SQLite3 file with a single works table whose columns map one-to-one to the commonmeta v1.0 schema. The id column is the canonical DOI URL (e.g. https://doi.org/10.5281/zenodo.1234). Complex fields (contributors, references, …) are stored as JSON text.
You can build a database from any commonmeta-supported source using the commonmeta CLI.
Running the server
Start
# Default port 3456
# Custom port
# With a local database
# Write a PID file so the process can be stopped later
# All options together
TLS
The --domain flag controls whether and how dragoman serves HTTPS.
--domain value |
Behaviour |
|---|---|
| (not set) | Plain HTTP on 127.0.0.1:<port>. Use this behind a reverse proxy. |
localhost |
HTTPS on 127.0.0.1:<port> using a locally-trusted certificate from mkcert. |
| any public hostname | HTTPS on 0.0.0.0:<tls-port> with automatic certificate provisioning via Let's Encrypt. Requires --email. |
Local development (mkcert)
Install mkcert once and add its CA to your trust store:
Then start dragoman with --domain localhost:
dragoman runs mkcert automatically on the first start; subsequent starts reuse the cached certificate. Certificates are stored in --acme-cache (default /var/lib/dragoman/acme).
Production (Let's Encrypt)
dragoman provisions and renews certificates automatically using the ACME TLS-ALPN-01 challenge. No separate HTTP challenge server or reverse proxy is needed.
- Binds HTTPS on
--tls-port(default 443) with automatic certificate provisioning. - Binds HTTP on
--portwith a permanent redirect tohttps://commonmeta.org/…. - Stores certificates in
--acme-cache(default/var/lib/dragoman/acme) so they survive restarts.
Use --acme-staging while testing to avoid Let's Encrypt rate limits:
Options can also be supplied as environment variables (flags take precedence):
PORT=8080 COMMONMETA_DB=/data/commonmeta-2026-06-15.sqlite3 RUST_LOG=dragoman=debug
During development you can use cargo run in place of the installed binary:
# Run from the project root — the sqlite3 file in the root is loaded by filename
# Or with a full path
Error: port already in use
If the chosen port is already in use, the server logs an error and exits:
ERROR dragoman: failed to bind port=3456 error=Address already in use (os error 48)
Choose a different port with --port or stop the process that holds the port.
Database file not found
If the resolved database path does not exist, dragoman logs a warning and starts without a local database, falling back to the live API for all requests:
WARN dragoman: sqlite file not found, running without local database path=…
To use a local database, place the SQLite file at the platform default path or set COMMONMETA_DB to its location.
Stop
# Stop using the default PID file location (/tmp/dragoman.pid)
# Stop using a custom PID file
dragoman stop sends SIGTERM to the running process. The server handles the signal gracefully: it finishes in-flight requests and removes the PID file before exiting. Pressing Ctrl-C has the same effect.
CLI reference
dragoman <COMMAND>
Commands:
start Start the server (runs in the foreground)
stop Stop a running server by sending SIGTERM to its PID file
help Print help
dragoman start [OPTIONS]
-p, --port <PORT> TCP port to listen on [env: PORT] [default: 3456]
-d, --db <PATH> Local commonmeta SQLite3 database [env: COMMONMETA_DB]
--pid-file <PATH> Write PID to this file on startup [env: DRAGOMAN_PID_FILE]
--domain <DOMAIN> Domain for HTTPS. Use `localhost` for local dev (mkcert) or a public hostname for Let's Encrypt [env: DRAGOMAN_DOMAIN]
--email <EMAIL> ACME contact email for Let's Encrypt (required for public domains) [env: DRAGOMAN_EMAIL]
--acme-cache <PATH> Directory for ACME certificate storage [env: DRAGOMAN_ACME_CACHE] [default: /var/lib/dragoman/acme]
--acme-staging Use Let's Encrypt staging (testing only) [env: DRAGOMAN_ACME_STAGING]
--tls-port <PORT> HTTPS port when --domain is set [env: DRAGOMAN_TLS_PORT] [default: 443]
dragoman stop [OPTIONS]
--pid-file <PATH> PID file to read [env: DRAGOMAN_PID_FILE] [default: /tmp/dragoman.pid]
Environment variables
All flags can also be supplied as environment variables; flags take precedence. See env.example for an annotated template.
| Variable | Default | Description |
|---|---|---|
PORT |
3456 |
TCP port to listen on. |
COMMONMETA_DB |
(platform default) | Path to a local commonmeta SQLite3 database. Served before falling back to the live API. Platform defaults: macOS → ~/Library/Application Support/commonmeta/commonmeta.sqlite3; Linux → /var/lib/commonmeta/commonmeta.sqlite3. |
DRAGOMAN_CACHE_DB |
(platform default) | Path to the dragoman cache database for API fallback results. Platform defaults: macOS → ~/Library/Application Support/commonmeta/cache.sqlite3; Linux → /var/lib/commonmeta/cache.sqlite3. |
DRAGOMAN_PID_FILE |
(none) | Path for the PID file written by start and read by stop. |
DRAGOMAN_DOMAIN |
(none) | Domain for HTTPS. localhost → mkcert; public hostname → Let's Encrypt. Unset → plain HTTP on 127.0.0.1 (reverse proxy mode). |
DRAGOMAN_EMAIL |
(none) | ACME contact email. Required when DRAGOMAN_DOMAIN is a public hostname. |
DRAGOMAN_ACME_CACHE |
(platform default) | Directory for ACME/mkcert certificates. Platform defaults: macOS → ~/Library/Application Support/dragoman/acme; Linux → /var/lib/dragoman/acme. |
DRAGOMAN_ACME_STAGING |
(false) | Set to any non-empty value to use the Let's Encrypt staging environment. |
DRAGOMAN_TLS_PORT |
443 / 3456 |
HTTPS port when DRAGOMAN_DOMAIN is set. Defaults to 3456 for localhost, 443 for public domains. |
DRAGOMAN_VIKTORIALOGS_URL |
(none) | VictoriaLogs OTLP/HTTP endpoint. When unset, logs go to stdout only. |
DRAGOMAN_METRICS_ALLOW |
(none) | Comma-separated IPs allowed to scrape /metrics. When unset, the route is not registered. |
RUST_LOG |
dragoman=info |
Log filter (see tracing-subscriber). Use dragoman=debug for per-request cache hits. |
DOI Resolution
dragoman implements the Handle System resolution protocol. Two entry points are available for each PID:
| Endpoint | Purpose |
|---|---|
/{path} |
Render the entity page (HTML · Svelte UI) or content-negotiate (API) |
/api/handles/{path} |
Handle resolver: redirect to URL of record, content negotiation, or ?noredirect |
Entity page — /{path}
Visiting a PID in a browser renders the dragoman entity page (Svelte UI with rich metadata). Sending an API Accept header to the same URL triggers content negotiation instead (see Content negotiation).
# Open entity page in a browser
# Works for ROR organisation IDs and ORCID person IDs too
Paths that are not recognised PIDs redirect to the search page:
# Non-PID path → 307 to /?q=<path>
Handle resolver — /api/handles/{path}
The /api/handles/ endpoint is the programmatic resolution interface. Three outcomes are possible depending on the request:
Redirect to URL of record (default)
Without a recognised Accept header, /api/handles/{path} redirects to the PID's URL of record:
| PID type | Redirect target |
|---|---|
| DOI | URL from metadata (data.url); fallback https://doi.org/{doi} |
| ROR | Organisation website (from database); fallback https://ror.org/{id} |
| ORCID | https://orcid.org/{id} |
# Follow the redirect to the landing page
# Inspect the redirect target without following
# https://zenodo.org/record/1089100
View entity page — ?noredirect
The ?noredirect parameter redirects the browser to /{path} to render the entity page:
# 307 → http://localhost:3456/10.5281/zenodo.1089100
Content negotiation at /api/handles/{path}
Send a recognised Accept header to receive metadata in a specific format.
DOI
See Supported formats for the full list. The ?format= override is also accepted.
ROR (organisations)
Accept header |
?format= value |
Output |
|---|---|---|
application/json |
ror |
ROR native JSON |
application/vnd.commonmeta+json |
commonmeta |
commonmeta JSON |
# ROR native JSON
# commonmeta JSON
# ?format= override
ORCID (people)
Accept header |
?format= value |
Output |
|---|---|---|
application/vnd.orcid+json |
orcid |
commonmeta JSON (native ORCID JSON planned) |
application/json |
orcid |
commonmeta JSON (native ORCID JSON planned) |
application/vnd.commonmeta+json |
commonmeta |
commonmeta JSON |
# ORCID JSON (currently returns commonmeta JSON; native ORCID JSON planned)
# commonmeta JSON
# ?format= override
Usage
Content negotiation
Send an Accept header to /{path} to receive metadata for a DOI without a browser redirect.
BibTeX
RIS
CSL (Citeproc) JSON
Crossref
Crossref XML
DataCite
Schema.org JSON-LD
InvenioRDM
Formatted citation
text/x-bibliography accepts optional style= and locale= parameters. Style names come from the CSL style repository; locale codes from the CSL locales repository.
# APA (default)
# Vancouver in French
Query parameter overrides
Use ?format= instead of an Accept header:
Format-specific URL prefixes
Two URL prefixes return a fixed format without requiring an Accept header or ?format= parameter:
| Prefix | Output format | Content-Type |
|---|---|---|
/dois/{doi} |
DataCite JSON | application/vnd.datacite.datacite+json |
/works/{doi} |
Crossref JSON | application/vnd.crossref+json |
These routes are equivalent to /{doi}?format=datacite and /{doi}?format=crossref respectively. The ?source= override is still accepted if you need to force a specific registration agency.
Supported formats
DOI formats (works)
Accept header |
?format= value |
Notes |
|---|---|---|
application/x-bibtex |
bibtex |
|
text/x-bibliography |
citation |
style= and locale= params |
application/vnd.commonmeta+json |
commonmeta |
|
application/vnd.crossref+json |
crossref |
|
application/vnd.crossref.unixref+xml |
crossref_xml |
|
application/vnd.crossref.unixsd+xml |
crossref_xml |
alias |
application/vnd.citationstyles.csl+json |
csl |
|
application/vnd.datacite.datacite+json |
datacite |
|
application/vnd.datacite.datacite+xml |
datacite_xml |
|
application/vnd.inveniordm.v1+json |
inveniordm |
|
application/x-research-info-systems |
ris |
|
application/vnd.schemaorg.ld+json |
schemaorg |
|
text/html / (absent) at /{path} |
— | Svelte entity page |
text/html / (absent) at /api/handles/{path} |
— | 307 redirect to URL of record |
ROR formats (organisations) — /api/handles/{id} only
Accept header |
?format= value |
Notes |
|---|---|---|
application/json |
ror |
ROR native JSON |
application/vnd.commonmeta+json |
commonmeta |
ORCID formats (people) — /api/handles/{id} only
Accept header |
?format= value |
Notes |
|---|---|---|
application/vnd.orcid+json |
orcid |
commonmeta JSON (native ORCID JSON planned) |
application/json |
orcid |
commonmeta JSON (native ORCID JSON planned) |
application/vnd.commonmeta+json |
commonmeta |
HTTP status codes
| Code | Meaning |
|---|---|
| 200 | Metadata returned |
| 307 | Redirect (entity page, URL of record, or search) |
| 404 | PID not found or path not recognised |
| 406 | Requested content type not supported |
| 502 | Upstream API error |
Deployment (macOS)
Installation via Homebrew
dragoman can be installed from the front-matter Homebrew tap:
This builds dragoman from source (requires Rust, installed automatically as a build dependency) and places the binary at $(brew --prefix)/bin/dragoman.
Place the database
The platform default path on macOS is ~/Library/Application Support/commonmeta/commonmeta.sqlite3. Place the database there and no further configuration is needed:
To use a different path, set COMMONMETA_DB in the launchd plist or pass --db on the command line.
Run as a background service (launchd)
# Start at login and keep alive
# Check status
# View logs
# Stop the service
brew services start installs a launchd plist in ~/Library/LaunchAgents/ and starts the service immediately. It restarts automatically on crash and at login.
To run as a system-level daemon (starts at boot, not tied to a user login), use sudo brew services start dragoman. This installs the plist in /Library/LaunchDaemons/ instead.
Configuration
To change the port or other settings, edit the service environment variables and restart:
# Open the generated plist for editing
Manual installation (without Homebrew)
# Install Rust if not already installed
|
Intel Macs: replace
/opt/homebrewwith/usr/localin all paths below.
Run as a launchd daemon
The bundled com.front-matter.dragoman.plist targets Apple Silicon paths.
Check logs:
Stop and disable:
Updating
With Homebrew
Manual
Deployment (Debian / systemd)
This section covers running dragoman as a persistent system service on a Debian 13 server.
1. Build the binary
On the server, install Rust and install the binary:
|
Or cross-compile locally and copy the binary:
# macOS → Linux x86-64 (requires cross)
2. Create system user and directories
3. Place the SQLite database
The platform default path on Linux is /var/lib/commonmeta/commonmeta.sqlite3. Place the database there and no further configuration is needed:
To use a different path, set COMMONMETA_DB in the environment file instead.
4. Create the environment file
Copy the bundled example and edit it for your deployment:
For a production server with Let's Encrypt, at minimum set:
DRAGOMAN_DOMAIN=commonmeta.org
DRAGOMAN_EMAIL=admin@example.org
See Environment variables for the full reference.
5. Install and enable the systemd unit
dragoman.service is included in the source repository but not installed by cargo install. Download it directly:
Or, if you have the source checked out locally:
Check the service is running:
Updating the binary
Updating the database
The database file can be replaced while the service is running without restarting. dragoman opens a new read-only SQLite connection per request, so an atomic rename takes effect immediately for new requests while in-flight requests finish against the old file.
Copy the new file to a staging path on the same filesystem, then rename it over the live file:
If the source database uses WAL mode (has accompanying -wal/-shm files), produce a clean single-file snapshot first with VACUUM INTO before copying:
Reverse proxy
When running behind a reverse proxy, do not set --domain. Let the proxy handle TLS termination; dragoman listens on plain HTTP at 127.0.0.1:3456 (or whatever --port you set).
Telemetry
Logs — VictoriaLogs
Set --viktorialogs-url (or DRAGOMAN_VIKTORIALOGS_URL) to the VictoriaLogs OTLP/HTTP endpoint. All tracing events are shipped in batches; the exporter is flushed on graceful shutdown.
Without the flag, logs are written to stdout only (the default).
Metrics — VictoriaMetrics / Grafana
Set --metrics-allow (or DRAGOMAN_METRICS_ALLOW) to a comma-separated list of IP addresses permitted to scrape the /api/metrics endpoint. The route is only registered when this flag is set; all other callers receive 403 Forbidden.
# curl http://localhost:3456/api/metrics # works from 127.0.0.1
The /api/metrics endpoint is served on the same port as the main application. The client IP is resolved from the X-Real-IP header first, then the first address in X-Forwarded-For, making it compatible with nginx, Caddy, and Traefik.
Metrics recorded:
| Metric | Labels | Description |
|---|---|---|
http_requests_total |
method, route, status |
Request count by route and HTTP status code |
http_request_duration_seconds |
method, route |
Request latency histogram |
Routes are normalised to templates (/dois/:doi, /works/:doi, /orcid/:id, /{pid}) to keep cardinality low.
License
MIT