DEV Community

praveenlavu
praveenlavu

Posted on Originally published at praveenlavu.com

Why We Skipped Packaging for Production

Why We Source-Deployed a Production Salesforce App Before Packaging It

There is a question that comes up the moment you are about to ship a Salesforce app to a real customer org. Not a sandbox. Not a scratch org for a demo. A real production environment with real users who are going to depend on this thing every single day. The question sounds responsible. It sounds like the mature, professional path. "Should we package this before we go live?"

We said no. And I am more confident in that call now than I was when we made it.

What packaging actually commits you to

Second-generation managed packaging in Salesforce is genuinely impressive platform engineering. You get namespace isolation, subscriber org management, controlled upgrade paths, AppExchange eligibility, and real version control over what you ship. For a stable product you plan to distribute across many orgs, it is the right architecture.

But the part that does not get talked about enough is what managed packaging commits you to permanently. Every field you include in a released managed package is a field you cannot delete from subscriber orgs. Every object in your schema is a promise. The platform enforces this through its deprecation policy, and it enforces it for good reason: customers build automations, reports, integrations, and entire workflows on top of your metadata. If you yank a field, you break things they built. So Salesforce makes it structurally difficult to change your mind.

That permanence is a feature when your product is mature and your schema is settled. It is a serious constraint when you are still figuring out what the product actually needs to be.

When we looked at our app before the first production deployment, we knew the core concept was right. We did not know if the data model was right. That distinction matters more than most people admit early in a product's life. The architecture you design before production contact and the architecture production actually demands are almost always different. Sometimes the gap is small. Sometimes it reshapes entire sections of the schema.

The honest question was not "should we package?" It was "do we know enough yet to commit to a schema we cannot change without accruing technical debt that outlives the lesson?" The answer was no. So we did not package.

What production actually teaches you

We deployed via source. Direct metadata push to a real org, running against real data volumes, real user workflows, and real edge cases that no sandbox had surfaced.

The first stretch of production operation was intense in a specific way. Not broken. The app worked. But we kept discovering things that changed our understanding of what the product needed to be. A field we built to carry an important signal turned out to be noise that nobody filled in consistently. A relationship we modeled one way turned out to need different ownership semantics because of how the data actually moved through the workflow. A capability we considered secondary became the one users reached for constantly.

Each of these discoveries was a schema change. In source deployment, a schema change is a refactor you can execute and redeploy in a matter of hours. You update the metadata, push, handle the data migration if needed, and move on. The org stays clean. The team stays fast. The feedback loop stays tight.

If we had been in a managed package, every one of those changes would have opened a negotiation. Do we mark this field deprecated and introduce a replacement? Do we cut a new package version? Do we hold the change until there are enough changes to justify the version overhead? The cognitive load of managing that process is real. The delay compounds. And the debris of deprecated fields kept alive for backward compatibility has a way of turning your schema into an archaeological site.

The ability to keep learning without accumulating debt was not a nice-to-have. It was the foundation that made the product better than it would have been if we had locked the schema before production taught us anything.

The discipline this path actually requires

I want to say something clearly, because source deployment can sound like permission to be loose about change control. It is not.

When you take away the packaging constraint, you have to supply the discipline yourself. Every schema change goes through review. Every modification gets documented and communicated. The deployment pipeline does not do this for you the way a package version process would. The accountability is different. Not to a deprecation policy enforced by the platform, but to the users in that org whose daily work depends on you not breaking anything they depend on.

That responsibility felt appropriate for where we were. The constraint we needed in that stage was not the packaging constraint. It was the production constraint. Real users creating real records, every day, with real consequences when things behaved unexpectedly. That is a sharpening force that no sandbox replicates.

There were moments late at night where a change that looked clean in testing revealed an edge case under real data conditions. You trace it back, find the assumption that was wrong, fix it, redeploy, and wait to see it hold. The focus in those moments is different from anything you feel in a development environment. The stakes are different and the clarity that comes from that is different too.

That is not a pleasant experience to seek out. But it is an honest one. And it made the product more solid than if we had been insulated from production feedback by packaging overhead.

Packaging when it is actually time

None of this is an argument against packaging. Second-generation managed packaging is the right architecture for what we are building toward. The goal has always been a distributable product that can live in multiple orgs, not a single-org custom build.

But the sequence matters more than most conversations about packaging acknowledge.

You package when you know what you are committing to. You package when the schema is stable enough that the deprecation policy becomes a feature you want because it protects your customers rather than a constraint that would have trapped you six months earlier. You package when production has finished teaching you what the data model actually needs to hold.

This is not a Salesforce-specific principle. Every platform has an equivalent version of it. You do not freeze your API contract until the API has survived real use. You do not publish a library until the interfaces are stable. You do not lock a schema until production has told you what the schema actually needs to be.

The temptation to package early is understandable. It feels like the responsible, disciplined path. It looks more mature from the outside. Sometimes it is exactly right. But when your schema is still in active conversation with production reality, packaging is discipline applied to the wrong problem at the wrong time.

Ship when the product is ready. Package when the contract is ready. They are not the same milestone. The engineering judgment that matters is knowing which one you are actually at, even when the responsible-sounding answer is pushing you toward the other one.

Top comments (0)