Your managed package passes every test. Every scratch org run comes back clean, every Apex test asserts what it should, and the whole thing sails through code review. You ship it to AppExchange. A week later, a subscriber's admin files a case: a sharing error on a record that your test user could always see, a null field that your code assumed would be there, a permission failure that a licensed end user hits on their first login. Nothing in your testing predicted any of it.
The confusion usually starts with a simpler question: Salesforce scratch org vs sandbox, which one should you actually build and test in? That comparison matters, but it can't explain a bug like this on its own, because neither a scratch org nor a sandbox is the environment your code has to survive. That's a subscriber org, your customer's live production Salesforce org, with its own edition, its own data, its own other packages, and a user who isn't you.
This guide explores all three environments, the use case each one actually serves, and the testing sequence that catches the gap before your customers do.
Key Takeaways
- A scratch org, a sandbox, and a subscriber org solve three different problems, not one continuum. Testing in the first two and shipping into the third is why "it worked when I tested it" keeps showing up in ISV support tickets.
- Namespace injection, Salesforce silently rewriting your packaged references at build time, is one of the biggest sources of code that behaves differently after install.
- A subscriber org runs your Apex in the same transaction as every other package and customization already installed there, so governor limits your isolated scratch org never approached become real.
- Sharing rules, org-wide defaults, and permission set licenses aren't shipped with your code. Someone configures them per org, and a mismatch changes what your package can see and do.
- The sequence that catches most of this before a subscriber does: build in a namespaced scratch org, install-test in a non-namespaced one, simulate a subscriber with real data and a non-admin user, then run UAT and your security scans.
What's the Difference Between a Salesforce Scratch Org and a Sandbox?
A sandbox is a copy of an org you already have. A scratch org is a disposable environment built from source, with nothing in it until you put it there. For an in-house team doing everyday Salesforce development, that difference usually decides where to build a new feature. For a Salesforce ISV running complex projects toward AppExchange, it's only the first of three environments to keep straight: sandbox, scratch org, and eventually the subscriber org your customer actually runs.
Sandboxes: What Each Type Actually Copies
Salesforce sandboxes come in four types, and they don't all copy the same thing:
A Developer sandbox gives you a clean copy of your org's configuration with no records in it. A Full Copy sandbox copies all the metadata and all the data, which is exactly why it's slower to create and refresh, and why it's the closest thing to real production environments you can get without touching production itself. Sandboxes don't expire on their own. They persist until you refresh or delete them, which also means an old sandbox can quietly accumulate stale configuration and outdated test data over months of use.
Scratch Orgs: Built From a Definition File, Not From Production
Here's how scratch orgs work, and why they're built so differently from a sandbox. A scratch org isn't a copy of anything. You define it with a scratch-org-definition-file, a fully configurable JSON file that lets you pick from Salesforce's different editions, enable different features, and have Salesforce spin up the org on demand via the Salesforce CLI. Most Salesforce developers pick up that CLI without much extra training.
That configurability (editions, features, settings) is what makes a scratch org different from a sandbox in the first place. Salesforce DX, the broader source-driven development model scratch orgs belong to, treats your Git repository, your version control system, as the source of truth and the org itself as disposable. Scratch orgs expire in 1 to 30 days depending on how you configure them, and once one is gone, it's gone.
Why You Refresh a Sandbox but Recreate a Scratch Org
In practice, this is the philosophical difference that trips people up. You refresh a sandbox because it's meant to persist and stay close to production over time. You never refresh a scratch org. When it expires, or you're done with a piece of work, you throw it away and rebuild it from source control. That's not a limitation; it's the point: version control is the cornerstone of scratch org development, and your Git history and scratch-org-definition-file are what's authoritative, not whatever state a particular org happens to be in.
Why Neither One Is "the Subscriber Org"
Here's where the standard scratch org vs sandbox comparison runs out of road for a Salesforce ISV. Both environments give you agility for day-to-day development, but agility isn't the same as an accurate read on what a subscriber will actually experience. Both environments are yours to control. A subscriber org isn't. It belongs to your customer, running whatever Salesforce edition they bought long before your package existed, already carrying other packages, other customizations, and real business data your scratch org never had to account for. That's the real test of your package's ability to hold up outside your own control.
What Is a Subscriber Org, and Why Doesn't a Clean Scratch Org Test Simulate It?
A subscriber org is the org your customer actually uses once they've installed your package. It has its own data, its own licensed users, its own Salesforce edition, and years of configuration your scratch org started without. Your package relies on assumptions about all of that, whether you wrote them down or not, and testing only in an environment you built yourself means testing it in the one place it will never run again. The rest of this guide helps you identify exactly where those assumptions break.
Your Admin Test User Hides What a Real Subscriber User Will Hit
Most scratch org testing happens as a system administrator, because that's the default user Salesforce gives you. An admin bypasses sharing rules, field-level security, and profile-based UI restrictions without even noticing. A subscriber's licensed end user doesn't have that luxury. A field visible to you in every test can be hidden from them by a permission set they were assigned. A record reachable in your admin session can be excluded for them by a sharing rule that an admin configured for reasons unrelated to your package.
Subscriber Orgs Run Whatever Salesforce Edition the Customer Bought
Different Salesforce editions ship with different capabilities, and a scratch org definition file can enable features that your customer's org might not have. Multi-Currency, Person Accounts, and State and Country Picklists all change the underlying data model, and plenty of real subscriber orgs on Group, Professional, or even Enterprise Edition don't have them enabled. If your Apex or Lightning Web Component quietly assumes one of those features exists, it can pass every scratch org test and still throw an error the moment a subscriber on a plainer edition installs it.
A Subscriber Org Already Has Other Packages Competing for the Same Limits
A fresh scratch org runs your package in isolation. A subscriber org is already running other AppExchange apps, triggers, and flows the admin built years ago, all sharing the same governor limits, the finite resources Salesforce caps per transaction that your code has to fit inside. That's the next place things quietly diverge.
Why Does Namespace Injection Break Code That Worked Fine in Development?
Salesforce rewrites every packaged reference in your code with your namespace prefix at build time, silently and automatically. You never type the prefix yourself, but a subscriber org sees it on every object, field, Apex class, and Lightning Web Component your package delivers. Code that assumes no prefix, or a scratch org that applies one inconsistently, is where many "it worked in dev" bugs come from.
How Namespace Injection Actually Works
If you're curious how Salesforce actually pulls this off, here's the mechanism. Say your source code inserts a record like this:
insert new Invoice__c(Amount__c = 100);
Once Salesforce packages that code under a namespace, say abc, the compiled version a subscriber org actually runs looks like this:
insert new abc__Invoice__c(abc__Amount__c = 100);
Salesforce rewrites every internal reference this way at packaging time: object names, field names, Apex class references, and Lightning Web Component references all get the prefix applied automatically, according to a detailed technical breakdown from Greytrix on 2GP namespace behavior. Your source repository never shows the namespace. The subscriber org always does.
Why Salesforce Recommends a Namespaced Scratch Org for Development
Salesforce's own guidance on org strategy for ISVs is direct about this. Its Org Recommendations for ISVs post advises developers to "use namespaced scratch orgs for development," because "the namespace prevents issues in packaging metadata later on," and to switch to non-namespaced scratch orgs specifically "for testing package installation and upgrade." The benefits show up early: namespace bugs surface while you're still writing the code, not after a subscriber installs it. Developing without a namespace at all means you're writing code that has never once run the way a subscriber will actually experience it.
Where Teams Still Get Bitten
Even teams that follow this advice can run into trouble. A CumulusCI engineering account from Salesforce.org describes exactly this: as their automation recipes grew more complex, they hit "increasing risk of encountering issues where the automatic namespace prefixing in namespaced scratch orgs causes build or test failures," serious enough that the team built custom tooling to work around it. Dynamic Apex, calls to Schema.getGlobalDescribe(), and any hardcoded object or field reference are the usual places this shows up, because they're the places your code has to know its own namespace to work correctly.
Why Do Governor Limits Behave Differently Once Your Package Is Live?
Salesforce enforces governor limits per transaction, across everything running inside it, not per package. A scratch org with only your code installed will never hit a limit your code alone doesn't approach. A busy subscriber org, running your package alongside three other AppExchange apps and a decade of custom automation, can hit that same limit in a transaction your code only partly contributes to.
What a Scratch Org's Isolation Hides From You
This is the tradeoff of a clean testing environment. A scratch org is an isolated environment by design, which is exactly what makes it fast and predictable, and exactly why it's the right place to build. It's also why a scratch org can't tell you what happens when your trigger fires in the same transaction as five other triggers a subscriber's admin wrote, all competing for the same SOQL query rows, CPU time, and heap space.
How MagicFuse Engineered Around This for Elements.cloud
We saw this directly while building the AppExchange managed package for Elements.cloud, a B2B SaaS platform that helps companies visualize and document their business processes. The package needed to sync millions of Salesforce records to an external application in near real time, well past what a straightforward implementation could do inside Salesforce's native limits.
We built a custom logging system and optimized the underlying queries to work around those limits at production scale, and the resulting package passed every security review, from Salesforce and from the external security firms Elements.cloud engaged separately. A scratch org holding a handful of test records would never have surfaced that constraint. A subscriber's real data volume did.
What Doesn't Ship With Your Package, and Who Configures It in the Subscriber Org?
Some of what makes your package work in your test environment isn't part of what you deploy at all. Organization-wide sharing defaults, the baseline access level for each object, are an org-level security setting, not something a managed package installs on a subscriber's behalf. The subscriber's admin sets it directly, the same way they would for any object built in-house, and if they set it more restrictively than your own test org, your code sees less than it did when you tested it.
Org-Wide Defaults and Sharing Rules Are an Org Setting, Not a Package Setting
If your Apex assumes it can read or write a record because your scratch org's sharing model was wide open, a subscriber org with a stricter default can silently block that same operation. This is one of the more common causes of a permission error that never once appeared during development, because your development org's sharing model was never the thing being tested.
Permission Set Licenses and Post-Install Scripts as the Safety Net
Permission set licenses control which subscriber users can even access your package's features, and a subscriber who assigns them differently than you expected gets a different experience than the one you tested. A post-install Apex script, run automatically the moment a subscriber installs or upgrades your package, is the tool Salesforce gives you to configure defaults on their behalf instead of leaving every setting to chance.
How Do You Actually Test a Package So It Survives a Subscriber Org?
Catching these gaps takes more than one more scratch org test. It takes a sequence of environments that each emulate a real subscriber a little more closely than the last.
Step 1: Build in a Namespaced Scratch Org
Do your day-to-day development in a namespaced scratch org, matching your packaging org's namespace. This is where dynamic Apex and hardcoded references get caught early, while they're still cheap to fix, which protects your team's productivity far more than catching the same bug after a subscriber reports it. Because your Dev Hub also caps how many active scratch orgs you can have running at once, keep this step moving rather than letting several pile up.
Step 2: Install-Test in a Non-Namespaced Scratch Org
Once a package version is built and ready for deployment, install it into a separate, non-namespaced scratch org and confirm it installs cleanly, assigns permission sets correctly, and functions the way an end user would experience it. A namespace conflict error at this step is telling you something you need to know before a subscriber ever sees it.
Step 3: Simulate a Subscriber With Real Data and a Non-Admin User
Before you submit anything, install your package into an environment built to replicate a subscriber org more closely than a scratch org ever will: a Partner Business Org, a Partner Developer Edition org, or even a plain sandbox or trial org, loaded with a realistic data volume, a representative Salesforce edition, a couple of other installed packages, and a test user on a standard, non-admin profile. This single step catches more edition, sharing, and governor-limit surprises than any amount of additional scratch org testing.
Step 4: Run UAT, Then Code Analyzer and Checkmarx
Have real business users, on the roles they'll actually hold, work through your package's core flows as part of a proper UAT pass, not just your own admin login one more time. Once that's clean, run Salesforce Code Analyzer and the Checkmarx-powered Source Code Scanner before you submit to Security Review. If you want a deeper look at building this into an ongoing testing process rather than a one-time pre-submission scramble, our guide to Salesforce test automation covers what to automate and what to leave to a human tester as part of a broader Salesforce DevOps process.
How Has MagicFuse Helped ISVs Ship Packages That Hold Up in Subscriber Orgs?
We've built and maintained managed packages for Salesforce ISVs for 12+ years, and getting a package to behave the same way in a subscriber org as it does in development is a normal part of that work.
Riptide: A 2GP Managed Package Built for Two Clouds
For Riptide, a text messaging platform that connects customers, drivers, and dispatchers in the automotive industry, we built a second-generation (2GP) managed package spanning Service Cloud and Experience Cloud, including a live chat capability orchestrated by a generative AI agent. Getting a 2GP package to behave consistently across the Service Cloud and Experience Cloud contexts a subscriber actually runs it in was part of the project from day one, not an afterthought.
Elements.cloud: Engineering Around Salesforce's Own Limits
As covered above, the managed package we built for Elements.cloud had to sync millions of records at a scale that only showed up once real subscriber data volume was in play. Passing every security review Salesforce and Elements.cloud's own external firms ran was the result of testing against that real-world scale, not just a clean scratch org.
More ISVs We've Built Managed Packages For
We've built and shipped managed packages for other named AppExchange ISVs too: Limio, an enterprise subscription commerce platform, ID-Pal, an identity verification provider, and Surfly, a co-browsing and interaction middleware platform. We've also worked inside an existing AppExchange package for Ascent Solutions, a logistics management platform, building calendar components and Lightning enhancements into their live subscriber base.
If you'd rather have a team that's already run this sequence than work it out yourself before your next submission, get in touch with our Salesforce developers, and we'll help you get there.
Where This Leaves You
The package that passed every scratch org test and still generated support tickets wasn't a testing failure in the way most teams assume. It was a gap in which environment got tested at all. A scratch org and a sandbox tell you your code works. Only something closer to a real subscriber org, with its own edition, its own data, and a user who isn't an admin, tells you it will keep working once it's out of your hands.
Namespace injection, shared governor limits, and unpackaged sharing settings are specific, predictable reasons for that gap, not bad luck. Test for them in that order, before Security Review does it for you. If you'd rather have a Salesforce team that has run this sequence for other AppExchange ISVs than work it out on your next release, our developers can help you get there.
FAQs
- What is a subscriber org in Salesforce?
A subscriber org is the org a customer runs after installing your managed package. It's a real, live production org with its own data, users, Salesforce edition, and other installed packages, none of which your scratch org or sandbox has by default.
- Is a scratch org the same as a sandbox?
No. A sandbox is a copy of an existing org, including its metadata and sometimes its data, and it persists until refreshed or deleted. A scratch org is built from scratch using a definition file and the Salesforce CLI, starts empty, and expires after 1 to 30 days.
- Why does my managed package fail after installing it in a new org, when it passed every test?
The most common causes are namespace prefixing your development environment didn't fully exercise, a Salesforce edition or feature gap between your test org and the subscriber's, governor limits that only appear once your code runs alongside a subscriber's other packages, and sharing or org-wide default settings the subscriber configured differently than your test org.
- Do I need a namespaced scratch org?
Yes, for development. Salesforce recommends building in a namespaced scratch org so namespace-related issues surface while you're writing the code, then testing installation and upgrades in a separate non-namespaced scratch org.
- What's the difference between scratch org, sandbox, and subscriber org, in one sentence each?
A scratch org is a disposable environment you build from source for development. A sandbox is a persistent copy of an org you already control. A subscriber org is your customer's live production org, running your installed package under conditions you don't control.
- How long does a scratch org last before it expires?
Between 1 and 30 days, depending on how you configure it in the scratch-org-definition-file. It can't be refreshed. Once it expires, you rebuild it from source control.









