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 / app / pack) 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",
"optional": false,
"bound": false,
"runAs": "user",
"unreachableForViewers": false,
"options": {},
"targetId": null,
"targetName": null,
"targetUrl": null,
"targetImageUrl": null,
"methodSurface": [
"search",
"fields"
],
"owned": false,
"connected": null,
"canConnect": false,
"sharedConnection": null,
"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 when the slot has a resolved binding. For ordinary slots that means 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, which the UI surfaces as "Target unavailable." App-owned (BYOI) integration slots always report bound: true for every viewer: their binding is deploy-managed and does not depend on the viewer's read access to the integration.

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. It is forced to false on app-owned slots (below), whose binding belongs to the manifest.

unreachableForViewers is a diagnostic for a slot that dispatches as the viewer (runAs: "user") but is bound to a target only its owner can read: it resolves for the App owner and answers 404 for every other non-superuser viewer outside the owning principal. It is derived per row from the effective runAs and binding (not the manifest), so the install UI can flag the problem while the binding is still being chosen.

App-owned (BYOI) integration fields​

Slots bound to an Integration the App declares in its manifest's integrations: block carry these extra fields on each items entry. Each defaults to its "nothing to connect" value for non-integration or non-oauth2 slots, and all are absent on servers that predate the feature.

FieldDescription
ownedtrue when the slot is bound to an app-owned Integration (a projection of the manifest's integrations: block). Owned slots are deploy-managed: userCanBind is forced false and PUT/DELETE are refused.
optionaltrue when the author marked the slot (or owned entry) optional: true. The install UI keeps optional slots out of the "needs binding" count and warning state.
connectedOAuth connection state for oauth2-bound slots: the requesting user's connection for per-user slots, the shared connection for shared-mode slots. null when there is nothing to connect (non-integration target or non-oauth2 auth type).
canConnecttrue when the requester is capable of running the slot's OAuth consent: the binding is oauth2 and the Integration resolves under their read access. Per-user owned slots inherit readability from the App (any viewer); shared-mode owned slots are readable only by the owner or team admins, who are exactly the identities that may establish the shared consent. Drives the App's Connections popover.
sharedConnectiontrue when the connection is shared-mode (consenting or disconnecting affects every viewer); false for per-user. null when there is nothing to connect.
targetImageUrlService icon of the bound Integration (an App-library file copied onto the Integration, a well-known registry match, or the plug fallback), or null for resource types without one.

Pack dependency fields​

Slots declared target: pack carry three extra fields; all are absent on other slot types.

FieldDescription
packThe author's pin plus local install state: slug, requires (semver range), the installed version/channel, inRange, and remotePackId.
packStateOne derived word for the connect UI: needsInstall (no local install), outOfRange (installed but outside requires), ambiguous (stable and beta both in range, awaiting an admin's channel pick), or consented.
consentedtrue when the slot is usable: either an admin picked a channel, or exactly one in-range install auto-consented. Distinct from bound; a pack pin can resolve (bound: true) while consented is still false.

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",
"optional": false,
"apiPaths": null,
"datasetId": "00000000-0000-4000-8000-000000000002",
"queryId": null,
"datasourceId": null,
"integrationId": null,
"targetAppId": 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.

Slots bound to an app-owned (BYOI) Integration are deploy-managed and reject both PUT and DELETE with a 400 ("bound to the app-owned integration ... and cannot be rebound. Change the manifest and redeploy instead."). Their binding is declared by the manifest's integrations: block, so it is changed by editing the manifest and redeploying, not through these routes.

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",
"optional": false,
"apiPaths": null,
"datasetId": null,
"queryId": null,
"datasourceId": null,
"integrationId": null,
"targetAppId": 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.

GET /apps/{id}/dependencies/{name}/connect​

Slot-addressed OAuth connect for oauth2-bound integration slots, including app-owned (BYOI) ones. Resolves the Integration bound to the named slot and 302-redirects into the existing GET /integrations/{id}/connect flow, so App code opens a connect popup by slot name and never learns the Integration UUID up front. Everything downstream is unchanged: the OAuth handler signs the state JWT, the License Manager relays authorize/callback, and the consent completion notifies the opener via an informer:integration-connected postMessage.

Permission: the bound Integration's read_access, the same bar as the Integrations admin page (enforced again by the redirect target's own lookup). For an app-owned per-user slot that is any viewer who can read the App (readability is inherited from the App); for a shared-mode owned slot it is the owner or team admins, who are exactly the identities that may establish the shared consent. Slots bound to an ordinary shared Integration let any user with read access create their own per-user connection without leaving the App.

Query: session (optional) carries a session JWT so the flow stays authenticated across the redirect in cookie-less popup contexts (Informer GO web and desktop popups carry no cookie). Any other query params ride through untouched.

Errors:

StatusCause
400The slot is not bound to an Integration.
400The bound Integration's auth type is not oauth2 (only oauth2 integrations have a connect flow).
404The App or the dependency name is not readable, or the name is not declared in the manifest.

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).

Staged deploys: the route rewrites a library file, so it answers 409 while the app has a staged deploy. Nothing is rewritten and no snapshot is taken; wait for the deploy, or discard it with DELETE /files/_stage.

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)
apprequest(payload)
packrequest(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'. Invoking an app-owned oauth2 integration slot that has no connection for the effective identity throws 422 with errorCode: 'integration_not_connected', which the App catches to prompt the user through the slot's connect flow. 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.js → POST /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.

GET /apps/{id}/openapi.json​

An OpenAPI 3.0.3 document describing the App's deployed server/ routes, read live from the deployed route table, with a root-level API.md folded into info.description. This is the discovery surface for cross-app dependencies (target: app and target: pack): a depending App, its author's tooling, or the App Builder fetches this instead of reading the target's source.

Authentication: Required. No permission beyond read access to the App; an API contract is readable by anyone allowed to see the App.

Response: raw application/json. Unlike most Informer endpoints this body is not HAL-wrapped (a top-level _links would be invalid OpenAPI). The endpoint is linked as inf:app-openapi; resolve it by rel rather than building the URL.

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: dataset → target: integration on an existing dependency). Drafts permit drift; production rejects.
400PUT binds a target: app slot to the app itself (an app cannot depend on itself).
400PUT attempts to change a target: pack slot's author-defined pin; only a redeploy changes it.
400PUT options fail the driver's optionsSchema.
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, or an app-owned oauth2 slot with no connection (returned to the app handler as a boom-shaped object with errorCode dependency_unbound / dependency_broken / integration_not_connected).