Client-side data

Obfuscation does not protect what you store in the browser

A protected bundle and an open storage viewer sit side by side in the same browser, and only one of them was affected by your build step. This is a distinction that gets lost regularly, usually in the form of a support question that begins “we obfuscated the app, so why can I still see the subscription flag in devtools?” The answer is short, and the design consequences are worth spelling out.

Obfuscation acts on code, not on data

A protection pass rewrites your JavaScript so it is expensive to read, while producing a program that behaves exactly as it did before. That behavioural equivalence is the point — it is what makes the technique safe to apply to a shipping product. It also settles the storage question completely. If your application wrote {"plan":"pro","trialEndsAt":"2026-09-01"} under a key before, it writes precisely that afterwards, because anything else would be a change in behaviour.

The stores involved are exposed by the browser rather than by your program. Open the Application panel and localStorage, sessionStorage, IndexedDB, the Cache API and cookies are all listed, per origin, with keys and values, editable in place. Nothing in that path runs your code, so nothing in that path is affected by how your code was transformed.

Obfuscation does remove the readable key names and the storage schema from your source, which is a genuine but limited benefit: it slows down someone reading the bundle to work out your layout, and it is undone the moment they look at the store instead, where the keys are enumerated for them.

The four stores and what each one actually offers

Developers often reach for a different store on the assumption that it is more protective. It is worth being explicit that none of these differ in confidentiality from page scripts.

localStorage is synchronous, string-only, origin-scoped and persists until cleared. Its distinguishing property is that it survives indefinitely, which makes it the worst place for anything time-sensitive left behind on a shared machine.

sessionStorage is identical in every respect except that it is scoped to the tab and cleared when the tab closes. That is a data-retention improvement and not a confidentiality one. While the tab is open it is exactly as readable.

IndexedDB adds structure, indexes, large quotas and asynchronous access. Because it presents as a database, it attracts the assumption that it has database-like access control. It does not: same origin scoping, same devtools browsing, same access from any script on the page.

The Cache API holds whole responses, which makes it the easiest place to leak something by accident — a caching layer that stores authenticated API responses has written those response bodies to disk in the user profile, where they remain after the session ends.

Cookies are the one genuine exception, and only when you use the relevant flags. A cookie marked HttpOnly is not readable from JavaScript at all, which is a real browser-enforced boundary rather than a cost increase. That property is why session handling belongs there rather than in web storage.

Encrypting it in the client moves the problem

The natural next thought is to encrypt values before storing them. This is worth doing sometimes, and worth being honest about always.

If your JavaScript performs the encryption, then your JavaScript contains or derives the key, and everything your JavaScript contains has been given to the person running the browser. Obfuscation makes finding the key harder; it cannot make it absent. And the reader has an easier route than searching: set a breakpoint after decryption, or call your own decrypt function from the console against the stored value.

So the honest description is that client-side encryption of stored data converts a trivial read into a mildly inconvenient one. Against a curious user poking at storage, it works. Against anyone motivated, it does not. This is the same boundary described in obfuscation versus encryption and you cannot hide an API key in JavaScript: a secret that must be usable by code the user controls is not a secret from that user.

The practical rule that follows: use it to reduce accidental exposure of things like cached personal data on a shared device, and never to protect something whose disclosure genuinely costs you.

Reading is the smaller half of the problem

Confidentiality gets the attention, and integrity is what actually breaks products.

Storage is writable. A value your application reads back and trusts is a value the user can set. Every one of these is a real pattern that ships:

  • An entitlement or plan flag cached in storage and trusted on the next load.
  • A trial expiry date the client compares against the local clock.
  • A feature-unlock map written after a purchase and read on startup.
  • A metered read counter, which is the paywall version of the same mistake.
  • A cached role or permission set used to decide which controls to render and which requests to allow.

Obfuscation does not help with any of these, because the attack does not involve reading your code. Someone edits a value in the Application panel and reloads. The rule is the one that recurs across paywalls, assessment and license checks: if the client computes or stores the decision, the client owns it. Cache the decision for rendering by all means, and re-derive it on the server for anything that matters.

Where protection genuinely applies

Having drawn that line, the code around storage is frequently worth protecting on its own merits, and it is easy to overlook because the data question dominates the conversation.

