dbnav
v0.1.0
Published
Unofficial Deutsche Bahn journey search CLI and TUI built on db-vendo-client. Deterministic JSON/NDJSON output for AI agents, rich local filters and sorting for humans.
Downloads
27
Maintainers
Readme
dbnav
Unofficial Deutsche Bahn journey search as a CLI and terminal UI, built on
db-vendo-client (dbnav profile).
It exposes more search, filter, and sort control than DB Navigator and is designed to be
reliable for AI agents: deterministic, versioned JSON/NDJSON output, strict schemas,
documented exit codes, and zero prompts in non-interactive use.
⚠️ Unofficial project — not affiliated with Deutsche Bahn
This is an independent, community-built tool. It is not endorsed by, supported by, or affiliated with Deutsche Bahn AG in any way, and it uses an undocumented API that can change or break at any time without notice.
- Prices shown with a BahnCard are local estimates, never binding quotes — always verify on bahn.de before booking.
- Use it responsibly: keep search windows small, leave the default retry/backoff and caching behavior on, and set a contact user agent (
DB_VENDO_USER_AGENT) so operators can reach you.- "DB", "Deutsche Bahn", and "DB Navigator" are trademarks of Deutsche Bahn AG and are used here only to describe compatibility.
Install & build
Requires Node.js 22+.
npx dbnav --help # run without installing (once published to npm)
npm install -g dbnav # or install globallyFrom a checkout:
npm install
npm run build # compiles to dist/, bin/dbnav.js is the executable
npm link # optional: makes `dbnav` available on PATHDevelopment: npm run dev -- journeys --help (tsx, no build needed).
Commands
dbnav open the TUI (only on an interactive terminal)
dbnav tui open the TUI explicitly
dbnav locations <query> resolve station names to stable IDs
dbnav journeys [options] search journeys in a datetime window
dbnav journey <token> refresh one journey by refresh token
dbnav schema [part] print JSON Schemas (request | response | stream)
dbnav config show|path inspect the config file
dbnav completion <shell> bash | zsh | fish | powershell completion scriptGlobal flags: --json (single JSON document; default when stdout is not a TTY),
--stream (NDJSON), --pretty (non-streaming JSON only), --table (force human table),
--debug (diagnostics on stderr), --no-config (ignore the user config file).
Quick start
# 1. Find station IDs (stable across searches):
dbnav locations "Frankfurt Hbf" --json
# 2. Search a window, sorted by effective price then duration:
dbnav journeys --json \
--from 8000105 --to 8000191 \
--start 2026-08-01T08:00 --end 2026-08-01T18:00 \
--bahncard bahncard50 --max-price 60 \
--sort price:asc,duration:asc,transfers:asc --limit 10
# 3. Stream results page-by-page as NDJSON:
dbnav journeys --stream --from 8000105 --to 8000191 --start 2026-08-01T08:00
# 4. Inspect what would be sent without contacting DB:
dbnav journeys --explain --from 8000105 --to 8000191 --start 2026-08-01T08:00Humans on a TTY get a table by default and may use relative datetimes
(--start "tomorrow 08:00"). Machine mode (--json/--stream/no TTY) accepts
ISO 8601 only and never prompts.
Journey options (selection)
Every TUI setting has a CLI flag. See dbnav journeys --help for the full list.
- Route & window:
--from/--to/--via(IDs or unambiguous names;--pick-firstresolves ambiguity deterministically),--start/--end(window may cross midnight or span up to 7 days;--allow-long-windowoverrides with a rate-limit warning),--time-mode departure|arrival,--timezone(defaultEurope/Berlin). - DB request:
--products ice,ic,re,rb,sbahn,ubahn,tram,bus,ferryor presets (all,rail,long-distance,regional,local),--max-transfers,--min-transfer-minutes,--first-class,--passengers,--age-group(baby 0-5, child 6-14, young 15-26, adult 27-64, senior 65+),--bahncard none|bahncard25|bahncard50+--bahncard-class 1|2,--bike,--accessibility none|partial|complete,--walking-speed,--no-start-with-walking,--include-slower,--best-price,--d-ticket-only,--d-ticket-discount,--stopovers,--no-remarks,--results-per-page. - Budget & transport:
--timeout <ms>,--max-pages <n>,--retries <n>(exponential backoff with jitter),--cache default|bypass|refresh(2-minute TTL). - Local filters (applied after fetching, never sent upstream):
--max-price,--min-price,--price-basis effective|base,--require-price,--max-duration 2h/--min-duration 45m,--filter-max-transfers,--direct,--depart-after/--depart-before/--arrive-after/--arrive-before(ISO sub-windows),--include-product/--exclude-product,--include-category/--exclude-category,--include-line/--exclude-line,--include-operator/--exclude-operator,--no-cancelled,--max-departure-delay,--max-arrival-delay,--min-transfer-buffer(+--reject-risky-transfersfor unknown buffers),--require-walking/--avoid-walking,--max-walking 15m,--require-bike-info,--require-d-ticket,--via-preferred,--via-avoided,--no-dedupe. - Sorting & selection:
--sort price:asc,duration:asc,transfers:asc(stable multi-key; keys:departure, arrival, price, duration, transfers, delay, walking, reliability, score; missing values always sort last),--score-weights price=1,duration=1,transfers=0.5,delay=0.5,walking=0.25(weighted min-max score, lower is better, missing values count as worst case),--pareto(drop journeys strictly dominated on price+duration+transfers; journeys with missing values are never removed),--limit.
Configuration & precedence
Config file (strict JSON, unknown keys rejected):
- Windows:
%APPDATA%\dbnav\config.json - macOS:
~/Library/Application Support/dbnav/config.json - Linux:
$XDG_CONFIG_HOME/dbnav/config.json - Override the path with
DBNAV_CONFIG.
{
"defaults": { "bahncard": "bahncard25", "filters": { "requirePrice": true } },
"presets": {
"commute": { "from": "8000105", "to": "8000244", "products": { "nationalExpress": false } }
}
}Use presets/saved searches with dbnav journeys --preset commute …. Load a full request
with --input request.json or --input - (stdin JSON).
Precedence (highest wins): CLI flags → --input JSON → environment → preset →
config defaults → built-in defaults. Nested filters/products/scoreWeights merge
per key. --explain shows the resolved request and the source of every value.
Environment variables
DB_VENDO_USER_AGENT honest contact-style UA passed to db-vendo-client
DB_NAVIGATOR_USER_AGENT transport-workaround UA (default DBNavigator/Android/25.18.2)
DB_NAVIGATOR_APP_VERSION transport-workaround app version (default 25.18.2)
DBNAV_TIMEZONE default timezone (default Europe/Berlin)
DBNAV_CONFIG explicit config file path
DBNAV_LIVE_TESTS=1 enable opt-in live smoke testsSee .env.example. No personal data is committed or emitted in output.
Output contract
schemaVersion: "1.0" is versioned independently of the package. Print the machine
schemas with dbnav schema request|response|stream.
- Datetimes are ISO 8601 with offsets; money is numeric major units + ISO currency code;
JSON field names never vary with locale;
missing,null,0, andfalseare distinct; results embed the effective request and app version. - Success envelope:
{ schemaVersion, ok: true, command, appVersion, query, range, journeys, stats, warnings }wherestats={ pages, fetched, deduplicated, inRange, returned, filtered: {reason: count}, cacheHit, elapsedMs, complete, truncatedReason }. - Each journey: stable fingerprint
id,refreshToken, planned/realtime departure/arrival, delays,durationMinutes,transfers,minTransferBufferMinutes, walking info, cancellation flags,loadFactor,dTicketSuitable,fare(see below),legs(with IDs, times, line/product/operator, walking, cancellation, delay, stopovers on request),remarks,officialDbSearchUrl,dataWarnings. - NDJSON (
--stream): one compact object per line, each withschemaVersion,type, monotonesequence. Event types:meta,journey(default framing),page(--stream-events page),progress,done(exactly once on success),error(only after a stream already began; before that, errors are a plain JSON error envelope on stdout). Diagnostics always go to stderr; stdout stays pure.
Fares and BahnCard estimates
DB's API often returns prices without the requested BahnCard reduction. dbnav keeps
the upstream amount as fare.basePrice and computes a local fare.effectivePrice:
BahnCard 25 → 25% off; BahnCard 50 → 50% off Flexpreis/flexible fares, 25% off saver
fares; rounded to 2 decimals. Estimates carry discountSource:
"bahncard-local-estimate", discountRate, priceConfidence: "estimated", and an
explanation. No price is ever invented when upstream returns none. Price filters and
sorting use the effective price by default (--price-basis base to switch);
max-price bounds are widened internally so journeys qualifying only after the local
discount are not lost.
Exit codes
0 success 4 upstream/network failure (after retries)
1 internal error 5 no matches
2 validation/usage error 6 partial/truncated success (see stats.truncatedReason)
3 ambiguous/unknown location 130 cancelled (Ctrl+C)Ambiguous locations (exit 3) return error.details.candidates so agents can pick an ID
and retry. Broken pipes (| head) end the stream gracefully.
Time-window pagination
db-vendo-client returns pages, not ranges. dbnav pages until the requested boundary is
crossed (based on normalized timestamps), the cursor is exhausted, or a budget
(--max-pages, --timeout, Ctrl+C) stops it — in which case stats.complete: false
and stats.truncatedReason say why. Departure mode pages forward via laterThan;
arrival mode pages backward via earlierThan; the first request carries exactly one of
departure/arrival and cursor requests never carry a datetime (vendo enforces this).
Pages are deduplicated by refresh token with a deterministic fingerprint fallback.
DB transport workaround
The DB backend sometimes rejects generic clients (HTTP 403) or negotiates HTTP/2 in a
way that breaks Node's fetch stack. src/db/dbnav-profile.ts wraps the dbnav profile:
HTTP/1.1 pinned via ALPN with keep-alive, DB Navigator-like Android headers with a
per-request Instana UUID, randomized user agents disabled, and the base profile's
transformReq preserved. App version/device values are configurable via environment
variables; the honest project user agent is passed separately to createClient.
TUI
dbnav tui — fully keyboard-driven (press ? for help):
- From/To/Via with debounced live suggestions (offline fallback) and explicit ID-visible selection; start/end editors supporting cross-day windows; every DB option and the common local filters in organized sections.
- Results stream in page by page with progress, hidden-by-filter count, and truncation
state. Sorting (
s/S), Pareto (p), and filter toggles re-rank instantly without refetching. Enter opens a detail view (legs, delays, remarks, fare explanation, refresh token, bahn.de handoff URL). - Clear loading/empty/error/cancelled/partial states, responsive narrow-terminal layout, no mouse required; the terminal is restored on exit, error, and Ctrl+C.
Official DB handoff
Each journey includes officialDbSearchUrl, a search handoff to int.bahn.de
pre-filled with origin, destination, time, class, and products. It is not a booking
deep link and may not show the exact itinerary first. refreshToken works with
dbnav journey <token> for realtime refresh via db-vendo-client; it is not a public
DB Navigator share URL.
AI agents
A ready-to-use agent skill lives at skills/dbnav-cli/SKILL.md
(non-interactive workflow, schemas, exit-code handling, streaming, caveats). Its
accuracy is enforced by tests/skill.test.ts, and it ships inside the npm package.
Install the skill (Claude Code / compatible agents)
# per project:
mkdir -p .claude/skills/dbnav-cli
curl -fsSL https://raw.githubusercontent.com/TheReal-Flo/dbnav/main/skills/dbnav-cli/SKILL.md \
-o .claude/skills/dbnav-cli/SKILL.md
# or globally for your user:
mkdir -p ~/.claude/skills/dbnav-cli
curl -fsSL https://raw.githubusercontent.com/TheReal-Flo/dbnav/main/skills/dbnav-cli/SKILL.md \
-o ~/.claude/skills/dbnav-cli/SKILL.mdIf you installed the npm package, the skill is also on disk at
node_modules/dbnav/skills/dbnav-cli/SKILL.md — copy it from there instead of curl:
mkdir -p .claude/skills/dbnav-cli
cp node_modules/dbnav/skills/dbnav-cli/SKILL.md .claude/skills/dbnav-cli/The skill teaches an agent to resolve station IDs first, run non-interactive JSON/NDJSON
searches, interpret exit codes and partial results, and never treat estimated BahnCard
prices as quotes. The dbnav binary itself is invoked via npx dbnav — no global
install required.
Development
npm run typecheck # strict TS
npm run lint # eslint
npm test # vitest unit + mocked integration tests
npm run build # tsc -> dist/
npm run test:live # opt-in live smoke tests (set DBNAV_LIVE_TESTS=1); never in CIThe domain/ and search/ layers are UI-free (no stdout writes, no process.exit)
and dependency-injected (client, clock, sleep, RNG) for deterministic tests.
Limitations
- BahnCard-reduced prices are estimates, not quotes; verify at bahn.de before booking.
- Load factor, bike info, D-Ticket suitability, and delay data appear only when upstream exposes them; filters that depend on them are conservative by default.
- Prices for multi-passenger groups depend on upstream behavior; passenger count is forwarded but per-person pricing is not recalculated locally.
- The vendo API is unofficial and may change without notice.
