DEV Community

praveenlavu
praveenlavu

Posted on Originally published at praveenlavu.com

The LWC CSP Wall Nobody Warned You About

The CSP Wall Nobody Warned You About

The component worked. It passed review, it passed testing, it passed QA on every sandbox I had access to. Then it shipped and a portion of the customer base started reporting that a specific interaction just stopped working. The other customers had no issues. Same component. Same package version. Different orgs.

I spent what felt like a long time trying to reproduce it. Swapping API versions, checking feature flags, comparing org configurations side by side. The logs were almost useless. The behavior wasn't a clean crash with a stack trace; it was a silent failure, the kind where the UI just stopped responding with no indication of why.

What I was actually looking at was a runtime problem. Not a bug in my code, exactly. A collision between two fundamentally different security models that the platform runs simultaneously, and the place where those models diverge happened to be exactly where my code did what it did.

That is the CSP wall. And almost nobody writes about what it actually feels like to hit it.

Two Runtimes, One Platform

Salesforce runs your Lightning Web Component JavaScript inside a security container. The purpose is isolation: preventing components from reaching outside their sandbox to do things the platform should not allow. The container enforces Content Security Policy, restricts certain DOM operations, and controls how your JavaScript interacts with the browser's native APIs.

The catch is that there are two of these containers, with meaningfully different behaviors. The older one, Locker Service, was the first serious attempt at this kind of isolation. It wraps DOM elements in proxy objects, so when your code accesses a native element, it's actually talking to a secured wrapper that filters what you're allowed to do. This worked, it shipped to production orgs everywhere, and a generation of ISV developers wrote code assuming this model was the whole story.

Then Lightning Web Security arrived as a next-generation approach, built on a different architectural philosophy. Instead of proxy wrappers, it uses the browser's own capabilities for sandboxing, a more principled design that aligns better with how modern JavaScript engines actually work. It's stricter in some places, more permissive in others, and it handles CSP enforcement differently at a fundamental level.

The problem for anyone shipping a managed package to a broad customer base is that both are real. Depending on an org's settings, API version, and feature enablement, your component runs in one or the other. You don't get to choose. The org chooses.

The Shape of the Wall

What this means in practice is that code patterns which pass cleanly under one runtime can hit hard stops under the other. The wall does not announce itself. Your component does not throw an error that says "CSP violation detected under Lightning Web Security." Instead you get behavior differences that look like flaky tests, like environment quirks, like someone has a corrupted browser cache or a plugin conflict.

The patterns that most reliably create this problem are the ones that touch JavaScript in ways that aren't purely declarative. Dynamic evaluation, runtime script construction, certain ways of reaching into the internals of third-party libraries, certain approaches to DOM manipulation that assume a specific relationship between your JavaScript context and the browser's native layer. These are exactly the patterns that experienced front-end engineers reach for naturally, because they're the ones that have historically given you power and flexibility on the open web.

Under the stricter runtime, that power is precisely the problem. CSP exists to prevent the kind of dynamic behavior that enables certain attack vectors, and the security container is not designed to distinguish between "this dynamic thing is fine, I promise" and "this dynamic thing is a potential injection surface." It enforces the policy because that is what it is for.

The most frustrating version of this is when you're integrating a third-party library, a visualization component, a charting tool, anything that was designed for the unrestricted browser environment and has its own ideas about initialization. Libraries like this often do things internally that are entirely normal in an open context but that land hard against a CSP wall. You didn't write the problematic patterns. You're just importing a dependency. And now you're the one debugging why it silently fails in a subset of customer orgs that you can't easily access or replicate.

What Actually Gets You Through

The shift that finally made this tractable for me was not finding a clever way to detect which runtime is active and branch accordingly. That path leads somewhere bad: a proliferation of conditional logic, an ever-growing matrix of runtime-specific workarounds, a codebase nobody can reason about six months later. It also fails eventually, because the detection surface is not stable across platform releases.

The shift was treating the strictest interpretation of CSP as the only target. Not "will this pass Locker Service?" Not "will this pass Lightning Web Security?" Instead: does this do what I need using only capabilities that a maximally strict policy would allow? When you frame it that way, the question gets simpler. You're not navigating between two sets of rules. You're writing to a single bar that both runtimes must pass.

In practice that means no dynamic evaluation in any form. No patterns that construct and execute JavaScript at runtime. No assumptions about what the global scope looks like or what native APIs are directly reachable. No importing external libraries without verifying that they were designed with strict CSP in mind, or without wrapping them in an abstraction that keeps the unsafe internals isolated from your component's execution context.

It also means: declarative over imperative, everywhere you can achieve it. Build behavior into reactive properties and event handlers rather than reaching out at runtime to touch things imperatively. The reactive model that Lightning Web Components is built on exists partly because it is fundamentally more compatible with security isolation than imperative DOM manipulation. Working with it rather than around it is not a constraint you're accepting reluctantly. It's the design expressing its intent.

The payoff surprised me. When you get this discipline right, the component becomes more maintainable, not less. The strictest-common-denominator approach forces you to reason clearly about what the component actually needs to do versus what is incidental complexity that accumulated from years of writing in environments without these restrictions. When the platform enforces a discipline, you stop accumulating that debt.

The Insight That Doesn't Show Up in the Docs

What nobody told me explicitly, but what I had to work out through a series of painful episodes across orgs I had no local access to, is that the dual-runtime situation is not a transitional state on the way to one unified model. It is the operational reality for any developer shipping broadly on this platform. The variance is not going away. Different customers move at different paces, have different features enabled, have made different configuration decisions at the org level. You are always shipping to a heterogeneous environment, and you are always the last one to find out what that environment looks like.

This is a harder version of the general problem of writing code that works across environments. It's harder because the environments are not fully documented, because the behavioral differences are subtle enough to miss in standard testing, and because the failure modes are often silent rather than loud. The org that's breaking your component is not generating an obvious error you can act on. It's just not doing the thing you expected.

The answer is not to master both runtimes deeply enough to code specifically to each. The answer is to code to the constraints that both enforce, which means stopping the patterns that neither should allow. That discipline, once it's internalized, is what makes your components actually portable. Not portable in theory, not portable on the sandboxes you control, but portable in the sense that they pass in every org you actually ship to, including the ones you haven't encountered yet.

That's the payoff. It's boring to describe. It is genuinely powerful to have.

Top comments (0)