Concepts
The Adapter Pattern
Section titled “The Adapter Pattern”@forge-clients separates what to call (the generated API functions) from how to make
the call (the adapter). This is the core design principle.
Your code BoundClient Adapter Forge runtime───────── ─────────── ─────── ─────────────asApp(adapter) → { adapter, authContext }asUser(adapter) → { adapter, authContext } ↓getIssue(client, params) → builds request → ForgeFunctionAdapter → @forge/api ForgeBridgeAdapter → @forge/bridge ForgeContainerAdapter → FORGE_EGRESS_PROXY_URL ForgeRemoteAdapter → FORGE_EGRESS_PROXY_URLEvery generated function takes a BoundClient as its first argument — a plain object
created by asApp(), asUser(), or asOfflineUser() that carries the adapter and auth
context together. Swap the adapter to change how the request is made without changing
your business logic.
Adapters
Section titled “Adapters”ForgeFunctionAdapter
Section titled “ForgeFunctionAdapter”Use this in Forge Functions (backend resolvers). It wraps @forge/api’s
requestJira and requestConfluence.
import { ForgeFunctionAdapter } from '@forge-clients/core';const adapter = new ForgeFunctionAdapter({ product: 'jira' });ForgeBridgeAdapter
Section titled “ForgeBridgeAdapter”Use this in Custom UI (browser-side code). It calls the Forge bridge, which proxies the request through the Forge runtime. The user context is implicit.
import { ForgeBridgeAdapter } from '@forge-clients/core';const adapter = new ForgeBridgeAdapter({ product: 'jira' });ForgeContainerAdapter
Section titled “ForgeContainerAdapter”Use this in Forge Containers (long-running Docker services). It uses the
FORGE_EGRESS_PROXY_URL sidecar proxy with explicit forge-proxy-authorization headers.
The installationId must be fetched at startup from GET <proxyUrl>/v0/installations.
import { ForgeContainerAdapter } from '@forge-clients/core';
const proxyUrl = process.env.FORGE_EGRESS_PROXY_URL!;const { installationId } = await fetch(`${proxyUrl}/v0/installations`).then(r => r.json());
const adapter = new ForgeContainerAdapter({ product: 'jira', proxyUrl, installationId,});ForgeRemoteAdapter
Section titled “ForgeRemoteAdapter”Use this in Forge Remotes (externally hosted backends). The installationId and
appSystemToken come from the Forge Remote invocation payload — use the
adapterFromForgePayload() convenience factory:
import { adapterFromForgePayload, type ForgeInvocationPayload } from '@forge-clients/core';
export async function handler(payload: ForgeInvocationPayload) { const adapter = adapterFromForgePayload(payload, 'jira'); // ...}Auth Contexts and BoundClient
Section titled “Auth Contexts and BoundClient”Auth context is set once using asApp(), asUser(), or asOfflineUser() — each
returns a BoundClient (a plain object holding the adapter + auth context). All
generated functions accept a BoundClient as their first argument.
import { asApp, asUser, asOfflineUser, withAuth } from '@forge-clients/core';
// Use the app's own credentialsconst appClient = asApp(adapter);
// Use the invoking user's credentials (Forge Functions / Custom UI)const userClient = asUser(adapter);
// Impersonate a specific user by account IDconst specificUser = asUser(adapter, 'account:abc123');
// Offline user impersonation (Containers / Remotes) — fetch token firstconst token = await tokenManager.getToken(accountId);const offlineClient = asOfflineUser(adapter, token.accountId, token.accessToken);// Or use the convenience method:const offlineClient2 = await tokenManager.boundClient(adapter, accountId);
// Switch auth context on an existing BoundClientconst asAppAgain = withAuth(userClient, { type: 'asApp' });See the Auth Contexts guide for full details.
Generated Functions
Section titled “Generated Functions”Every Atlassian REST API endpoint becomes a named async function:
// Jira v3 (default — recommended)import { getIssue, createIssue, searchForIssuesUsingJqlPost } from '@forge-clients/jira/v3';
// Jira Softwareimport { getBoard, getSprint } from '@forge-clients/jira/software';
// Confluence v2 (default — recommended)import { getPages, getPageById } from '@forge-clients/confluence/v2';
// Confluence v1 (legacy)import { getContentById, createContent } from '@forge-clients/confluence/v1';Functions are individually exported — bundlers tree-shake unused functions automatically.
Error Handling
Section titled “Error Handling”All errors from generated functions are instances of ForgeApiError subclasses:
import { ForgeApiError, NotFoundError, RateLimitError } from '@forge-clients/core';
try { const issue = await getIssue(asApp(adapter), { path: { issueIdOrKey: 'PROJ-999' } });} catch (err) { if (err instanceof NotFoundError) { console.log('Issue not found'); } else if (err instanceof RateLimitError) { console.log(`Retry after ${err.retryAfterSeconds}s`); }}See the Error Handling guide for full details.