dragoman 0.4.7

Server for scholarly metadata in commonmeta format stored in a SQLite database.
dragoman-0.4.7 is not a library.

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

Install

cargo install dragoman

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
dragoman start

# Custom port
dragoman start --port 8080

# With a local database
dragoman start --db /data/commonmeta-2026-06-15.sqlite3

# Write a PID file so the process can be stopped later
dragoman start --pid-file /tmp/dragoman.pid

# All options together
dragoman start --port 8080 --db /data/commonmeta-2026-06-15.sqlite3 --pid-file /tmp/dragoman.pid

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:

brew install mkcert   # macOS; see https://github.com/FiloSottile/mkcert for Linux/Windows
mkcert -install

Then start dragoman with --domain localhost:

dragoman start --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.

dragoman start \
  --port 80 \
  --domain commonmeta.org \
  --email admin@example.org
  • Binds HTTPS on --tls-port (default 443) with automatic certificate provisioning.
  • Binds HTTP on --port with a permanent redirect to https://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:

dragoman start \
  --port 80 \
  --domain commonmeta.org \
  --email admin@example.org \
  --acme-staging

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 dragoman start

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
cargo run -- start --db commonmeta-2026-06-15.sqlite3

# Or with a full path
cargo run -- start --db /data/commonmeta-2026-06-15.sqlite3

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)
dragoman stop

# Stop using a custom PID file
dragoman stop --pid-file /var/run/dragoman.pid

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
open http://localhost:3456/10.5281/zenodo.1089100

# Works for ROR organisation IDs and ORCID person IDs too
open http://localhost:3456/027k65916
open http://localhost:3456/0000-0001-5727-2427

Paths that are not recognised PIDs redirect to the search page:

# Non-PID path → 307 to /?q=<path>
curl -v http://localhost:3456/machine-learning

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
curl -L http://localhost:3456/api/handles/10.5281/zenodo.1089100

# Inspect the redirect target without following
curl -s -o /dev/null -w "%{redirect_url}" http://localhost:3456/api/handles/10.5281/zenodo.1089100
# https://zenodo.org/record/1089100

View entity page — ?noredirect

The ?noredirect parameter redirects the browser to /{path} to render the entity page:

curl -v "http://localhost:3456/api/handles/10.5281/zenodo.1089100?noredirect"
# 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
curl -H "Accept: application/x-bibtex" \
     http://localhost:3456/api/handles/10.5281/zenodo.1089100

curl -H "Accept: application/vnd.commonmeta+json" \
     http://localhost:3456/api/handles/10.5281/zenodo.1089100

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
curl -H "Accept: application/json" \
     http://localhost:3456/api/handles/027k65916

# commonmeta JSON
curl -H "Accept: application/vnd.commonmeta+json" \
     http://localhost:3456/api/handles/027k65916

# ?format= override
curl "http://localhost:3456/api/handles/027k65916?format=ror"
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)
curl -H "Accept: application/vnd.orcid+json" \
     http://localhost:3456/api/handles/0000-0001-5727-2427

# commonmeta JSON
curl -H "Accept: application/vnd.commonmeta+json" \
     http://localhost:3456/api/handles/0000-0001-5727-2427

# ?format= override
curl "http://localhost:3456/api/handles/0000-0001-5727-2427?format=orcid"

Usage

Content negotiation

Send an Accept header to /{path} to receive metadata for a DOI without a browser redirect.

BibTeX

curl -H "Accept: application/x-bibtex" \
     http://localhost:3456/10.5281/zenodo.1089100

RIS

curl -H "Accept: application/x-research-info-systems" \
     http://localhost:3456/10.5281/zenodo.1089100

CSL (Citeproc) JSON

curl -H "Accept: application/vnd.citationstyles.csl+json" \
     http://localhost:3456/10.5281/zenodo.1089100

Crossref

curl -H "Accept: application/vnd.crossref+json" \
     http://localhost:3456/10.1016/j.jaci.2019.09.015

Crossref XML

curl -H "Accept: application/vnd.crossref.unixref+xml" \
     http://localhost:3456/10.1016/j.jaci.2019.09.015

DataCite

curl -H "Accept: application/vnd.datacite.datacite+json" \
     http://localhost:3456/10.5281/zenodo.1089100

Schema.org JSON-LD

curl -H "Accept: application/vnd.schemaorg.ld+json" \
     http://localhost:3456/10.5281/zenodo.1089100

InvenioRDM

curl -H "Accept: application/vnd.inveniordm.v1+json" \
     http://localhost:3456/10.5281/zenodo.1089100

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)
curl -H "Accept: text/x-bibliography; style=apa" \
     http://localhost:3456/10.5281/zenodo.1089100

# Vancouver in French
curl -H "Accept: text/x-bibliography; style=vancouver; locale=fr-FR" \
     http://localhost:3456/10.5281/zenodo.1089100

Query parameter overrides

Use ?format= instead of an Accept header:

