Skip to content

Forge API

Every Forge app runs inside its own isolated runtime. The platform injects a $forge global that apps can call directly — no imports required. These methods bridge the running app back to the platform's UI for consent flows, marketplace operations, and integration management.

$forge is a global constant equivalent to window.$forge. Both forms work, but $forge is the recommended shorthand.


TypeScript types

The @ptkl/components package ships a dedicated forge-api sub-entry that provides the full $forge type declaration and named types for all method signatures.

Installation

npm install @ptkl/components

Enabling $forge types globally

Add a single side-effect import in your app's entry point (e.g. main.ts or index.ts). This augments the global scope so both $forge and window.$forge are typed everywhere in your project — no per-file imports needed:

// main.ts
import '@ptkl/components/forge-api'

// `$forge` is now fully typed across the entire app:
const granted = await $forge.requestPermissions(['orders.create'])
const result  = await $forge.requestMarketplaceInstall('my-integration', 'integration')
await $forge.openDeveloperIntegrations()

Importing individual types

When you need a type for a function parameter or return value, import it by name:

import type { ForgeAPI, MarketplaceInstallResult, OnboardingResult } from '@ptkl/components/forge-api'

// Use ForgeAPI when you want to reference the full interface
function initForge(api: ForgeAPI) {
  api.requestPermissions(['orders.read'])
}

// Use MarketplaceInstallResult when handling the resolved value of requestMarketplaceInstall
async function ensureInstalled(): Promise<MarketplaceInstallResult> {
  return $forge.requestMarketplaceInstall('smtp-integration', 'integration')
}

// Use OnboardingResult when handling the resolved value of openOnboarding
async function checkOnboarding(): Promise<OnboardingResult> {
  return $forge.openOnboarding()
}

Available exports

Export Kind Description
(side-effect) import '@ptkl/components/forge-api' Declares $forge and window.$forge as globals with the ForgeAPI type.
ForgeAPI interface Full shape of the $forge object.
MarketplaceInstallResult interface Return type of $forge.requestMarketplaceInstall.
OnboardingResult interface Return type of $forge.openOnboarding.

$forge.requestPermissions

Prompt the current user to approve additional permissions at runtime. A modal consent dialog opens over the app, and the function resolves with the list of permissions the user actually granted.

When to use it

Use this when your app needs a permission that is not declared in runtime_permissions (or the user launching the app does not currently hold it). Common patterns:

  • An action that is rarely needed and should not appear in the app's default token.
  • An operation that requires explicit user acknowledgement before it runs.

Signature

$forge.requestPermissions(
  permissions: string[],
  reason?: string
): Promise<string[]>
Parameter Type Required Description
permissions string[] Permission strings to request (e.g. 'orders.create').
reason string Human-readable explanation shown on the consent screen.

Returns — a Promise<string[]> that resolves with the subset of requested permissions the user approved. Rejects if the user closes or dismisses the dialog.

Example

try {
  const granted = await $forge.requestPermissions(
    ['orders.create'],
    'Required to submit the order on your behalf.'
  )

  if (granted.includes('orders.create')) {
    await placeOrder(cart)
  } else {
    showError('Permission was not granted.')
  }
} catch {
  // User closed the dialog
}

Multiple permissions

const granted = await $forge.requestPermissions(
  ['customers.read', 'customers.update', 'tags.write'],
  'Needed to tag customers based on their purchase history.'
)

const missing = ['customers.read', 'customers.update', 'tags.write']
  .filter(p => !granted.includes(p))

if (missing.length) {
  console.warn('Not all permissions were granted:', missing)
}

Token update

Once the user approves, the platform mints a new token that includes the granted permissions and posts it back. The SDK picks it up automatically — you do not need to refresh the page or re-initialise anything. The next API call made through the SDK will carry the updated token.


$forge.requestMarketplaceInstall

Prompt the current user to install a marketplace item (integration, app, or template) from within your app. A modal install dialog opens and the function resolves once the user completes (or already has completed) the installation.

When to use it

Use this when your app depends on another marketplace item and wants to guide the user through installing it without leaving your app's context.

Signature

$forge.requestMarketplaceInstall(
  itemId: string,
  itemType: 'integration' | 'app' | 'template',
  options?: { initiator?: string }
): Promise<{ id: string; type: string; already_installed: boolean }>
Parameter Type Required Description
itemId string The marketplace item ID.
itemType 'integration' \| 'app' \| 'template' The item category.
options.initiator string Override the initiating app name (defaults to the current app).

Returns — a Promise that resolves with:

Field Type Description
id string The installed item's ID.
type string The item type as returned by the platform.
already_installed boolean true if the item was already installed before the user opened the dialog.

Rejects if the user cancels or closes the dialog.

Example

