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.