Skip to main content

Dependencies

Looking for the product view?
The Help Center explains this surface as users see it: Dependencies.

Apps can declare typed dependencies on Informer resources (datasets, queries, datasources, integrations) and invoke them through a dependency-injected proxy from server-side handlers. Dependency bindings are install-tenant-local: the publisher's manifest names the slot and target type, the recipient picks the actual entity at install time.

Concept

Two row shapes live in the unified app_entity table:

ShapeManifest sectionSet by
Dependencydependencies:Author at deploy + installer at bind. Has name, target (resource type), and an optional FK to the bound entity. Addressed at runtime via context.<name>.<method>(...) from server handlers.
Whitelist entryaccess:Author at deploy. Has apiPaths (the paths the path-pattern proxy accepts) and an optional FK. Addressed by direct fetch from app code.

Dependency rows persist their declared target (dataset / query / datasource / integration) in the row's target column. The target field is fixed at deploy time; the install/edit UI changes targetId, runAs, and options but not the type.

Manifest

# informer.yaml
dependencies:
northwindOrders:
target: dataset
runAs: user
description: "Northwind orders dataset"
options:
filter:
region: $user.custom.region
reportTemplates:
target: query
runAs: owner

runAs accepts user (default — viewer's credentials) or owner (app owner's credentials, looked up at request time, cached for 5 minutes). options is a per-driver schema validated at deploy time.

Routes

GET /apps/{id}/dependencies

Returns the app's dependency rows + the read-only whitelist (granted access) rows. The install/audit UI consumes both arrays.

Permission: app.lookup (read access on the app).

List an App's dependenciesGET /api/apps/admin%3Aorder-desk/dependencies
items are manifest-declared slots (bound or not); whitelist and apis surface the read-only granted-access entries.
Response · 200
{
"_links": {
"self": {
"href": "https://informer.example.com/api/apps/admin%3Aorder-desk/dependencies"
}
},
"items": [
{
"name": "orders",
"target": "dataset",
"description": "The orders Dataset this app reads",
"bound": false,
"runAs": "user",
"options": {},
"targetId": null,
"targetName": null,
"targetUrl": null,
"methodSurface": [
"search",
"fields"
],
"userCanBind": true
}
],
"whitelist": [],
"apis": [
{
"method": "GET",
"url": "/api/jobs/*",
"runAs": "user"
}
],
"apisError": null
}
Captured from the API examples test suite; ids and timestamps are normalized.

Response Sections:

FieldDescription
itemsManifest-declared dependency slots (bound or not), with each driver's describeBinding output
whitelistRead-only granted-access rows (access: typed entries with apiPaths)
apisThe manifest's raw access.apis entries, normalized to { method, url, runAs }. Only populated for app types whose driver honors access.apis (full Apps)
apisErrornull, or { kind: "parse" | "load", message } when the manifest could not be read — distinct from "no raw access declared" so the audit panel can show an error instead of a clean empty state

bound is true only when the FK target resolves under the requesting user's read-access scope. A row with targetId set but bound: false indicates the target was deleted or read access was revoked since the binding was made — the UI surfaces this as "Target unavailable."

userCanBind reflects whether the requester holds app:bind (Member+) — view-only users see the rows and binding metadata for transparency but cannot trigger the PUT.

PUT /apps/{id}/dependencies/{name}

Binds or rebinds a dependency to a target.

Permission: permission.app.bind (Member+, level 2). Distinct from app.edit: binding/unbinding dependency slots is allowed on managed apps (origin: deployed | marketplace) because the whole point of an unbound row is for the installer to fill it in. app.edit would lock managed apps and leave them permanently broken at runtime.

runAs and options are optional; existing values are preserved when omitted. targetId is required and must be a UUID.

Bind a dependency slotPUT /api/apps/00000000-0000-4000-8000-000000000001/dependencies/orders
The target must resolve under the CALLER’s read access or the bind is refused 403. Rebinding overwrites; unknown slot names are 404 (slots are manifest-declared).
Request body
{
"targetId": "00000000-0000-4000-8000-000000000002",
"runAs": "user"
}
Response · 200
{
"_links": {
"self": {
"href": "https://informer.example.com/api/apps/00000000-0000-4000-8000-000000000001/dependencies/orders"
}
},
"id": "00000000-0000-4000-8000-000000000003",
"tenant": "acme",
"appId": "00000000-0000-4000-8000-000000000001",
"name": "orders",
"target": "dataset",
"configId": null,
"runAs": "user",
"options": {},
"description": "The orders Dataset this app reads",
"apiPaths": null,
"datasetId": "00000000-0000-4000-8000-000000000002",
"queryId": null,
"datasourceId": null,
"integrationId": null,
"createdAt": "2026-01-15T16:20:00.000Z",
"updatedAt": "2026-01-15T16:20:00.000Z"
}
Captured from the API examples test suite; ids and timestamps are normalized.

The route resolves the driver from row.target (set by extractAppEntities at deploy time), so first-bind on a freshly-deployed unbound row works the same way as rebind. The payload targetId is validated against the driver's read_access scope — a 403 with "not found, or you don't have read access" surfaces both the not-found and access-denied cases.

DELETE /apps/{id}/dependencies/{name}

Unbinds a dependency slot: clears the FK and binding-time config (runAs back to user, options back to {}) but keeps the declared row, so a subsequent PUT can rebind without a redeploy. Slots themselves are added/removed only by the deploy pipeline's manifest reconciliation.

