.agents/features/app-connections.md
App Connections store encrypted authentication credentials (OAuth2 tokens, API keys, basic auth, custom piece-defined fields, or OIDC props) that flow steps use to call external services. They support automatic OAuth2 token refresh with distributed locking, a dual-scope model (project-level or platform-wide), and a project-scoped "replace" operation that rewires flow references from one connection to another. Platform/global connections can be selected as the replace source, but only project-scoped sources can be deleted afterwards — deleting platform connections stays exclusive to the platform admin page. The module handles all OAuth2 variants: user-supplied credentials, platform-managed OAuth apps, and Activepieces-hosted cloud OAuth. Users can optionally select a subset of a piece's declared OAuth2 scopes when creating a connection. OIDC connections enable pieces to obtain short-lived credentials from cloud providers (e.g. AWS via IRSA/Web Identity) using the Activepieces platform as an OIDC identity provider.
packages/server/api/src/app/app-connection/ — backend module (controller, service, entity)packages/core/shared/src/lib/automation/app-connection/app-connection.ts — core types, enums, and value union typespackages/core/shared/src/lib/automation/app-connection/dto/upsert-app-connection-request.ts — upsert DTOpackages/core/shared/src/lib/automation/app-connection/dto/read-app-connection-request.ts — list query DTOpackages/core/shared/src/lib/automation/app-connection/oauth2-authorization-method.ts — OAuth2 authorization method enumpackages/web/src/features/connections/api/app-connections.ts — frontend API clientpackages/web/src/features/connections/api/global-connections.ts — global (platform-scope) connections API clientpackages/web/src/features/connections/hooks/app-connections-hooks.ts — TanStack Query hooks (appConnectionsQueries, appConnectionsMutations)packages/web/src/features/connections/hooks/global-connections-hooks.ts — global connections hookspackages/web/src/features/connections/utils/oauth2-utils.ts — OAuth2 redirect URL helperspackages/web/src/features/connections/utils/utils.ts — name-uniqueness check helperspackages/web/src/app/routes/connections/index.tsx — project connections list pagepackages/web/src/app/routes/platform/setup/connections/index.tsx — platform-wide global connections pagepackages/web/src/app/connections/new-connection-dialog.tsx — new connection dialog wrapperpackages/web/src/app/connections/create-edit-connection-dialog.tsx — create/edit connection form dialogpackages/web/src/features/connections/components/edit-global-connection-dialog.tsx — edit global connection dialogpackages/web/src/features/connections/components/rename-connection-dialog.tsx — rename connection dialogpackages/web/src/app/connections/oidc-connection-settings.tsx — OIDC connection form componentpackages/server/api/src/app/core/security/oidc/oidc-key-manager.ts — RSA key lifecycle: mutex-protected caching, auto-generation persisted (encrypted) to the shared flag table, RFC 7638 kid fingerprintpackages/server/api/src/app/core/security/oidc/oidc.module.ts — module wrapper that registers the OIDC token controller under /v1/workerpackages/server/api/src/app/core/security/oidc/oidc-token.controller.ts — engine-only endpoint that issues RS256 JWTs (POST /api/v1/worker/oidc-token)packages/server/api/src/app/core/security/oidc/oidc-discovery.controller.ts — public OIDC discovery endpoints (GET /.well-known/openid-configuration, GET /.well-known/jwks.json)globalConnectionsEnabled plan flag.secrets.activepieces.com for token exchange.Canonical term definitions live in the bounded-context glossaries — see CONTEXT-MAP.md.
PROJECT (restricted to projects in projectIds[]) or PLATFORM (available to all projects).OAUTH2, CLOUD_OAUTH2, PLATFORM_OAUTH2, SECRET_TEXT, BASIC_AUTH, CUSTOM_AUTH, NO_AUTH, OIDC.{ type: OIDC, props: T } — piece-defined OIDC props (role ARN, audience, etc.) stored encrypted; the actual token is fetched at runtime from the POST /api/v1/worker/oidc-token endpoint./.well-known/openid-configuration and /.well-known/jwks.json so cloud providers (e.g. AWS STS) can verify issued tokens.projectIds for every new project.PLATFORM-scope connection managed from the platform admin UI, shared across all (or selected) projects.403 (mirrors the project-route delete guard). Deleting a project source is rejected with 409 while any published version the replace does not update still references it, and a draft-and-published replace is rejected with 409 while a published version it cannot see (flow's newer draft dropped the connection) still references the source.AppConnection: id, displayName, externalId, type (AppConnectionType), status (ACTIVE/EXPIRED/ERROR), value (EncryptedObject), platformId, pieceName, pieceVersion, ownerId (nullable FK), projectIds[] (string array — multi-project), scope (PROJECT/PLATFORM), metadata (JSONB), preSelectForNewProjects (boolean).
| Type | Value Fields | Refresh |
|---|---|---|
| OAUTH2 | access_token, refresh_token, client_id, client_secret, token_url, expires_in, claimed_at | Auto-refresh |
| CLOUD_OAUTH2 | Same but tokens exchanged via secrets.activepieces.com | Auto-refresh via cloud |
| PLATFORM_OAUTH2 | Same but uses platform-managed OAuth app credentials | Auto-refresh |
| SECRET_TEXT | token | None |
| BASIC_AUTH | username, password | None |
| CUSTOM_AUTH | piece-defined custom fields + optional access_token, token_refresh_at | Optional — piece opts in via refresh callback; server caches token with 15-min early-refresh buffer (clamped to half the token lifetime) |
| NO_AUTH | (empty) | None |
| OIDC | piece-defined props (e.g. role ARN, audience) — token fetched at runtime | None (short-lived JWT issued per-request) |
Activepieces acts as an OIDC identity provider to enable pieces to assume cloud roles without long-lived credentials (e.g. AWS IRSA / Web Identity Federation).
Runtime flow:
POST /api/v1/worker/oidc-token with { audience, expiresInSeconds? } (engine-only endpoint, guarded by securityAccess.engine())sub: platform:{platformId}:project:{projectId}, aud set to the requested audience (e.g. sts.amazonaws.com), TTL defaults to 1 hour (capped at 1 hour)AssumeRoleWithWebIdentity) to get temporary credentialsCaller contract (pieces calling from run() via context.server):
audience (non-empty after trim) is mandatory; expiresInSeconds is optional (integer, 60–3600, default 3600). Requests without a body get 400.const response = await fetch(`${server.apiUrl}v1/worker/oidc-token`, {
method: 'POST',
headers: {
Authorization: `Bearer ${server.token}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({ audience: 'sts.amazonaws.com' }),
})
const { token } = await response.json()
Discovery endpoints (public, CORS-open, available when system.isApp() is true):
GET /.well-known/openid-configuration — issuer metadata pointing to the JWKS URIGET /.well-known/jwks.json — RSA public key set; kid is an RFC 7638 SHA-256 thumbprintKey management (oidcKeyManager):
flag table (encrypted with the platform encryption key)INSERT ... ON CONFLICT DO NOTHING + re-read, giving first-writer-wins convergence across all nodes (single- and multi-node alike — no env var or manual provisioning required)privateKeyMutex and publicKeyMutex guard caching independently to avoid deadlocksprojectIds[] array. Query with ArrayContains([projectId]).Automatic on connection retrieval:
lockAndRefreshConnection() checks if OAuth token expired (15-min early refresh threshold)key = ${platformId}_${externalId}, 60s timeout) — keyed on the connection's project-invariant identity so projects sharing a connection serialize on one lockrefresh() method (different per type: cloud/platform/credentials)Opt-in on connection retrieval — piece authors define a refresh callback in CustomAuthProperty:
PieceAuth.CustomAuth({
props: { ... },
refresh: {
generate: async ({ auth }) => {
const res = await httpClient.sendRequest({ ... })
return { access_token: res.body.token, expires_in: 3300 }
},
defaultExpiresIn: 3300, // seconds; used when generate() omits expires_in
},
})
Runtime flow:
needRefresh() checks connection.value.access_token:
now >= token_refresh_at (the precomputed refresh instant)pieceRefreshSupportCache (in-process LRU, 500 entries, 5-min TTL keyed by pieceName@pieceVersion); on cache miss, load piece metadata and check for refresh callback; result cached for future executionskey = ${platformId}_${externalId}, 60s)EXECUTE_TOKEN_REFRESH worker job (user-interaction queue, same pattern as EXECUTE_VALIDATION)refresh.generate() callback, returns { access_token, expires_in? }access_token and token_refresh_at stored encrypted in CustomAuthConnectionValue, status=ACTIVE. token_refresh_at = now + expiresIn - min(15 min, expiresIn / 2) so the 15-min early-refresh buffer never exceeds half the token's lifetime (a short-lived token would otherwise be stale the instant it is minted); expiresIn <= 0 means "never expires" → token_refresh_at is left unsetCustomAuthRefreshError → sets status=ERRORrefresh callback (returns skipped: true): clears stale access_token/token_refresh_at and sets cache to false so no further jobs fireInside piece actions/triggers, context.auth.access_token holds the cached token alongside the raw props.
POST /v1/app-connections — create/upsert connection (validates via worker EXECUTE_VALIDATION job)POST /v1/app-connections/:id — update displayName, metadata, preSelectForNewProjectsGET /v1/app-connections — list with filters (pieceName, displayName ILIKE, status, scope, externalIds)GET /v1/app-connections/owners — list connection owners (platform admins + project members)POST /v1/app-connections/replace — replace a source connection with a target across the current project's flows; source may be project-scoped or platform/global, but deleteSourceConnection on a platform source is rejected with 403 (platform connections are deleted from the platform admin page)DELETE /v1/app-connections/:id — hard delete; rejects PLATFORM-scope connections with 403 (delete those via the platform admin DELETE /v1/global-connections/:id instead)POST /v1/app-connections/oauth2/authorization-url — generate OAuth redirect URL from piece metadata; accepts optional scopes array to restrict the authorization to a user-selected subset of the piece's declared scopes (all piece scopes used when omitted)replace() updates the current project's flow references page by page (draft versions always, published versions when requested)deleteSourceConnection is set, the server refuses up-front if any published version the replace will not update still references the source, and re-checks right before deleting that no flow still uses it; PLATFORM sources cannot be deleted through replace at allAll connection values encrypted with encryptUtils.encryptObject() (AES-256) before storage, decrypted on retrieval.
The project connections page (/connections) shows a data table with status badges, piece icons, scope indicators, and bulk-delete support. Bulk-delete operates only on PROJECT-scope rows — PLATFORM-scope (global) connections are filtered out client-side and the delete button reflects only the deletable count; global connections must be deleted from the platform admin global connections page. The new-connection-dialog opens create-edit-connection-dialog which renders piece-auth-specific form fields. The "Replace" action opens replace-connections-dialog which calls the replace endpoint. The platform admin global connections page (/platform/setup/connections) uses global-connections-hooks and surfaces edit-global-connection-dialog for managing platform-scope connections and their projectIds assignment. The builder uses connection-select inside step settings to pick, create, or reconnect connections inline; active connections show a "Connected" status with a compact reconnect icon, while connections needing re-authentication keep a "Reconnect" action.