Build Integration

Obfuscating htmx and Alpine apps: your logic is in the markup

Every framework page on this site answers a version of the same question: which part of this stack is public, and what does protecting it buy. For hypermedia and attribute-driven stacks the answer is unusual enough to deserve its own article, because the thing you would want to protect is mostly not JavaScript at all — and one common configuration will break your page rather than protect it.

What the protector actually reads

Start with the tool's real boundary, because everything else follows from it. The protector transforms JavaScript. Give it a .js file and it parses the program, rewrites it and emits an equivalent one. There is a second mode for files that are not pure JavaScript — an Enterprise option that locates script regions inside an .aspx, .php, .jsp or .html page, protects those regions, and leaves every byte around them untouched.

Read that second sentence carefully, because the distinction it draws is the whole article. The option finds script blocks. An HTML attribute is not a script block. hx-post="/cart/add" and x-on:click="open = !open" are markup that a library reads at run time and interprets. To the protector they are text inside a tag, indistinguishable from a class name or a piece of copy, and they are preserved exactly as written — which is the correct behaviour, and also means they are not protected in any sense.

So in an attribute-driven application, the amount of your behaviour that the tool can reach is however much of it you put in real script files. That fraction is deliberately small in these stacks. That is the point of them.

htmx: there is very little there, and that is the design working

A hypermedia application's attributes describe requests and where to put the responses. An endpoint, a trigger, a target element, a swap strategy. Four facts, all of which the browser is about to reveal anyway the moment the user clicks something and the request appears in the network panel.

There is no meaningful confidentiality loss to prevent, because there was no logic in the browser to lose. The decisions — what a user may do, what the response should contain, what the transaction costs — happen on your server and are returned as rendered HTML. This site spends a lot of pages arguing that authority belongs on the server because the browser cannot keep a secret. A hypermedia stack starts from that position rather than arriving at it, which means the usual argument for protection is largely already satisfied by the architecture.

What you may still have worth protecting: htmx extensions you wrote, event handlers and initialisation in real script files, and any client-side module doing something genuinely proprietary. Those are ordinary JavaScript and protect normally with no special handling. If you are on this stack because you have no build step at all, the no-build-step guide covers the desktop and command-line paths that fit that workflow.

Alpine: the failure mode nobody warns you about

Alpine is where this gets sharp, because an Alpine attribute contains something that genuinely is JavaScript — an expression, written as a string, evaluated by the library at run time. That combination produces a specific and quite avoidable breakage.

The protector never sees those expressions, so it cannot rename anything inside them. But it does see, and does rename, the module that the expression refers to. Consider a real shape: a script file exports a helper, the page has x-data="cartPanel()", and a button carries x-on:click="cart.addItem(id)". Turn on global renaming and the function that was cartPanel in your source becomes a short generated name in the output. The attribute still says cartPanel(). Alpine evaluates it, finds nothing, and the component silently fails to initialise. Member renaming produces the same result one level down: the object still exists, but the property the markup names has been rewritten.

The important part is what this looks like when it happens. It is not a build error and it is not a syntax error. The page loads, the styling is intact, and one interactive region quietly does nothing — which is the most expensive class of defect to find, because nothing in your pipeline reports it. The general verification advice applies with more force here than almost anywhere else.

The fix is an exclusion list, and it is small

The mechanism exists and it is straightforward: any name that markup refers to is part of your public interface and has to be excluded from renaming. The configuration accepts a list of regular expressions for names to preserve, mapped onto the variable exclusion list the engine already uses for exactly this purpose.

The practical difficulty is not configuring it, it is knowing what to put in it, because the names live in your templates rather than in your code. Two approaches work. The thorough one is to grep your templates for the attribute prefixes and collect every identifier they mention, which is mechanical and worth automating if your template count is large. The better one is to reduce the surface deliberately: expose one namespace object per page or per feature, keep that single name excluded, and reach everything else through it. Then your exclusion list has a handful of entries that change rarely, instead of one entry per handler that drifts every sprint.

The same reasoning covers string handling. If a template names something that your code compares against a literal — a component key, an event name, a data attribute value — and you have literal patterns you need preserved verbatim, the reserved strings mechanism keeps matching literals out of the string transforms. It accepts up to a hundred patterns, which is generous for this purpose and a signal that the intended use is a small deliberate list rather than a catch-all.

A content security policy note that is not about us

Teams often arrive at this question from the opposite direction, having been told that their content security policy needs unsafe-eval and assuming the protector caused it. Worth separating the two, because they are unrelated.

Evaluating an expression that arrives as a string at run time is what an attribute-driven library does by definition, and that is what a strict policy objects to. Alpine ships an alternative build specifically to address this, at the cost of a restricted expression syntax. That requirement comes from the library and exists whether or not you ever protect anything.

The protector's own relationship to eval is narrow and documented: only two options put an evaluation construct into the output, and one of them force-enables the other. Leave both off and the protected output introduces no new policy requirement. If you are working through a policy in detail, the CSP article names the two options precisely, and the Trusted Types article covers the stricter regime.

