DVT DOCS

dvt docs generatebuilds your project's documentation catalog — the command you already know, invoked exactly as you've always invoked it. What's different is what ends up inside: DVT's catalog carries column metadata from every connection in your profile, collected natively from each engine, not just from the one warehouse your default target points at.

GENERATE IT

From inside your project:

$ dvt docs generate

14:32:09  DVT catalog: 38 relations cataloged across 4 connections, 51 nodes engine-stamped, dvt_manifest.json written
14:32:09  Serve it with `dvt docs serve` (this catalog) or `dvt serve` (the suite, then open /catalog)

The compile logs you're used to stream first, then DVT's summary names the three things that just happened: how many relations got real column metadata, how many connections answered for them, and how many nodes were stamped with their engine so the lineage graph can color them. Selection flags you already use — --select, --exclude — are forwarded unchanged and work as you know them.

WHAT GENERATE ACTUALLY DOES

Four steps, in order. First the docs scaffolding is compiled — manifest and docs site, exactly as you know it (like every graph-bearing verb, it begins from a passing dvt parse). Then DVT takes over:

It writes DVT's own manifest, beside dbt's. target/dvt_manifest.jsonis dbt's manifest plus what only DVT knows: every model, seed, snapshot and source gets a dvt_adapter_type; sources bound via config.meta.connectioncarry their connection name and the database they really live in (dbt fills a source's database from the default target when sources.ymldeclares none). That's what lets the catalog's lineage graph color nodes by engine and a source page name its real database. dbt's own target/manifest.jsonis left byte-for-byte as dbt wrote it — no DVT command writes into it — so every dbt tool that reads it (VS Code extensions, lineage viewers, CI checks) keeps seeing plain dbt. The served catalog reads DVT's view of the current manifest, so it stays right after every dvt run.

It collects column metadata from every connection, natively. Sources read from the engine their config.meta.connection points at; models materialized on a non-default target read from thattarget's engine. Collection is batched — one metadata query per connection, covering every schema it carries, never per schema or per table — so a connection carrying twenty schemas and five thousand tables costs one round trip, not twenty. The schema names in that query are spelled the way the engine stores them (Oracle and Snowflake upper-case, Databricks lower-case) so the engine's own catalog index serves it.

It leaves dbt's docs site alone. target/index.html stays the stock dbt UI and target/manifest.json stays dbt's manifest, so dbt docs servekeeps working exactly as before. DVT's themed catalog — dark, engine-aware, built to show a federated graph — is served from the package by dvt docs serveand by the suite, never written over dbt's files.

IN PLAIN DBT

dbt docs generatecatalogs exactly one warehouse — the catalog step queries the default adapter only, and a manifest that reaches into a second database is refused outright ("Cross-db references not allowed"). DVT builds the whole catalog itself instead: it asks every connection for its own metadata through that engine's native driver and assembles one catalog spanning all of them — Postgres beside Snowflake beside DuckDB, in the same lineage graph, colored by engine.

WHEN A CONNECTION CAN'T ANSWER — LOUD GAPS

A catalog that spans every connection has to decide what happens when one of them is down. DVT's answer: the catalog still builds, the gap is named, and nothing hangs. An unreachable engine gets a warning that says exactly what you'll be missing:

dvt docs: could not connect to snowflake — its relations will have NO
columns in the catalog: 250001: Could not connect to Snowflake backend
after 3 attempt(s)

Slow is handled the same way as down. A connection is one metadata query, and it gets the larger of the query timeout (60 seconds) and the connection budget (120 seconds) — a connection that accepts the socket and then never answers is abandoned, with the reason stated:

dvt docs: connection 'snowflake_prod' (snowflake) did not answer within
60s — abandoned; its relations will have NO columns in the catalog.
Raise DVT_DOCS_CONNECTION_BUDGET / DVT_DOCS_QUERY_TIMEOUT if this
engine is simply slow.

And an incomplete catalog says so in the summary itself — on stdout, not buried in a log:

14:32:09  DVT catalog: 29 relations cataloged across 3 connections, 51 nodes engine-stamped, dvt_manifest.json written
14:32:09  1 connection(s) timed out and were abandoned (snowflake_prod) — their relations have no columns in this catalog
14:32:09  Serve it with `dvt serve` (then open /catalog)

The doctrine behind it: a catalog missing one engine beats a command that never returns, and incompleteness that announces itself is fine — silence is the sin. If an engine is genuinely slow rather than gone, raise the two environment knobs in the reference below and regenerate.

SERVING IT — THREE SURFACES, ONE RULE

dbt docs servehosts dbt's own catalog, exactly as it always has: the stock UI over target/manifest.json and target/catalog.json. DVT never writes over those files, so every dbt tool keeps seeing plain dbt.

dvt docs servehosts DVT's catalog and nothing else — the themed one, with the engine under every node of the graph, the target and engine of every model that lands on a custom target, and the source, database, schema, table and connection of every source. Foreground, port 8080 like dbt's, Ctrl+C stops it (so does dvt kill):

$ dvt docs serve
✅ dvt docs serve · 'analytics' · http://localhost:8080
   DVT's catalog, live: refresh after any run or parse. Ctrl+C (or `dvt kill`) stops it.

dvt serve hosts the whole suite, and its /catalog is the same DVT catalog — one server, two fronts:

$ dvt serve

✅ DVT suite · 'analytics' · http://localhost:46100
   http://localhost:46100/             hub + Settings
   http://localhost:46100/catalog      catalog
   http://localhost:46100/portal       api-portal
   http://localhost:46100/chat         Chat with Martin
   http://localhost:46100/profiling    profiling
   http://localhost:46100/scheduler    scheduler
   http://localhost:46100/explorer     explorer
   http://localhost:46100/dashboards   dashboards
   http://localhost:46100/executions   executions monitor

Always up until `dvt kill`. Apps with no artifacts serve their empty
state and pick up compiles on refresh.

Both DVT fronts are live, the way dbt's catalog is: the manifest they serve is DVT's view of dbt's current target/manifest.json, built on request, so a page refresh after any dvt run or dvt parse shows the new graph with its engine stamps — no restart, no regenerate. Column metadata is the one thing only dvt docs generate refreshes (as with dbt); before the first generate the catalog serves without columns rather than a broken page, and target/dvt_manifest.jsonfollows dbt's manifest after every graph verb.

Like every DVT operation verb, dvt docs generatealso journals an execution record to the project's ledger — so the run, its outcome and its timing show up in the executions monitor at /executions alongside your runs and builds.

REFERENCE — EVERY FLAG

dvt docs generate:

FLAGDEFAULTWHAT IT DOES
--no-compileoffBuild the catalog without recompiling first — reuse the existing manifest.
--staticoffEmit a single self-contained HTML file you can hand around without a server.
--project-dir.The project to catalog. Manifest, catalog and themed UI all land in its target/ directory.
--profiles-dir(profiles.yml's usual home)Read connections from a non-default profiles.yml location — the same connections metadata collection walks.

Everything else — --select, --exclude, the selection surface you already know — is forwarded unchanged. Two environment variables bound the collection phase:

ENVIRONMENT VARIABLEDEFAULTWHAT IT DOES
DVT_DOCS_QUERY_TIMEOUT60Seconds one metadata query may take. One query covers a whole connection — every schema it carries — so this is generous headroom, not a per-table clock.
DVT_DOCS_CONNECTION_BUDGET120Wall-clock seconds one connection may cost the command. A connection is one query, so it waits max(budget, query timeout); raising either lengthens the wait.