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.
- Create in Dev Hub: The ISV creates the ECA in their Dev Hub org, generating the global consumer key and secret.
- 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.
- Scratch Org Development: Developers create Scratch Orgs and use the
oauthLinkfield to link their local ECA definition back to the Dev Hub’s global credentials, enabling seamless local testing. - 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).
- 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 Type | Directory | File Suffix | Packageable in 2GP? |
|---|---|---|---|
ExternalClientApplication | externalClientApps/ | .eca-meta.xml | Yes |
ExtlClntAppOauthSettings | extlClntAppOauthSettings/ | .ecaOauth-meta.xml | Yes |
ExtlClntAppGlobalOauthSettings | extlClntAppGlobalOauthSets/ | .ecaGlblOauth-meta.xml | No |
ExtlClntAppOauthConfigurablePolicies | extlClntAppOauthPolicies/ | .ecaOauthPlcy-meta.xml | No |
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:
- Delete any
.ecaGlblOauth-meta.xmlfiles in theextlClntAppGlobalOauthSets/directory. - Delete any
.ecaOauthPlcy-meta.xmlfiles in theextlClntAppOauthPolicies/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.
- Find the
.eca-meta.xmlfile in yourexternalClientApps/directory. - Change the
<distributionState>toPackaged. - Add the
<orgScopedExternalApp>element, formatted asDevHubOrgId: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.
Step 6: Link Scratch Orgs to Global Credentials
To properly test your package during development without creating new external client apps per scratch org, use the Linked ECA strategy:
- Open your OAuth settings file (
.ecaOauth-meta.xml) in theextlClntAppOauthSettings/directory. - 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):
- Go to Setup > App Manager
- Find the installed ECA and click the dropdown arrow > View
- Under OAuth Settings, click Edit Policies
- Check Enable Client Credentials Flow (or configure the allowed user for JWT)
- Set the Run As user to a system user with the appropriate permission set assigned.
- 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:
-
distributionStateis set toPackagedin.eca-meta.xml. -
orgScopedExternalAppis added and references the Dev Hub org ID. -
oauthLinkin local settings points toDevHubOrgId:ConsumerIdfor scratch org reuse. -
.forceignoreexcludes**/extlClntAppGlobalOauthSets/**and**/extlClntAppOauthPolicies/**. - No
ExtlClntAppOauthConfigurablePoliciesfiles exist in the source directory. - No
ExtlClntAppGlobalOauthSettingsfiles 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:
- 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. - Strict Metadata Segregation: Packaged Connected Apps bundled globals (callbacks, credentials) and local policies (assigned profiles) into one fragile
.connectedappfile. 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.