DEV Community

Appnigma
Appnigma

Posted on

Building a Salesforce Managed Package for Your SaaS: 9 Gotchas Nobody Warns You About

If your B2B SaaS sells to companies that run on Salesforce, sooner or later a customer will ask: "Do you have an AppExchange app?"

Saying yes means building a managed package, and the docs make it sound straightforward. It is not. Our team has watched engineering teams burn entire quarters on this. Here are the gotchas that actually cost time, in the order you will hit them.

  1. Your namespace is forever Before you write a line of Apex, you register a namespace: a 1 to 15 character prefix stamped onto every class, object, and field in your package (acme_Invoice_c, acme.PaymentService).

The gotcha: it is permanent. You cannot rename it, and you cannot migrate an existing package to a new one. Teams that register acmetest "just to try things out" and then ship with it regret it in every API call for the life of the product. Register the real one on day one, in a dedicated Developer Edition org that you never delete.

  1. 1GP vs 2GP is not a real choice, but the docs pretend it is Salesforce documentation still describes two packaging models: first-generation (1GP) and second-generation (2GP). Ignore 1GP entirely. It is legacy, tied to a single packaging org, and does not support modern source-driven development.

For 2GP, everything runs through the Salesforce CLI against a Dev Hub:

Create the package (once)

sf package create \

--name "Acme Connector" \

--package-type Managed \

--path force-app \

--target-dev-hub DevHub

Create a new version (every release)

sf package version create \

--package "Acme Connector" \

--installation-key-bypass \

--wait 30 \

--code-coverage

That --code-coverage flag matters: you cannot promote a package version to released status unless Apex test coverage is at least 75%, computed at package version creation, not install time.

  1. Scratch orgs lie to you 2GP development happens in scratch orgs, which are ephemeral and clean. Your subscribers' orgs are neither. A package that installs perfectly into a scratch org can fail in a real org because of:

Person Accounts being enabled (changes the Account object model)
Multi-currency (adds CurrencyIsoCode everywhere)
State and Country picklists
Existing validation rules and triggers firing on your inserts

Test installs into orgs with these features enabled. The scratch org definition file lets you simulate most of them:

{

"orgName": "Acme Test - Person Accounts",

"features": ["PersonAccounts", "MultiCurrency"]

}

  1. Once you ship a global class, you own it forever Anything marked global in a released managed package version is a public API contract. You cannot delete it, cannot change its signature, and in most cases cannot even delete global methods, ever. Deprecation exists (@deprecated) but the symbol still ships in every future version.

Design your API surface as if every global keyword costs $10,000. In practice it costs more.

  1. The security review is a project, not a checkbox Every AppExchange listing goes through Salesforce's security review. Plan for weeks, not days, and budget for at least one rejection on your first submission. The most common failure reasons we see:

CRUD/FLS violations: every SOQL and DML operation must check object and field-level permissions. WITH USER_MODE in SOQL and Security.stripInaccessible() are your friends.
Hardcoded secrets: API keys in custom settings or, worse, in code. Use Protected Custom Metadata or Named Credentials.
External endpoint findings: your SaaS backend gets scanned too. A stray verbose error page or missing security header on your API can fail the package review.

Run Salesforce Code Analyzer with the AppExchange rule set locally before submitting. It catches most of what the review team will flag.

  1. Callouts to your API need Named Credentials, not Remote Site Settings Your package almost certainly calls your SaaS backend. The pattern reviewers want to see is Named Credentials plus External Credentials, so authentication config lives outside your code and admins can rotate secrets without a package update:

HttpRequest req = new HttpRequest();

req.setEndpoint('callout:Acme_API/v1/sync');

req.setMethod('POST');

// No auth headers in code. The Named Credential injects them.

Shipping Remote Site Settings with hardcoded OAuth flows is the fastest way to a security review rejection in 2026.

  1. Governor limits are shared with everyone else's code Your Apex runs in the same transaction as the subscriber's own triggers, flows, and every other installed package. You do not get your own limits for synchronous transactions in most contexts. That elegant trigger that works fine alone will blow up when a subscriber's org has three other packages inserting the same records.

Bulkify everything, assume batches of 200, and push anything heavy into Queueables or Platform Events.

  1. Push upgrades can brick your customers 2GP supports push upgrades: you push a new version to all subscriber orgs without them clicking anything. It is powerful and terrifying. A bad push lands in every customer org simultaneously, including the Fortune 500 account your CEO just closed.

Treat push upgrades like a production deploy to every customer at once, because that is what they are. Stage rollouts, keep versions ancestry-clean (--ancestor-version in your project config), and never push on a Friday.

  1. The whole thing is a parallel engineering discipline Here is the honest summary: a managed package is not "an integration." It is a second product, in a different language (Apex), on a different runtime, with a different release process, deployment model, and review gate. Most B2B SaaS teams either hire Salesforce specialists, contract a PDO (Product Development Outsourcer) for six figures, or park the AppExchange plan indefinitely.

That gap is exactly what we are working on at Appnigma AI: generating production-ready native Salesforce managed packages from natural-language prompts, so SaaS teams can ship to the AppExchange without building a parallel Salesforce engineering function. (Disclosure: I work on Appnigma. This article is the checklist we wish existed when we were shipping packages by hand as ex-Salesforce engineers.)

The short version

Register your real namespace on day one. It is permanent.
Use 2GP. Pretend 1GP does not exist.
Test installs in orgs with Person Accounts and Multi-Currency enabled.
Treat global as a lifetime commitment.
Start security review prep before you finish the code, not after.
Named Credentials for callouts, always.
Bulkify for 200 records and shared governor limits.
Respect push upgrades like production deploys.
Budget for a parallel discipline, or find a way to generate it.

If you have shipped a managed package, what caught you off guard? The weirdest gotchas always come from subscriber orgs doing things no one anticipated. Drop yours in the comments.

Top comments (0)