All Articles
Salesforce March 25, 2026

Packaging External Client Apps in 2GP: A Complete How-To Guide

Packaging External Client Apps in 2GP: A Complete How-To Guide

Salesforce’s External Client Apps (ECAs) are the better-architected replacement for Connected Apps. They offer better secret management, operational controls, and a cleaner separation between developer-owned settings and subscriber-owned policies. If you are building a new OAuth integration - especially one destined for a managed package - ECAs are no longer just the right choice; they are the only choice.

This guide walks you through the exact steps to successfully package an ECA for global ISV distribution, eliminating the guesswork and common deployment errors.


The Use Case

While the core principles in this guide apply to all OAuth flows (like the interactive Web Server flow), we will focus specifically on packaging ECAs for server-to-server integrations. For ISVs, this typically means using either the Client Credentials flow or the JWT Bearer flow.

Both of these headless, server-to-server flows introduce a specific packaging hurdle: the requirement to configure an execution user (a “Run As” user) that won’t exist in the packaging org. Because of this, you must carefully exclude certain metadata files during the 2GP packaging process.

In our example, the packaged app authenticates a backend service (e.g., an external billing system) to subscriber Salesforce orgs. When a subscriber installs the package, the ECA is included. The ISV securely holds the global consumer key and secret/certificate; the subscriber only needs to assign a Run As user and grant a permission set.

This is the “global ECA” model - one app definition distributed to many orgs via a managed package, with a single set of credentials managed by the ISV.


The Architecture: Setting Up a Global ECA

Here is the ideal workflow for an ISV distributing a “Global” External Client App. The goal is to maintain a single consumer key and secret on the Dev Hub, which are securely replicated to every subscriber org that installs the package.

  1. Create in Dev Hub: The ISV creates the ECA in their Dev Hub org, generating the global consumer key and secret.
  2. Package Metadata: The packageable metadata (basic definition and OAuth settings) is added to the 2GP managed package. The global credentials and auto-generated policies are excluded.
  3. Scratch Org Development: Developers create Scratch Orgs and use the oauthLink field to link their local ECA definition back to the Dev Hub’s global credentials, enabling seamless local testing.
  4. Subscriber Installation: Subscribers install the 2GP package. They receive the ECA definition and configure their local policies (such as the Run As user for client credentials flow).
  5. Global Replication: Behind the scenes, Salesforce’s secure global replication automatically distributes the global credentials from the Dev Hub to the subscriber orgs.
graph TD
    subgraph DevHub [Dev Hub Org]
        GlobalECA[Global External Client App]
        GlobalCreds[Global Consumer Key & Secret]
        GlobalECA -.-> GlobalCreds
    end

    subgraph ISVDev [ISV Development]
        ScratchOrg[Scratch Org]
        ScratchLocalECA[Local ECA Definition]
        ScratchOrg --> ScratchLocalECA
        ScratchLocalECA -- oauthLink --> GlobalCreds
    end

    subgraph SubA [Subscriber Org A]
        SubA_ECA[Installed ECA]
        SubA_Policies[Local OAuth Policies & Run As]
        SubA_ECA --> SubA_Policies
    end

    subgraph SubB [Subscriber Org B]
        SubB_ECA[Installed ECA]
        SubB_Policies[Local OAuth Policies & Run As]
        SubB_ECA --> SubB_Policies
    end

    GlobalECA == 2GP Packaging & Installation ===> SubA_ECA
    GlobalECA == 2GP Packaging & Installation ===> SubB_ECA

    GlobalCreds -. Secure Global Replication .-> SubA_ECA
    GlobalCreds -. Secure Global Replication .-> SubB_ECA

This architecture is powerful. It completely removes credential management from the subscriber’s plate, while giving the ISV centralised control. Here are the step-by-step instructions to implement it.


Step 1: Understand the Four Metadata Types

An ECA is not a single metadata file. It spans four distinct metadata types across four directories, and each has different packaging rules. Only two of them can be packaged.

Metadata TypeDirectoryFile SuffixPackageable in 2GP?
ExternalClientApplicationexternalClientApps/.eca-meta.xmlYes
ExtlClntAppOauthSettingsextlClntAppOauthSettings/.ecaOauth-meta.xmlYes
ExtlClntAppGlobalOauthSettingsextlClntAppGlobalOauthSets/.ecaGlblOauth-meta.xmlNo
ExtlClntAppOauthConfigurablePoliciesextlClntAppOauthPolicies/.ecaOauthPlcy-meta.xmlNo

This distinction is by design:

  • Global OAuth settings (consumer key, secret, callback URLs, PKCE config) are stored on the Dev Hub and replicated to subscriber orgs automatically via Salesforce’s global replication mechanism.
  • Configurable policies (Run As user, IP relaxation, refresh token config) are subscriber-managed. They are auto-generated with defaults when the ECA is installed.

Step 2: Retrieve ECA Metadata from the Dev Hub

First, create your External Client App manually in the Dev Hub org via Setup. Then, retrieve it using the Salesforce CLI.

Option A: Using the —metadata Flag (The Case-Sensitivity Trap)

The CLI --metadata flag requires all-lowercase type names for ECAs. Using the PascalCase API names silently returns zero results.

# Retrieve the packageable types
sf project retrieve start --metadata externalclientapplication --target-org DevHub
sf project retrieve start --metadata extlclntappoauthsettings --target-org DevHub

Option B: Using a Manifest File

If you prefer to use a package.xml manifest, you must use the standard PascalCase API names:

<?xml version="1.0" encoding="UTF-8" ?>
<Package xmlns="http://soap.sforce.com/2006/04/metadata">
  <types><members>*</members><name>ExternalClientApplication</name></types>
  <types><members>*</members><name>ExtlClntAppOauthSettings</name></types>
  <version>64.0</version>