Permission: permission.app.bind (Member+, level 2). Same rationale as the PUT — installers of managed apps can unbind.

Unbind a dependency slotDELETE /api/apps/00000000-0000-4000-8000-000000000001/dependencies/orders
Clears the binding but keeps the declared slot, so it can be rebound without a redeploy.
Response · 200
{
"_links": {
"self": {
"href": "https://informer.example.com/api/apps/00000000-0000-4000-8000-000000000001/dependencies/orders"
}
},
"id": "00000000-0000-4000-8000-000000000002",
"tenant": "acme",
"appId": "00000000-0000-4000-8000-000000000001",
"name": "orders",
"target": "dataset",
"configId": null,
"runAs": "user",
"options": {},
"description": "The orders Dataset this app reads",
"apiPaths": null,
"datasetId": null,
"queryId": null,
"datasourceId": null,
"integrationId": null,
"createdAt": "2026-01-15T16:20:00.000Z",
"updatedAt": "2026-01-15T16:20:00.000Z"
}
Captured from the API examples test suite; ids and timestamps are normalized.

POST /apps/{id}/_modernize-manifest

Migrates a legacy access: block in the app's informer.yaml into the dependencies: shape that the install panel can re-bind without manifest edits. Used when shipping an app whose dependencies were originally declared as raw access entries.

Permission: permission.app.edit (Member+, level 2 + not managed). The mutation rewrites the manifest, so it's gated by edit not bind.

Behavior:

  1. A pre-modernize snapshot is taken (app_snapshot row labeled modernize) so the change is reversible.
  2. Each access: entry that resolves under the requesting user's read_access scope is moved to a dependencies: slot with a pre-filled defaultBinding: <UUID>.
  3. Entries that don't resolve are kept in access: with a skipped: reason in the response summary.
  4. The route deploys the rewritten manifest. If the deploy fails (e.g. because a phantom entry is still in access: and the resource isn't visible), the route returns 400 with the snapshotId so the user can restore.

Response (200, success):

{
"summary": {
"migrated": [{ "target": "dataset", "fromConfigId": "...", "defaultBinding": "<UUID>" }],
"skipped": [{ "section": "datasets", "entry": "...", "reason": "..." }],
"kept": ["apis"],
"unchanged": false
},
"snapshotId": "<UUID>"
}

Response (400, redeploy failed):

{
"statusCode": 400,
"message": "Deploy failed: ...",
"data": { "snapshotId": "<UUID>" }
}

Idempotency: a second call on an already-modernized app returns 200 with unchanged: true and no snapshotId (no-op skips the snapshot to avoid churn from polling clients).

GET /apps/{id}/draft/_diff

Returns a structured diff of app_entity rows between an app and its draft, used by the commit confirmation UI.

Permission: app.lookup.

Response shape:

{
"dependencies": [
{
"name": "northwindOrders",
"status": "changed",
"was": { "target": "dataset", "targetId": "<old>", "targetName": "Old", "runAs": "user", "options": {} },
"now": { "target": "dataset", "targetId": "<new>", "targetName": "New", "runAs": "owner", "options": {} },
"changes": ["targetId", "runAs"]
}
],
"whitelist": {
"added": [ { "configId": "admin:foo", "target": "dataset", "apiPaths": ["..."] } ],
"removed": [ { "configId": "admin:bar", "target": "query", "apiPaths": ["..."] } ]
}
}

status is one of added / removed / changed / preserved. Returns 404 when no draft exists for the app.

Runtime invocation

Server-side handlers receive context as their first parameter, populated with one entry per declared dependency. Each entry is a typed proxy whose methods route through server.inject against the bound target's API endpoints.

// server/orders/list.js
module.exports = async ({ context, query }) => {
const result = await context.northwindOrders.search({
query: { match: { region: query.region } }
});
return result.hits.hits.map(h => h._source);
};

The proxy's method surface comes from the driver:

DriverMethods
datasetsearch(esQuery), fields()
queryexecute(payload)
datasourcequery(payload)
integrationrequest(payload)

Calling a method on an unbound dependency throws a structured 422 with errorCode: 'dependency_unbound'. Calling a method on a bound dependency whose target was deleted throws 422 with errorCode: 'dependency_broken'. Non-2xx responses from the upstream API route propagate as boom errors with the upstream status code attached.

Canonical handler invocation

Apps invoke their own server-side handlers via:

GET /apps/{id}/view/_/{path*}
POST /apps/{id}/view/_/{path*}
PUT /apps/{id}/view/_/{path*}
PATCH /apps/{id}/view/_/{path*}
DELETE /apps/{id}/view/_/{path*}

{path} matches the route filename in the app's server/ directory (e.g. server/orders/list.jsPOST /view/_/orders/list). Authentication flows through the app's view session (cookie or token).

The legacy /view/api/_server/{path*} form continues to work — both routes dispatch through the same handler logic.

Errors

StatusCause
400Manifest references an unregistered driver target (typo in target:) — caught at deploy time before the row is written.
400Manifest dependency name doesn't match ^[a-z][a-z0-9_]*(\.[a-z][a-z0-9_]*)*$ (lowercase dot-segmented).
400Production deploy with target-type drift (e.g. target: datasettarget: integration on an existing dependency). Drafts permit drift; production rejects.
403PUT target doesn't resolve under read_access for the requester.
404Dependency name not declared in the app's manifest.
412runAs: 'owner' binding on a team-owned app whose owner team has no admins.
422Runtime invocation of an unbound or broken dependency proxy (returned to the app handler as a boom-shaped object with errorCode).