Outcome
Disable an approved organization app connection, clean up its subscriptions and mark affected placements as inaccessible.
POST /api/developer-console/connections/{consumerAppId}/{providerAppId}/disable
Action: app_topolo_developers.connections.disable.
Before you start
Use the original authenticated active organization with Developers connections:write. The selected service also requires organization-admin authority (including its explicit platform-access branch), regardless of the credential’s action permission. Approval is required for disabling the actual pair, removing its notification subscriptions and marking affected placements access_removed; CLI/MCP confirmation is not that approval.
No organization id is accepted in action input. This handler derive it from the authenticated user. No workspace resource selector is declared in these exact action inputs or passed by the selected handler; do not invent one or mistake consumer/provider workspaceScope for a credential binding. Shared console authentication/resource/profile initialization is outside these selected handlers; its existing owner-qualified context must remain in force. This source packet does not certify the whole request as zero-write, and no automatic deletion of any profile/mirror is a rollback.
Discover the exact consumer/provider app ids and read the current connection before planning this mutation. Input is exactly consumerAppId and providerAppId matching ^app_[a-z0-9]+(?:_[a-z0-9]+)+$. No organization/workspace selector, status, retry token or expected revision is admitted. Preserve both current subscription collections and known placement use for impact review.
All examples are complete source-derived synthetic UNRUN carriers with fictional identities and specified storage/dependency premises. No disable, Notify cleanup, grant/provider operation, placement change, app import/test or private app read was performed. Current declaration source matches immutable7b89; deployed runtime is not certified.
Review the request
These ids are fictional. Discover the real pair, validate the current input and review the actual plan and cleanup impact before confirming a real call.
{
"consumerAppId": "app_example_consumer",
"providerAppId": "app_example_provider"
}
Cleanup and partial outcomes
POST /api/developer-console/connections/{consumerAppId}/{providerAppId}/disable loads the exact organization/pair, treats a truthy removedAt as not live, and returns404 connection_not_found if no live row exists. It then requires actor.isAdmin, including for removal of a pending needs_approval request. This path does not require the apps still be published or freshly entitled before cleanup; it does not call resolveDeclaration/requireEntitled.
The action is not an atomic state toggle. It sequentially removes every saved ordinary subscription, then every reference subscription. For a truthy subscriptionId it calls the configured Notify removal port with the original organization. Ordinary subscriptions use the consumerAppId; reference subscriptions use Developers’ app id. If the id is null/falsy, the local subscription row is deleted without a Notify call. There is no declared provider business action execution or new notification send in this cleanup path.
When Notify is not configured or removal throws, the selected helper marks that subscription failed and retains its existing id, then returns a labeled failure. Other subscriptions continue cleanup. Successful removals delete their local rows. A database error while recording/deleting can instead propagate immediately, leaving earlier external/local effects in place. Removing a subscription is not proof that previously delivered or already queued events have been recalled.
If any collected cleanup failures remain, the connection is set degraded with lastError beginning subscription cleanup failed:, then HTTP502 subscription_cleanup_failed includes the freshly read connection in error.details.connection. Removal actor/time and placement access_removed updates have not run in this collected-failure branch. Already successful subscription cleanup is not undone. A later exception can produce a different outer failure; no exact error receipt is invented for unknown infrastructure failures.
After cleanup succeeds, the store marks removedBy/removedAt, clears lastError and updates updatedAt on the still-unremoved row. It does not change the stored status to disabled or erase approval/request/declaration history. A normal200 response can therefore have outcome disabled while connection.status is still active, degraded or needs_approval. The removal timestamp is what makes the row no longer live.
The service then marks live placements for this organization/consumer/provider pair access_removed with statusReason connection_disabled and a fresh updatedAt, excluding already access_removed rows and soft-deleted placements. It does not erase the placed reference or provider data. The returned response contains no placement count. Connection removal and placement updates are separate awaited writes without one transaction or compare-and-set over the entire cleanup.
The complete normal200 envelope is {success:true,message:"OK",data:{outcome:"disabled",connection:<full stored ConnectionRecord or null>}}. The final store read includes removed rows and both remaining subscription collections. Concurrent changes or a missing final row can change the returned value; it is not a global transaction receipt or an entitlement/provider/Notify-delivery certificate.
The route accepts an empty HTTP body or a body matching the path pair. Malformed JSON, arrays/nonobjects, conflicting body/path ids or invalid strict pair input produce422 invalid_input with details.body:true. For supported CLI/MCP calls, validate the exact current two-id payload; do not exploit permissive raw-body branches. It still requires the intended active organization; no organization id is accepted from input.
A repeated disable after soft removal returns404, not an idempotent200 receipt. If removal committed but placement marking or final read failed, a blind retry may return404 without completing the missing placement work. Preserve actual saved effects and route that case to the owning repair workflow; do not re-enable just to make cleanup callable again.
Disabling is not a universal revocation of every historical API key, provider record, workspace grant or queued message. This selected source removes its subscriptions, soft-removes the connection and marks applicable placements. Shared console authentication/context may initialize state outside this helper, and underlying dependency failures are not certified absent.
Worked responses
Successful cleanup preserves historical status on a removed row
Complete synthetic UNRUN HTTP200. A fictional administrator disables a stored active row after all subscription cleanup succeeds, placement marking finishes and the final read returns the shown full record.
{
"success": true,
"message": "OK",
"data": {
"outcome": "disabled",
"connection": {
"id": "conn_docs_fixture",
"organizationId": "org_docs_fixture",
"consumerAppId": "app_example_consumer",
"providerAppId": "app_example_provider",
"status": "active",
"workspaceScope": "linked",
"declaration": {
"actions": [
{
"action": "app_example_provider.items.list",
"scopes": [
"items:read"
],
"resource": "workspace",
"optional": false,
"purpose": "Read example items"
}
],
"events": []
},
"declarationDigest": null,
"declarationSource": "topolo.app.json",
"lastError": null,
"requestedBy": "user_docs_requester",
"requestedAt": "2026-10-21T08:00:00.000Z",
"approvedBy": "user_docs_approver",
"approvedAt": "2026-10-21T08:00:00.000Z",
"removedBy": "user_docs_admin",
"removedAt": "2026-10-22T18:00:00.000Z",
"createdAt": "2026-10-21T08:00:00.000Z",
"updatedAt": "2026-10-22T18:00:00.000Z",
"subscriptions": [],
"referenceSubscriptions": []
}
}
}
outcome disabled does not require connection.status disabled. Read removedAt and inspect actual downstream effects; no real subscription or placement was changed.
Reference subscription cleanup fails after an ordinary local row was removed
Complete synthetic UNRUN HTTP502. The starting ordinary subscription has a null id and its local row is deleted. The reference subscription has a saved id but the configured dependency is absent, so it is recorded failed. No connection removal or placement marking runs in this branch.
{
"success": false,
"error": {
"message": "subscription cleanup failed: app_example_provider example.reference.changed@1 (references): Notify is not configured for this environment",
"code": "subscription_cleanup_failed",
"details": {
"connection": {
"id": "conn_docs_fixture",
"organizationId": "org_docs_fixture",
"consumerAppId": "app_example_consumer",
"providerAppId": "app_example_provider",
"status": "degraded",
"workspaceScope": "linked",
"declaration": {
"actions": [
{
"action": "app_example_provider.items.list",
"scopes": [
"items:read"
],
"resource": "workspace",
"optional": false,
"purpose": "Read example items"
}
],
"events": []
},
"declarationDigest": null,
"declarationSource": "topolo.app.json",
"lastError": "subscription cleanup failed: app_example_provider example.reference.changed@1 (references): Notify is not configured for this environment",
"requestedBy": "user_docs_requester",
"requestedAt": "2026-10-21T08:00:00.000Z",
"approvedBy": "user_docs_approver",
"approvedAt": "2026-10-21T08:00:00.000Z",
"removedBy": null,
"removedAt": null,
"createdAt": "2026-10-21T08:00:00.000Z",
"updatedAt": "2026-10-22T18:00:00.000Z",
"subscriptions": [],
"referenceSubscriptions": [
{
"sourceAppId": "app_example_provider",
"eventType": "example.reference.changed",
"eventVersion": 1,
"purpose": "changed",
"referenceTypes": [
"example.item"
],
"subscriptionId": "whs_docs_fixture",
"status": "failed",
"lastError": "Notify is not configured for this environment",
"updatedAt": "2026-10-22T18:00:00.000Z"
}
]
}
}
}
}
The full degraded connection retains its reference subscription id and removal fields remain null. Successful earlier cleanup is not rolled back. Resolve actual dependency failure before considering an approved retry.
A nonadministrator cannot remove a live connection
Complete synthetic UNRUN HTTP403. A live row exists, but the actor has the action permission without organization-admin authority.
{
"success": false,
"error": {
"message": "Only an organization admin can disable a connection",
"code": "organization_admin_required"
}
}
Action permission and organization-admin authority are separate. Do not change identity or expand grants automatically.
The pair has no live row, including after successful removal
Complete synthetic UNRUN HTTP404. This occurs before the selected service admin check and does not distinguish absent from already soft-removed state.
{
"success": false,
"error": {
"message": "app_example_consumer has no connection to app_example_provider in this organization",
"code": "connection_not_found"
}
}
Do not treat this as proof that all earlier cleanup completed. Reconcile retained outcomes and owner evidence rather than recreating or re-enabling the pair.
Verify and recover
-
Keep the original principal, organization and discovered pair. Retain the complete actual disable response or error and request id; compare removal fields, preserved stored status and both subscription collections to the reviewed preimage.
-
Use connections.status after the action: its connection view should be null once removedAt is set, but state can be needs_setup or not_connected from current declarations/entitlements. Neither value alone proves all prior subscription/placement work finished. connections.list normally excludes soft-removed rows and has a500 cap.
-
For separately authorized affected consumer records, read the relevant placements and verify access_removed/connection_disabled while preserving provider data. The disable response has no placement count; do not infer a complete inventory from an empty bounded read.
-
On502 or unknown response, reconcile the returned/current connection and subscription outcomes before any approved retry. If the row remains live, a later supported cleanup may be appropriate after the real dependency issue is resolved. If already removed and placement cleanup is uncertain, involve the owner; do not broaden grants, create new ids, blindly resend or force a new connection.
No automatic inverse. Re-enabling is a separately approved mutation with current declarations, entitlements and administrator policy, potentially new subscriptions, and is not a restoration of original subscription ids, prior deliveries or placement snapshots. Read actual outcomes first; use owner repair for uncertain post-removal placement work.
Related actions
- Inspect connection status with entitlement and history limitations.
- List organization connections to discover current live pairs.
Change log
- 2026-10-22: Added exact administrator/permission boundaries, sequential subscription cleanup, full success and partial-failure responses, soft-removal versus stored-status semantics, placement effects and safe recovery limits. Examples remain synthetic UNRUN; no deployed cleanup is certified.