totpd is a small Go web daemon that serves current TOTP codes from key files on disk.
Live example: https://anarjafarov.me/totp/example
The daemon scans a keys/ directory at startup, loads each token, and exposes:
GET /totp/<name>: HTML page with the current code and countdown.GET /totp/<name>/status: JSON status for the current code.
totpd does not include authentication. Anyone who can reach a token URL can read that token's current one-time password, so run it behind your own access control, TLS, VPN, allowlist, or trusted reverse proxy.
- Go
1.25.5or compatible, matchinggo.mod. - Key material in
keys/.
Build the daemon:
makeRun it locally:
make runOpen the bundled example token:
http://127.0.0.1:3080/totp/example
The default listen address is 127.0.0.1:3080. Override it with TOTPD_ADDR:
make run TOTPD_ADDR=127.0.0.1:4000Keys live in keys/. Each token name comes from the filename, and names may contain letters, numbers, -, _, and ..
Supported file forms:
keys/<name>.url.txt: a text file containing anotpauth://totp/...URL, anotpauth-migration://...URL, or a raw Base32 secret.keys/<name>.qr.pngkeys/<name>.qr.jpgkeys/<name>.qr.jpeg
Examples included in this repository:
keys/example.url.txt
keys/example.qr.png
For a raw Base32 secret, totpd uses the filename as the label and issuer, with SHA1, 6 digits, and a 30 second period.
Supported TOTP settings:
- Algorithms:
SHA1,SHA256,SHA512 - Digits:
6,7, or8 - Periods:
30or60seconds
On startup, totpd reconciles URL and QR files for each token name:
- If only a QR file exists, it decodes the QR payload and writes
keys/<name>.url.txt. - If both URL and QR files exist and they match, the URL file is used.
- If both exist and differ, the newer source wins.
- Older conflicting files are moved into
keys/deleted/YYYYMMDD/.
Run reconciliation without starting the server:
make syncEquivalent direct command:
TOTPD_DIR=. go run ./cmd -sync-onlytotpd resolves its base directory in this order:
TOTPD_DIR, when set.- The directory containing the
totpdbinary, when running an installed binary namedtotpd. - The current working directory.
Runtime environment variables:
| Variable | Default | Description |
|---|---|---|
TOTPD_ADDR |
127.0.0.1:3080 |
HTTP listen address. |
TOTPD_DIR |
auto-detected | Directory containing the keys/ folder. |
cmd/main.go: CLI entry point.internal/app/: application runtime, HTTP routes, and HTML template.internal/config/: flags, environment variables, and base directory resolution.internal/keys/: key directory scanning, QR decoding, and key file reconciliation.internal/otp/: OTP parsing, migration payload handling, and TOTP generation.test/: Docker-run spec tests for externally visible behavior.
GET /totp/<name>/status returns JSON:
{
"name": "example",
"code": "123456",
"valid": true,
"remainingSeconds": 17,
"periodSeconds": 30,
"progress": 0.43333333333333335,
"expiresAt": "2026-08-07T12:00:30Z",
"label": "TOTPApp:example@totpd",
"issuer": "TOTPApp",
"algorithm": "SHA1"
}Responses use Cache-Control: no-store.
Install the binary and keys into /etc/totpd:
make installInstall and enable the systemd unit:
make servicemake install and make service write to /etc paths by default, so run them
with sudo when your shell user cannot write there.
The included totpd.service runs:
WorkingDirectory=/etc/totpd
Environment=TOTPD_ADDR=127.0.0.1:3080
ExecStart=/etc/totpd/totpd
Start or restart the service after installing:
sudo systemctl restart totpd.serviceRun the test suite inside Docker:
make testThe test target uses the golang:1.25.5 image by default and runs go test ./....
example.nginx.conf proxies /totp/ to the local daemon and denies /keys/:
location /totp/ {
proxy_pass http://127.0.0.1:3080;
}
location /keys/ {
return 404;
}Add your own authentication, IP restrictions, or private network boundary in front of /totp/ before exposing real tokens.
Remove build artifacts and local Go caches:
make clean