try {
  const result = await $forge.requestMarketplaceInstall(
    'smtp-integration',
    'integration'
  )

  if (result.already_installed) {
    console.log('Integration was already installed — continuing.')
  } else {
    console.log('Integration installed successfully.')
  }

  await initSmtp()
} catch {
  showError('The required integration was not installed.')
}

Checking already_installed

The already_installed flag lets you distinguish between a fresh install and a user who had the item installed before opening the dialog:

const { already_installed } = await $forge.requestMarketplaceInstall(
  'payment-gateway',
  'integration'
)

if (!already_installed) {
  await showWelcomeTour()
}

$forge.openOnboarding

Open the platform onboarding flow in a modal overlay, directly from within your Forge app. Use this when your app is in waiting_for_onboarding status and needs the user to complete their workspace setup before the app can function.

When to use it

The platform calls this automatically when it detects that your app's status is waiting_for_onboarding. You can also call it manually when you want to re-surface the onboarding flow at a later point — for example, after an incomplete earlier session.

  • The overlay loads the platform's /onboarding page inside an iframe (960 × 85 vh).
  • When the user completes all onboarding steps, the page signals completion and the overlay closes with { completed: true }.
  • If the user closes the overlay without finishing, it resolves with { completed: false }.
  • Calling openOnboarding from within an iframe is a no-op — it resolves immediately with { completed: false } to prevent nested overlays.

Signature

$forge.openOnboarding(): Promise<OnboardingResult>

Returns — a Promise<OnboardingResult> that always resolves (never rejects):

Field Type Description
completed boolean true if the user finished all onboarding steps; false if they closed without finishing.

Example

const { completed } = await $forge.openOnboarding()

if (completed) {
  // Onboarding done — boot the app normally
  await initApp()
} else {
  // User dismissed without finishing — show a blocked state
  showOnboardingRequired()
}

Preflight pattern

import '@ptkl/components/forge-api'
import type { OnboardingResult } from '@ptkl/components/forge-api'

async function bootstrap() {
  const status = await fetchAppStatus()

  if (status === 'waiting_for_onboarding') {
    const result: OnboardingResult = await $forge.openOnboarding()
    if (!result.completed) {
      renderBlockedState()
      return
    }
  }

  renderApp()
}

Behaviour in ptkl forge dev

Scenario Behaviour
--platform-url provided Opens a full modal overlay with the onboarding page loaded from <platform-url>/onboarding.
--platform-url not provided Logs a warning and resolves immediately with { completed: false } — no overlay is shown.
Called from inside an iframe Resolves immediately with { completed: false } — overlay is suppressed to prevent nested iframes.

$forge.openDeveloperIntegrations

Open the platform's developer integrations management page in a modal overlay, directly from within your Forge app. The overlay closes when the user dismisses it, and the returned Promise resolves at that point.

When to use it

Use this when your app requires one or more integrations to be activated before it can operate, and you want to guide the user to the integrations screen without leaving the app context. Common pattern:

  • A preflight check detects that required integrations are not yet active.
  • Your app surfaces a blocked state and offers a direct "Open integrations" action.
  • After the user activates the integrations and closes the overlay, your app re-runs its preflight check.

Signature

$forge.openDeveloperIntegrations(): Promise<void>

Returns — a Promise<void> that resolves when the user closes the integrations overlay. The function does not indicate which integrations were changed; your app should re-query its state after resolution.

Example

const missing = await checkRequiredIntegrations()

if (missing.length > 0) {
  document.getElementById('open-integrations').addEventListener('click', async () => {
    await $forge.openDeveloperIntegrations()
    // User closed the overlay — re-run preflight
    await runPreflight()
  })
}

Behaviour in ptkl forge dev

Scenario Behaviour
--platform-url provided Opens a full modal overlay with the integrations page loaded from <platform-url>/admin/developer/integrations.
--platform-url not provided Logs a warning and resolves immediately — no overlay is shown.

Availability

$forge is injected by the platform runtime. It is not available outside a Forge app context (e.g. in a standalone browser tab or during server-side rendering).

When developing locally with the Protokol Toolkit's ptkl forge dev command, all methods are shimmed automatically. Pass --platform-url <url> to enable the full overlay implementations.


Error handling

requestPermissions and requestMarketplaceInstall reject the returned Promise when the user dismisses the dialog. Always wrap calls in try/catch (or .catch()) to handle cancellation gracefully:

try {
  await $forge.requestPermissions(['reports.export'])
} catch {
  // User closed the dialog — no action needed
}

openDeveloperIntegrations and openOnboarding always resolve (never reject), so no error handling is required for them. Check the resolved value to determine the outcome:

Never make permissions a hard requirement

Your app should remain usable even if the user declines. Disable or hide the relevant action rather than blocking the entire app.