Connections
A connection is a document-scoped data source. Items bind to a connection by id and carry their own query. In this effort connections are declared and validated only — never dialed: there is no live fetch, no network request, no real data. The point is to validate the model — that the wiring is well-formed and the shapes line up.
Declaring connections
Connections live in the top-level connections array. Each instance has the
shape { id, $ref, config?, secretRefs? }:
{
"id": "metrics-api",
"$ref": "https://lattice.dev/schemas/connections/http/1.0.0",
"config": { "url": "https://api.example.com/metrics", "method": "GET" },
"secretRefs": { "token": "vault://lattice/metrics-api#token" }
}
| Field | Required | Notes |
|---|---|---|
id | yes | Document-unique identifier; items bind by this id. |
$ref | yes | URI of the connection-type schema (validated against the catalog). |
config | no | Per-connection config; shape defined by the connection type. |
secretRefs | no | Indirection map from a logical secret name to an opaque reference token. Secret values are never inlined. |
The resolver resolves each $ref to a connection-type schema using the same
machinery as item $refs, validates the config against that schema, and rejects
duplicate ids (CONNECTION_DUPLICATE_ID). An unresolvable $ref is
CONNECTION_TYPE_UNRESOLVED; an invalid config is CONNECTION_CONFIG_INVALID.
The two connection types are http (a
query-style endpoint) and static
(inline rows embedded in config).
The direct binding model
An item draws data by naming a connection in its config:
{
"$ref": "https://lattice.dev/schemas/items/table/1.0.0",
"config": {
"connectionId": "metrics-api",
"query": {
"region": { "$var": "region" },
"hours": { "$var": "window" },
"label": "last ${window}h"
}
}
}
connectionIdnames a document-scoped connection. If it matches no declared connection, resolution fails withBINDING_CONNECTION_NOT_FOUND.queryis an arbitrary object passed to the connection. Its parameters may reference variables using the same$var/${}forms as any config — the interpolation pass runs over the whole item config (including the query) before binding, so by the time the binding is lifted onto the resolved node the query carries concrete, typed values, not references.- A
querydeclared without aconnectionIdis malformed (BINDING_INVALID).
In the resolved tree, a bound item gains a binding block:
"binding": {
"connectionId": "metrics-api",
"query": { "region": "us-east", "hours": 24, "label": "last 24h" },
"contract": { ... }
}
Secret handling
Credentials are never stored in a dashboard document or its resolved tree. A
connection’s config may carry a secret reference of the exact shape
{ "$secret": "NAME" }. At resolution time the resolver:
- Reads
NAMEfrom the process environment (os.LookupEnv). - Substitutes the value only to validate the connection config (a connection-type schema expects the concrete value, e.g. a header string).
- Discards the resolved value immediately afterward.
What is kept in the resolved tree is not the value:
- The connection’s
configretains the{ "$secret": "NAME" }reference object, unchanged. - A sorted
secretslist records which secret names the connection consumed — names only, never values.
So the serialized resolved tree is secret-value-free by construction. A
malformed reference (empty or non-string name) fails fast with SECRET_INVALID;
a reference whose NAME is absent from the environment fails fast with
SECRET_MISSING.
Because a
$secretmust resolve from the environment, documents that use one require the variable to be set even just toresolveby hand. For example,examples/kitchen-sink-dashboard.jsonneedsMETRICS_API_TOKEN:METRICS_API_TOKEN=xyz lattice resolve examples/kitchen-sink-dashboard.jsonThe token value never appears in the output.
The secretRefs map on a connection is a separate, complementary indirection:
it maps a logical secret name to an opaque reference token (e.g. a vault
URI) and is passed through verbatim. It, too, never carries a secret value.
Result-shape contract
A bound item type may declare what its connection’s results should look like via
the expectedResult schema-level keyword. For the table type, that contract
is: an array of non-empty row objects whose cells are non-null scalars (string,
number, or boolean).
When an item declares a connectionId, the resolver enforces the contract:
- The item’s type must declare an
expectedResult, or resolution fails withCONTRACT_MISSING— a binding with no shape to validate against is an error. - The
expectedResultfragment must be a well-formed draft 2020-12 schema, or resolution fails withCONTRACT_INVALID. - For a
staticconnection, the inlinerowsare the one place a real data check is possible without a live fetch, so the resolver validates them against the contract; non-conforming data fails withRESULT_SHAPE_INVALID. (Note the contract is stricter than the static connection’s own config schema, which also permits null cells.)
The validated contract is recorded on the binding as contract
({ itemType, connectionId, expectedResult }). It is model-only: the
resolver validates the declared shape (and inline static data), never live
fetched data.