Script inside the page, and the trap in it

These stacks encourage inline script, since half the appeal is not having a build. The mixed-file option handles that: point it at your .html, .php, .jsp or .aspx templates and it protects the script regions while leaving the surrounding markup byte-identical.

Two rules come with it, and both are about server-rendered pages rather than static ones. A server tag can appear inside a JavaScript expression, which is not valid JavaScript until the server has rendered it, so the tag is preserved as an opaque token and must never straddle a statement boundary. And you have to smoke-test the rendered page rather than the template, because the template is not what runs. That advice matters more than usual here: a server-rendered hypermedia application returns fragments, so the thing to test is a fragment fetched after an interaction, not just the initial document.

What this stack should actually do

Five points, and the first two are the ones that matter.

Accept that the architecture has already moved most of the value out of the browser, and stop looking for a build setting that adds confidentiality the design deliberately does not need. Protect the script files that genuinely contain your work, and leave the attributes alone. Keep an exclusion list for every name your markup refers to, and shrink that list by routing through one namespace per feature instead of many loose globals. Test an interactive page and a swapped-in fragment against the protected build, because the failure mode here is silence rather than an error. And if you want a runtime lock, remember the domain lock reads the hostname of the document it runs in — fine on your own pages, and worth understanding before you put protected script inside a fragment that might be rendered somewhere else.

Frequently asked questions

Can the protector transform code inside an htmx or Alpine attribute?

No, and that is the correct behaviour rather than a gap. The tool transforms JavaScript files, and it has a separate mode that locates script blocks inside an html, php, jsp or aspx page. An attribute is neither. To the parser, the text inside an attribute is markup, indistinguishable from a class name or a piece of copy, so it is preserved byte for byte. The practical consequence is that in an attribute-driven application, the share of your behaviour the tool can reach is however much of it lives in real script files.

Is there any point protecting an htmx application?

There is, but it is narrower than usual and the architecture is the reason. The attributes describe an endpoint, a trigger, a target and a swap strategy, all of which the browser reveals the moment a user interacts and the request appears in the network panel. The decisions happen on your server and come back as rendered HTML. What remains worth protecting is anything you wrote in real script files: htmx extensions, initialisation, event handling, and any client-side module doing genuinely proprietary work.

What breaks when renaming meets an Alpine expression?

The link between markup and module, silently. An Alpine attribute holds a JavaScript expression as a string, evaluated by the library at run time, and the protector never sees it. But it does rename the module the expression refers to. If your markup says one name and global or member renaming has rewritten the definition to another, the library evaluates the expression, finds nothing, and the component fails to initialise. There is no build error and no syntax error. The page loads, the styling is intact, and one interactive region quietly does nothing.

How do we prevent that?

Treat every name your markup mentions as public interface and exclude it from renaming. The configuration accepts a list of regular expressions for names to preserve, mapped onto the variable exclusion list the engine already uses for this purpose. The harder part is knowing what belongs in the list, since the names live in templates rather than in code. Grep your templates for the attribute prefixes to collect them, then shrink the list permanently by exposing one namespace object per feature and reaching everything else through it.

Do string transforms cause the same class of problem?

They can, wherever a template names something your code compares against a literal, such as a component key, an event name or a data attribute value. The reserved strings mechanism covers it: matching literals stay verbatim and are kept out of the string transforms. It accepts up to a hundred patterns of moderate length, which is generous for this purpose and a fair signal that the intended use is a short deliberate list rather than a blanket exclusion. Keep it alongside the name exclusions so both are reviewed together.

Does the protector cause our need for unsafe-eval in the content security policy?

Almost certainly not, and the two are worth separating. Evaluating an expression that arrives as a string at run time is what an attribute-driven library does by definition, and that is what a strict policy objects to. Alpine ships an alternative build specifically to address it, at the cost of a restricted expression syntax. On the protector's side only two options put an evaluation construct into the output, and one of them force-enables the other. Leave both off and the protected output adds no new policy requirement.

How do we protect script that sits inline in our templates?

Use the mixed-file option, which locates script regions inside html, php, jsp and aspx files, protects them, and leaves the surrounding markup byte-identical. Two rules come with it. A server tag may appear inside a JavaScript expression and is preserved as an opaque token, so never let one straddle a statement boundary. And smoke-test the rendered page rather than the template, because the template is not what runs. In a hypermedia application, extend that to a fragment fetched after an interaction, not just the initial document.

What is the shortest sensible policy for this stack?

Five points. Accept that the architecture has already moved most of the value to the server and stop looking for a build setting that replaces a design decision. Protect the script files that contain your actual work and leave attributes alone. Maintain an exclusion list for every name the markup refers to, and keep it short by routing through one namespace per feature. Test both a full page and a swapped-in fragment against the protected build, because the failure mode here is silence rather than an error. And remember the domain lock reads the hostname of the document it runs in.

Related reading