curl "http://localhost:3456/10.5281/zenodo.1089100?format=bibtex"
curl "http://localhost:3456/10.5281/zenodo.1089100?format=citation&style=apa&locale=de-DE"

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
curl http://localhost:3456/dois/10.1371/journal.pcbi.1000204
curl http://localhost:3456/works/10.1371/journal.pcbi.1000204

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:

brew tap front-matter/tap
brew install dragoman

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:

mkdir -p "$HOME/Library/Application Support/commonmeta"
cp commonmeta.sqlite3 "$HOME/Library/Application Support/commonmeta/commonmeta.sqlite3"

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
brew services start dragoman

# Check status
brew services info dragoman

# View logs
tail -f "$(brew --prefix)/var/log/dragoman.log"

# Stop the service
brew services stop dragoman

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
open "$(brew --prefix)/opt/dragoman/homebrew.mxcl.dragoman.plist"
brew services restart dragoman

Manual installation (without Homebrew)

# Install Rust if not already installed
curl https://sh.rustup.rs -sSf | sh

cargo install dragoman
sudo install -m 755 ~/.cargo/bin/dragoman /opt/homebrew/bin/dragoman

Intel Macs: replace /opt/homebrew with /usr/local in all paths below.

Run as a launchd daemon

The bundled com.front-matter.dragoman.plist targets Apple Silicon paths.

sudo mkdir -p /opt/homebrew/var/dragoman /opt/homebrew/var/log
sudo cp commonmeta.sqlite3 /opt/homebrew/var/dragoman/commonmeta.sqlite3

sudo cp com.front-matter.dragoman.plist /Library/LaunchDaemons/
sudo launchctl load -w /Library/LaunchDaemons/com.front-matter.dragoman.plist

Check logs:

tail -f /opt/homebrew/var/log/dragoman.log

Stop and disable:

sudo launchctl unload -w /Library/LaunchDaemons/com.front-matter.dragoman.plist

Updating

With Homebrew

brew upgrade dragoman
brew services restart dragoman

Manual

cargo install dragoman
sudo install -m 755 ~/.cargo/bin/dragoman /opt/homebrew/bin/dragoman
sudo launchctl kickstart -k system/com.front-matter.dragoman

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:

curl https://sh.rustup.rs -sSf | sh
source ~/.cargo/env
cargo install dragoman
sudo install -m 755 ~/.cargo/bin/dragoman /usr/local/bin/dragoman

Or cross-compile locally and copy the binary:

# macOS → Linux x86-64 (requires cross)
cargo install cross
cross build --release --target x86_64-unknown-linux-gnu
scp target/x86_64-unknown-linux-gnu/release/dragoman user@server:/tmp/dragoman
ssh user@server 'sudo install -m 755 /tmp/dragoman /usr/local/bin/dragoman'

2. Create system user and directories

sudo useradd --system --no-create-home --shell /usr/sbin/nologin dragoman
sudo mkdir -p /var/lib/dragoman /var/lib/commonmeta /etc/dragoman
sudo chown dragoman:dragoman /var/lib/dragoman /var/lib/commonmeta

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:

sudo cp commonmeta.sqlite3 /var/lib/commonmeta/commonmeta.sqlite3
sudo chown dragoman:dragoman /var/lib/commonmeta/commonmeta.sqlite3

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:

sudo cp env.example /etc/dragoman/env
sudo chmod 640 /etc/dragoman/env
sudo chown root:dragoman /etc/dragoman/env
sudo nano /etc/dragoman/env   # uncomment and fill in the variables you need

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:

sudo curl -fsSL https://codeberg.org/front-matter/dragoman/raw/branch/main/dragoman.service \
  -o /etc/systemd/system/dragoman.service
sudo systemctl daemon-reload
sudo systemctl enable --now dragoman

Or, if you have the source checked out locally:

sudo cp dragoman.service /etc/systemd/system/dragoman.service
sudo systemctl daemon-reload
sudo systemctl enable --now dragoman

Check the service is running:

sudo systemctl status dragoman
sudo journalctl -u dragoman -f

Updating the binary

cargo install dragoman
sudo install -m 755 ~/.cargo/bin/dragoman /usr/local/bin/dragoman
sudo systemctl restart dragoman

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:

sudo cp commonmeta-new.sqlite3 /var/lib/commonmeta/commonmeta_new.sqlite3
sudo chown dragoman:dragoman /var/lib/commonmeta/commonmeta_new.sqlite3
sudo mv /var/lib/commonmeta/commonmeta_new.sqlite3 /var/lib/commonmeta/commonmeta.sqlite3

If the source database uses WAL mode (has accompanying -wal/-shm files), produce a clean single-file snapshot first with VACUUM INTO before copying:

sqlite3 /path/to/source.sqlite3 "VACUUM INTO '/tmp/commonmeta_clean.sqlite3'"

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.

dragoman start \
  --viktorialogs-url http://localhost:9428/insert/opentelemetry/v1/logs

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.

dragoman start --metrics-allow 127.0.0.1,10.0.0.5
# 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