dragoman 0.4.11

Server for scholarly metadata in commonmeta format stored in a SQLite database.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
# dragoman

A web server for scholarly metadata with full [DOI Resolution](https://www.doi.org/the-identifier/resources/factsheets/doi-resolution-documentation) and [DOI content negotiation](https://citation.doi.org/docs.html). 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]https://rustup.rs)

### Install

```bash
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](https://commonmeta.org) 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](https://github.com/front-matter/commonmeta-rs) CLI.

## Running the server

### Start

```bash
# 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]https://github.com/FiloSottile/mkcert. |
| any public hostname | HTTPS on `0.0.0.0:<tls-port>` with automatic certificate provisioning via [Let's Encrypt]https://letsencrypt.org. Requires `--email`. |

#### Local development (mkcert)

Install mkcert once and add its CA to your trust store:

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

Then start dragoman with `--domain localhost`:

```bash
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](https://datatracker.ietf.org/doc/html/rfc8737). No separate HTTP challenge server or reverse proxy is needed.

```bash
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:

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

Options can also be supplied as environment variables (flags take precedence):

```bash
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:

```bash
# 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:

```text
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:

```text
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

```bash
# 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

```text
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`](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`]https://docs.rs/tracing-subscriber). Use `dragoman=debug` for per-request cache hits. |

## DOI Resolution

dragoman implements the [Handle System resolution protocol](https://www.doi.org/the-identifier/resources/factsheets/doi-resolution-documentation). 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](#content-negotiation)).

```bash
# 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:

```bash
# 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}` |

```bash
# 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:

```bash
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

```bash
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](#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 |

```bash
# 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 |

```bash
# 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

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

#### RIS

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

#### CSL (Citeproc) JSON

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

#### Crossref

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

#### Crossref XML

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

#### DataCite

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

#### Schema.org JSON-LD

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

#### InvenioRDM

```bash
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](https://github.com/citation-style-language/styles); locale codes from the [CSL locales repository](https://github.com/citation-style-language/locales).

```bash
# 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:

```bash
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` |

```bash
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](https://github.com/front-matter/homebrew-tap):

```bash
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:

```bash
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)

```bash
# 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:

```bash
# Open the generated plist for editing
open "$(brew --prefix)/opt/dragoman/homebrew.mxcl.dragoman.plist"
brew services restart dragoman
```

### Manual installation (without Homebrew)

```bash
# 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.

```bash
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:

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

Stop and disable:

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

### Updating

#### With Homebrew

```bash
brew upgrade dragoman
brew services restart dragoman
```

#### Manual

```bash
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:

```bash
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:

```bash
# 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

```bash
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:

```bash
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:

```bash
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:

```bash
DRAGOMAN_DOMAIN=commonmeta.org
DRAGOMAN_EMAIL=admin@example.org
```

See [Environment variables](#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:

```bash
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:

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

Check the service is running:

```bash
sudo systemctl status dragoman
sudo journalctl -u dragoman -f
```

### Updating the binary

```bash
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:

```bash
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:

```bash
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](https://docs.victoriametrics.com/victorialogs/data-ingestion/opentelemetry/). All `tracing` events are shipped in batches; the exporter is flushed on graceful shutdown.

```bash
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`.

```bash
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.

## Keyboard shortcuts

| Shortcut | Action |
| --- | --- |
| `⌘K` / `Ctrl-K` | Focus search bar |
| `⌘Y` / `Ctrl-Y` | Toggle History & Bookmarks panel (History tab) |
| `⌘B` / `Ctrl-B` | Toggle History & Bookmarks panel (Bookmarks tab) |
| `Escape` | Close panel or cancel search |
| `` / `` | Navigate search suggestions |
| `Enter` | Navigate to selected search result |

## License

MIT