</Package>
sf project retrieve start --manifest manifest/package-eca.xml --target-org DevHub

Step 3: Configure .forceignore for 2GP

You must explicitly exclude the non-packageable metadata directories in your .forceignore file. If these are packaged, the build will fail.

Add the following to your .forceignore:

# Global OAuth settings are distributed via global replication from the Dev Hub.
# Configurable policies (Run As user, IP relaxation) are subscriber-managed
# and auto-generated when the ECA is installed.
**/extlClntAppGlobalOauthSets/**
**/extlClntAppOauthPolicies/**

Note on naming: The directories are abbreviated (extlClntAppGlobalOauthSets, not Settings). Match these exactly.


Step 4: Clean Up Source Files

If you accidentally retrieved all four ECA metadata types, you must delete the non-packageable ones from your local source folder before packaging:

  1. Delete any .ecaGlblOauth-meta.xml files in the extlClntAppGlobalOauthSets/ directory.
  2. Delete any .ecaOauthPlcy-meta.xml files in the extlClntAppOauthPolicies/ directory.

Leaving these policies in your source directory will trigger the Enter a valid execution user for the OAuth client credentials flow error during packaging, because the policy file tries to assign a specific user (which doesn’t exist in the packaging scratch org).


Step 5: Update the ECA Definition File

Because you retrieved the custom ECA from your Dev Hub org, if the retrieved metadata has a Local state, change it to Packaged. You must prepare the main definition file (.eca-meta.xml) for packaging.

  1. Find the .eca-meta.xml file in your externalClientApps/ directory.
  2. Change the <distributionState> to Packaged.
  3. Add the <orgScopedExternalApp> element, formatted as DevHubOrgId:AppDeveloperName.

Your file should look like this:

<ExternalClientApplication xmlns="http://soap.sforce.com/2006/04/metadata">
    <contactEmail>hello@example.com</contactEmail>
    <description>External Billing Integration</description>
    <distributionState>Packaged</distributionState>
    <isProtected>false</isProtected>
    <label>External Billing Integration</label>
    <orgScopedExternalApp>00DXX0000XXXXXX:External_Billing_Integration</orgScopedExternalApp>
</ExternalClientApplication>

Tip: To get your 18-character Dev Hub Org ID, run sf org display --target-org DevHub.


To properly test your package during development without creating new external client apps per scratch org, use the Linked ECA strategy:

  1. Open your OAuth settings file (.ecaOauth-meta.xml) in the extlClntAppOauthSettings/ directory.
  2. Add the <oauthLink> element pointing to your Dev Hub’s Org ID and the ECA’s Global Consumer ID.
    <oauthLink>00DXX0000XXXXXX:888RE000000XXXX</oauthLink>

When you deploy your source code to a scratch org, the ECA will now “link” back to the Dev Hub. The scratch org will show the exact same Consumer Key as your production environment, allowing instantaneous API testing.


Step 7: Create the 2GP Package Version

You are now ready to create your package version:

sf package version create -p "My App" -x -w 20

Note on Beta Upgrades

If you need to iterate on your beta package and create new versions, remember that beta packages cannot be upgraded in place. You must uninstall the previous beta before installing the new one:

sf package uninstall -p 04tXXXXXXXXXXXXX -o TestOrg --wait 15
sf package install -p 04tYYYYYYYYYYYYY -o TestOrg --wait 15 --no-prompt

Uninstalling can fail if Lightning Pages reference your components or if permission sets are assigned to users, so you will need to remove those references first.


Step 8: Subscriber Post-Install Configuration (For Server-to-Server Flows)

After installing the managed package, subscribers need to perform a single step to manually activate the client_credentials (or JWT) flow. (If you are packaging interactive OAuth flows, this specific user-assignment step is usually unnecessary):

  1. Go to Setup > App Manager
  2. Find the installed ECA and click the dropdown arrow > View
  3. Under OAuth Settings, click Edit Policies
  4. Check Enable Client Credentials Flow (or configure the allowed user for JWT)
  5. Set the Run As user to a system user with the appropriate permission set assigned.
  6. Click Save

No consumer key or secret needs to be copied. No callback URLs or IP relaxations need to be manually configured. The global credentials handle the authentication securely and automatically.


Summary: Pre-Packaging Checklist

Before running sf package version create, check off these items verify your metadata is completely prepared:

  • distributionState is set to Packaged in .eca-meta.xml.
  • orgScopedExternalApp is added and references the Dev Hub org ID.
  • oauthLink in local settings points to DevHubOrgId:ConsumerId for scratch org reuse.
  • .forceignore excludes **/extlClntAppGlobalOauthSets/** and **/extlClntAppOauthPolicies/**.
  • No ExtlClntAppOauthConfigurablePolicies files exist in the source directory.
  • No ExtlClntAppGlobalOauthSettings files exist in the source directory.

Final Thoughts

Salesforce no longer allows the creation of new Connected Apps, making External Client Apps mandatory for new integrations. Even for existing apps, the ECA model provides two massive architectural improvements for ISV distribution:

  1. No More Dummy Logins: Historically, unpackaged headless integrations (client_credentials) required admins to perform dummy web logins just to force the app to appear in their org. Packaged ECAs deploy instantly and are ready for configuration out of the box.
  2. Strict Metadata Segregation: Packaged Connected Apps bundled globals (callbacks, credentials) and local policies (assigned profiles) into one fragile .connectedapp file. ECAs decouple these, ensuring your package upgrades never accidentally overwrite a subscriber’s custom security configurations.

By keeping the right metadata types packageable, configuring your .forceignore to exclude the rest, and linking your scratch orgs appropriately - you can achieve a frictionless, single-click installation for your customers.