Offline-first applications carry substantial engineering in exactly this layer: sync and conflict resolution, cache invalidation and keying strategy, partial-state reconciliation, queue-and-retry semantics for writes made while disconnected. That is design work a competitor can lift directly out of a readable bundle, and it is a legitimate target for renaming and string protection. The member renaming and string protection options are the relevant ones, with one operational caveat that matters here more than elsewhere.

Persisted key names are a serialization boundary. If you rename the properties of an object and then write it to IndexedDB, a build that renames them differently will not read back records written by an earlier build. Storage keys, persisted object shapes and anything crossing that boundary must be reserved, exactly like an API surface. It is the same rule as cross-build naming generally, and the failure mode is unusually unpleasant: it appears only for users with existing data, and only after they upgrade.

The short version

Obfuscation transforms code and preserves behaviour, so the values your application writes are unchanged and the browser exposes them directly through devtools regardless of how your bundle was built. localStorage, sessionStorage, IndexedDB and the Cache API differ in lifetime, structure and capacity, and not in who may read them; only HttpOnly cookies put a browser-enforced boundary between page scripts and a value. Encrypting in client code raises the cost of a casual read and cannot deliver confidentiality, because the key ships alongside. Keep decisions and credentials off the client, treat anything stored as user-editable input when you read it back, reserve persisted key names before renaming, and use protection for the sync and caching logic that is genuinely yours.

Frequently asked questions

Does obfuscating my JavaScript encrypt what it writes to localStorage?

No. Obfuscation transforms the code and leaves its behaviour identical, so whatever your application stored before it stored the same values afterwards. Storage is a separate surface that the browser exposes directly through the Application panel in devtools, and reading it does not involve your code at all. The one thing obfuscation changes is the readability of the key names inside your source, and even that is undone the moment someone opens the storage viewer and sees the keys listed with their values.

Can I encrypt values before putting them in localStorage?

You can, and it is worth understanding exactly what it buys. If your JavaScript performs the encryption then your JavaScript holds the key, and everything you ship is available to the person running the browser. Someone reading the storage no longer sees plaintext, and someone willing to set a breakpoint or call your own function from the console recovers it. That converts a trivial read into a slightly less trivial one. It is a reasonable thing to do against casual inspection and it is not confidentiality, so never store something whose disclosure genuinely matters.

Is sessionStorage safer than localStorage?

Only in lifetime, not in confidentiality. sessionStorage is scoped to a tab and cleared when it closes, which reduces how long data sits on a shared machine and how likely it is to leak between sessions. While the tab is open, it is exactly as readable as localStorage: same devtools panel, same origin-scoped access from any script on the page. Choosing it is a data-retention decision rather than a security one.

Is IndexedDB more secure because it is a real database?

No. IndexedDB gives you structure, larger quotas, indexes and asynchronous access, all of which are good reasons to use it. It has the same trust model as the simpler stores: it lives in the user profile, it is browsable in devtools, and any script running on your origin can read it. Developers sometimes assume that because it looks like a database it has database-style access control, and it does not. The storage type is a capability choice, not a protection choice.

What is actually safe to keep in browser storage?

Anything whose disclosure to the person using that browser costs you nothing. User interface preferences, draft content that belongs to the user anyway, cached responses the user is already entitled to see, and opaque identifiers that mean nothing without a server-side lookup are all fine. What does not belong there is anything the client should not be able to change without you noticing: entitlement flags, remaining trial days, feature unlocks, prices, roles, or long-lived credentials. Those are decisions, and decisions belong on the server.

Attackers can read storage, so does obfuscation help at all here?

It helps with the code, which is a different problem worth being precise about. Obfuscation raises the cost of understanding the logic that decides what to store, when to sync it, how the cache is keyed and how conflicts resolve, and that logic is often real engineering investment. It also removes the readable key names and structure from your source, which slows down someone writing tooling against your storage layout. What it never does is change the values in the store or restrict who may read them.

Is a token in localStorage protected once the bundle is obfuscated?

No, and this is the single most common version of the mistake. A token in storage is readable from the storage viewer, and it is readable by any script that runs on your origin, which is what makes cross-site scripting so damaging in this design. Obfuscating the code that placed it there changes neither fact. The controls that apply are architectural: short-lived tokens, refresh handled through cookies marked HttpOnly and SameSite so page scripts cannot read them, and a strict content security policy to reduce the chance of hostile script running on the origin at all.

Related reading