On-Premises & Self-Hosted

You handed your entire product to the customer. On purpose.

SaaS keeps the interesting code on your servers. On-premises delivery does the opposite: the bundle, the Node services, the licence check and the deployment scripts all end up on hardware owned by someone else, behind a firewall you will never see, administered by people who have root and, occasionally, a renewal they would rather not pay.

Reality Check

tar -xzf acme-appliance-4.2.tgz

One archive, unpacked by the customer, on a machine where they are the administrator and you are a guest with no session.

Per-customer buildA leaked archive names the account it came from.
Offline expiryDate locking needs no network to end a term.
Signed attestationThey can prove the install is the build you sent.
What Changes

Four assumptions that stop being true

Most JavaScript protection advice quietly assumes a server you control is one request away. On-premises removes that assumption, and with it several controls people reach for by reflex.

1. The server is not yours

“Move authority server-side” is the standard answer to every client-side limit. Here the server is the customer’s. Entitlement logic you push into the backend lands on the same machine as the frontend, with the same administrator.

2. There may be no network at all

Regulated and defence deployments are genuinely air-gapped. A licence check that phones home does not fail loudly — it fails on install day, in a room you are not in, and becomes a support escalation.

3. You cannot ship a hotfix

Upgrades happen on the customer’s change calendar, not yours. A build that goes out wrong stays wrong for a quarter, which raises the value of everything you verify before the archive leaves your CI.

4. Someone else audits your code

The customer’s security team will scan what you delivered. Obfuscated JavaScript inside their perimeter reads as an anomaly unless you arrive with evidence — see what to do when a scan flags it.

One customer, one release
npm run build                        # bundle -> dist/

npx jso-protector \
  --config jso.config.json \
  --label      "4.2.0+$GIT_SHA/acme-corp" \
  --watermark  "acme-corp-4.2.0" \
  --option     LockDomain=true \
  --option     LockDomainList=intranet.acme-corp.com \
  --option     LockDate=true \
  --option     LockDateValue=20270331 \
  --manifest   out/jso-manifest.json \
  --report     out/jso-report.json \
  --sign-release keys/release.priv.pem

# the customer, on their own machine:
npx jso-protector --verify-release out/jso-manifest.json.sig \
  --public-key acme-vendor.pub.pem --verify-root ./app
The Shape Of It

Build per account, verify at install

  • The label is your index. Version, commit and account in one string, so a stack trace six months later finds its report.
  • The watermark is your attribution. An HMAC-SHA256 marker naming the account, readable out of any stray copy.
  • The locks are your term. Domain and date, both evaluated locally, both needing zero network.
  • The signature is their assurance. Ed25519 over the build id, fingerprint and per-file hashes.
  • The report never leaves your side. It is the key to reading their crashes; it is not a delivery artifact.
Offline Controls

The two locks that work with no network

Domain and date locking are evaluated inside the protected file itself, before your program runs. That property is unremarkable in a SaaS context and decisive in an air-gapped one, because they are the only entitlement signals that still function when nothing can be reached.

Domain lock, per install

The guard reads location.hostname and compares it against the origins you authorised, optionally accepting subdomains. Ask for the intranet hostnames during onboarding — internal deployments have more of them than anyone expects, and a missed staging. host becomes a go-live incident.

Date lock, for terms and pilots

LockDate ends access after a date; LockStartDate refuses to run before one. Both take a plain YYYYMMDD value. A 90-day proof-of-concept that stops working on day 91 without anyone remembering to revoke it is the clearest use.

Choose the failure carefully

A failed lock routes through the runtime defense action: throw, blank, redirect, reload, callback or degrade. Inside a customer’s production system, degrade or callback lets their operators see a clear message. Blanking a screen during their business hours makes an expiry look like an outage.

Both locks are policy controls, not trust boundaries, and the distinction matters more here than anywhere else on this site. The customer administers the machine; a determined administrator with your archive and enough time can reach any check inside it. What the locks buy you is that overrunning a term stops being an accident and becomes a deliberate, documented act — which is exactly the footing a contract dispute needs. Pair them with a licence check written to be tamper-evident rather than tamper-proof.

Delivery Evidence

Sign what you ship, then let them check it

On-premises is the one distribution model where the customer has a supply-chain problem about you. Answering it well removes friction from every future release, and the artifacts are the same ones you need for your own release hygiene.

The signed attestation

--sign-release writes an Ed25519 envelope over the build id, the polymorphism fingerprint and a SHA-256 per output file. Generate the keypair once with --genkey-release, publish the public half, and keep the private half in your CI secret store. Never in the repository.

Verification on their side

--verify-release checks the envelope. With --public-key it pins to your key rather than trusting the one embedded in the file, and with --verify-root it re-hashes the installed files on disk. That last flag is what turns “the download was intact” into “the deployment is intact.”

No maps in the appliance

