THE API PORTAL
A model is not just something you materialize — it is a virtual view. The api-portal turns any model into a REST endpoint: flip a switch, choose the columns, hand out a URL instead of database credentials. Every request executes the model's logic live against its sources through the federation engine — decomposed, pushed down to each engine, computed — never a stale read of a materialized table. Change a row in a source system and the API answers differently on the next call, no dvt run required. A model joining PostgreSQL to MySQL federates per request; a single-engine model pushes the whole query down in one hop.
THE DVT APP SUITE
DVT owns ports 46100–46109— deliberately far from every tool that usually squats on a data engineer's machine. One command starts the whole suite:
$ dvt serve ✅ hub http://localhost:46100 # the front door: links, license, running apps ✅ catalog http://localhost:46101 # cross-engine docs & lineage ✅ api-portal http://localhost:46102 # REST exposure over models ✅ chat http://localhost:46104 # ask your data in English, answered live ✅ scheduler http://localhost:46106 # dvt commands on a cron `dvt kill` stops everything; `dvt kill -s <app>` stops one.
Each app also runs standalone (dvt hub, dvt docs serve, dvt api-portal). Everything binds to localhost unless you widen --host deliberately.
EXPOSING A MODEL
The portal lists every model in your project — searchable, with its target, materialization and columns. Each model has an exposure switch and a column panel: untick anything the API must never serve. Access is deny-by-default (a model you never touched is not exposed), and the column allow-list is intersected with the real catalog at request time, so a stale config can never widen access.
# prerequisites, once per project $ dvt compile # manifest $ dvt docs generate # column metadata (the same artifacts the catalog uses)
CONSUMING THE API
# what's exposed
$ curl localhost:46102/api/v1/models
# rows — the model's logic, executed live against its sources
$ curl 'localhost:46102/api/v1/models/orders?limit=5'
# equality filters on any accessible column + pagination
$ curl 'localhost:46102/api/v1/models/orders?region=EMEA&limit=100&offset=200'
{
"model": "orders",
"virtualized": true,
"columns": ["id", "region", "amount"],
"rows": [{"id": 1, "region": "EMEA", "amount": "29.99"}, ...],
"limit": 100, "offset": 200, "row_count": 100
}Filters are exact-match equality; filter columns must be accessible columns — anything else is a 400 (this doubles as the injection guard: identifiers are validated against the catalog and quoted in each engine's own dialect — mixed-case columns and reserved words included — and values are escaped literals). Limit caps at 1000. The model's config panel carries a consumer console: curl / Python / JavaScript / Java snippets and an in-browser try-it runner.
Fresh, not fast. Every call federates for real — Sling extractions from live engines, DuckDB compute — expect one to tens of seconds depending on the sources. That buys always-current answers from any mix of engines; it is not a low-latency serving layer. Models need dvt docs generate once for column metadata.
NAMED ENDPOINTS — MANY APIS OVER ONE MODEL
A model can carry any number of named endpoints, each with its own column allow-list and fixed row filters— WHERE clauses baked into the endpoint that callers can neither see past nor override. The fixed filter may restrict on a column the endpoint doesn't even expose: that's row-level security in one move.
# endpoint "orders-eu": columns [id, amount], fixed filter region=EU $ curl 'localhost:46102/api/v1/endpoints/orders-eu?limit=5' # → only EU rows, region column never leaves the server $ curl 'localhost:46102/api/v1/endpoints/orders-eu?region=US' # → 400 "filter 'region' is fixed by this endpoint"
Endpoints are created from the model's panel in the portal: name it, pick the columns, add the locked filters — done.
API KEYS
The portal generates a dvt_ bearer token — shown exactly once, stored only as a hash — and a switch turns enforcement on:
$ curl -H 'Authorization: Bearer dvt_...' localhost:46102/api/v1/models/orders
Enforcement is off by default for localhost development. The management UI itself carries no auth — it belongs on loopback; widening --host warns loudly.