Run --verify-manifest with --audit-source-maps in CI so the build fails if a .map file or a sourceMappingURL comment survives into the archive. Shipping a map into a customer’s filesystem hands over the original source and undoes the whole exercise.

A deployment note about egress

State plainly which outbound connections the delivered software makes, and whether the runtime defense beacon is unset or pointed at a collector inside their network. Security reviewers reward a vendor who volunteers this and punish one who is discovered by a firewall log.

What To Protect

Not everything in the archive is worth the same effort

The server-side JavaScript

An on-premises Node service is source code sitting in a directory the customer can read. It is usually the most valuable part of the delivery and the part people forget, because habit says “the backend is safe.” Protecting Node.js source covers the specifics.

The proprietary core

The pricing engine, the matching algorithm, the model that took two years. Scope VM protection to those functions specifically. It is slower than plain JavaScript, so keeping it off hot paths matters as much here as anywhere.

The migration and admin scripts

Operational tooling shipped alongside the product usually contains no secrets and needs to be readable when something goes wrong at 2am on their side. Protecting it costs you support hours and buys nothing. Leave it alone deliberately.

Support

Diagnostics for a machine you will never log into

Archive one report per customer release

Not per version — per build. If four customers get four differently-locked builds of 4.2.0, that is four reports, and only the right one will decode a given stack trace through symbolication. The label you set is how you find it.

Give them a way to report cleanly

Document exactly what you need in a ticket: the version string, the label from the build banner, and the raw stack trace. Customers who cannot share logs off-site can still read a trace out to you, and that is often all it takes.

Reproduce with the same seed

Seeding protection from the release tag makes a build byte-reproducible, so you can rebuild a customer’s exact artifact months later. Reproducible builds explains the trade-off against per-build polymorphism.

Verify before the archive ships

Run the product’s own end-to-end suite against the protected output, not the unprotected one. You cannot patch this in an hour after it leaves — where testing belongs in a protected pipeline has the ordering.

Start Now

Unpack your own appliance archive

Install your product the way a customer does, then read the files as their administrator would: open the bundle, look for source maps, look for the licence check, and see how long it takes to find the line that decides whether the software runs. That walkthrough is the requirements document for everything above.

Frequently Asked

On-premises protection questions, answered

Can I enforce a licence in an air-gapped install?

Not enforce - deter. With no network there is no server to hold authority, so every check runs on hardware the customer controls and can in principle be edited out. Date locking gives you an expiry that needs no network: LockDate and LockStartDate take a YYYYMMDD value and route a failure through the runtime defense action you choose. Treat it as a contractual tripwire that makes overrun deliberate and provable, not as a technical boundary.

Should I produce a separate build per customer?

For on-premises delivery, usually yes. A per-customer build lets you lock the domain to that customer's hostnames, set an expiry matching their term, and carry a watermark tag naming the account. A leaked file then identifies itself. The cost is a release matrix that grows with your customer list, so automate it in CI from the start rather than retrofitting it after the first leak.

How does the customer verify the build I sent them?

Sign the release. The CLI writes an Ed25519 attestation over the build id, the polymorphism fingerprint and a SHA-256 for every output file. The customer runs verify-release against your pinned public key, and with a verify root it re-hashes the files on disk as well. That answers their supply-chain question and yours: what is installed is exactly what you shipped.

Will obfuscated JavaScript get flagged by the customer's security team?

Sometimes, and you should expect it. Scanners and endpoint tools treat unreadable JavaScript as a signal worth reporting. Arrive with the answer already prepared: the signed attestation, the manifest with per-file hashes, a statement that no source maps ship in the appliance image, and a named contact. A vendor who can produce that in a day turns a finding into a formality.

Does the protected output call home at runtime?

Not unless you configure it to. Protection happens at build time on your machines. The runtime defense beacon is opt-in through RuntimeDefenseBeaconUrl, so for an air-gapped install you either leave it unset or point it at a collector inside the customer's own network. Ship a deployment note saying which, because an unexplained outbound request is the fastest way to fail an on-premises security review.

How do I debug a crash on a customer's server I cannot reach?

Keep the protection report for every customer release, labelled with the commit and the account. Symbolication translates an obfuscated stack trace back to real names, but only against the report that produced that exact build. On-premises makes this critical, because the trace someone pastes into a support ticket is often the only evidence you will ever get.

Related Guides

Protecting other JavaScript targets

Same protection engine, different distribution model. These are the guides closest to on-premises delivery:

Protecting a progressive web app · White-label and OEM redistribution · Embeddable widgets and SDKs · Can you lock JavaScript to a device? · Kiosks and embedded devices · Node.js source · Electron apps · License checks · Published npm packages · Online exams and assessment · Protecting JavaScript (overview) · E-learning and SCORM courseware · Binaries and bytecode · Web3 dApp frontends · Multi-tenant SaaS frontends · Tauri apps · ASP.NET, Razor and Blazor · Your JavaScript expiry date runs on their clock · Should you obfuscate an edge function? · IEC 62443 and industrial control