brownies: typed cookie, local, session and db storage behind one proxy
🍫 Tastier cookies, local, session, and db storage in a tiny package. Includes subscribe() events for changes.
At a glance
- What is it?
- brownies is a small TypeScript library that wraps cookies, localStorage, sessionStorage and IndexedDB in a getter/setter interface, keeps JavaScript types on round-trip, and exposes a subscribe() hook for changes. The trade-off is that the wrapper owns the storage keys, so code that reads the raw stores directly will not see the same values.
- Who is it for?
- brownies fits browser code that wants one typed interface over cookies, localStorage, sessionStorage and IndexedDB, and that can afford to route all storage access through its objects. Skip it if you need to read or write the same keys with plain localStorage.getItem or document.cookie, if you need cookie options that differ per key, or if you need a server-side store, since the README documents none.
- Can I use it commercially?
- Not without permission. GitHub finds no licence file in the repository, and without a licence all rights are reserved by default: you may read the code but not reuse it. Check the README, or ask the authors, before using it.
- Is it still maintained?
- Yes. The repository last received commits 137 days ago.
- What is it written in?
- Mainly TypeScript, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 1, 2026, and from our analysis. They are not legal advice.
Editorial analysis
The problem brownies targets: typed values in four browser stores
Browser storage APIs disagree with each other. localStorage and sessionStorage accept only strings, cookies are strings with attribute syntax, and IndexedDB is asynchronous and keyed. Writing an object, a boolean or an array means stringifying on write and parsing on read, and every caller has to remember which store it is talking to. brownies puts a single property-access interface in front of all four and keeps the JavaScript type on the way back out. The README shows typeof cookies.id returning 'number', typeof cookies.accepted returning 'boolean', and Array.isArray(cookies.friends) returning true. That type retention is the library's main selling point, and it is also the source of its sharpest constraint, covered below. The intended audience is front-end code that stores small pieces of state, a token, a preference, a cart, and does not want to hand-roll serialization for each store. The package is written in TypeScript, ships an index.d.ts, and the package.json lists zero runtime dependencies, with the build done by rolldown.
How the proxy interface and subscribe() actually behave
Access is property-based rather than method-based. cookies.token = 42 writes, cookies.token reads, and delete cookies.token removes the entry. The README states that a deleted key reads back as null rather than undefined, and explains that this was chosen to stay consistent with other local storage technologies. The db object breaks the symmetry deliberately: db.token = 42 is a plain assignment, but reading is await db.token, because IndexedDB is asynchronous. That is a real API wart. A synchronous-looking assignment followed by an awaited read is easy to get wrong in a loop, and the README flags the difference with a comment rather than hiding it. subscribe() is the second mechanism. You pass the store object, a key name, and a callback, and the callback fires on assignment and on delete, with null for the deleted case. The README's example assigns 42, then 'Hello', then deletes, and the callback receives 42, 'Hello' and null in that order. Iteration is also routed through the proxy: Object.keys, Object.values, Object.entries, for...in and for...of all work over the store objects, and the README shows a for...in loop over local as the way to delete everything. The underlying implementation detail that matters most is encoding. Cookies are serialized with JSON.stringify and then encodeURIComponent to stay RFC 6265 compliant, and the README warns that if you set a cookie manually through document.cookie you must reproduce both steps or brownies will not read it back correctly.
Installing brownies with npm and storing a first typed value
The README gives npm as the install path, and the package has no runtime dependencies, so nothing else is pulled in. Run the install command from your project root.
npm install browniesThen import the parts you need. The README shows both the ES module form and the CommonJS require form, and they pull from the same entry point.
import { cookies, local, db } from 'brownies';
// or: const { cookies, local, db } = require('brownies');For a plain browser page with no bundler, the README points at jsDelivr and warns that the script defines a single global named brownies, so you destructure from it rather than importing. After the script tag loads, window.brownies holds the store objects.
<script src="https://cdn.jsdelivr.net/npm/brownies"></script>
<script>
const { cookies, local } = brownies;
</script>With the import in place, the first real use is a write, a read and a delete. The key detail to check in your console is the type of the value that comes back, not just its contents, since type retention is the reason to pick this library over calling localStorage directly.
import { cookies, local } from 'brownies';
cookies.token = 42; // Set it
const t = cookies.token; // Get it
console.log(typeof t); // 'number'
delete cookies.token; // Eat it
console.log(cookies.token); // nullTo react to changes instead of polling, subscribe to a key on any of the store objects. The callback runs on every set and on delete. The README's example uses session, and the same signature applies to cookies, local and db.
import { session, subscribe } from 'brownies';
subscribe(session, 'token', value => {
console.log(value); // 42, 'Hello', null
});
session.token = 42;
session.token = 'Hello';
delete session.token;The constraint that decides adoption: brownies owns the key format
Since version 2.0, brownies stores values in localStorage using its own format so that types survive a round trip. The README states the consequence plainly: you cannot read items that were set by brownies with localStorage.getItem(KEY), and you should use the local.KEY accessor instead. This runs in both directions. Anything another script writes with plain localStorage.setItem will not appear correctly through the brownies proxy, and anything brownies writes will look wrong to code that expects a raw string. On a page where an analytics snippet, a framework router or a legacy module also touches localStorage, that is a genuine integration problem, not a documentation footnote. The same applies to cookies: the README's warning about manual document.cookie writes means server-set cookies must be encoded with JSON.stringify followed by encodeURIComponent to be readable. If you cannot route every reader and writer of a key through brownies, the library is the wrong tool for that key. A second, smaller limitation is that cookie options are global. The README sets them through cookies[options], and explicitly warns that cookies.options and cookies['options'] do not work, which is an unusual and easy-to-miss API. Because the options object is shared, you cannot give one cookie a 100-day expiry and another a session-only expiry through this interface. The README also does not document rollback, migration or a way to read pre-2.0 values, so an upgrade path from an older brownies version is not described anywhere in the repository's documentation.
Alternatives and how their approach differs
The closest alternative visible in the repository itself is idb-keyval, which appears in the devDependencies alongside fake-indexeddb. That is a hint about the intended comparison rather than a documented recommendation, but the difference in approach is clear. idb-keyval exposes promise-returning functions such as get and set over IndexedDB only. brownies exposes property access over four stores and adds a change subscription. If your state lives in IndexedDB and you are comfortable with promises everywhere, idb-keyval is a narrower tool with a smaller surface. If you need to treat a cookie and a localStorage entry the same way, brownies is the one that unifies them. The other alternative is doing nothing: calling localStorage.setItem with JSON.stringify and JSON.parse on read. That is a few lines of code, keeps the raw keys readable by any other script, and avoids the ownership problem described above. It costs you the subscribe() hook and the uniform interface across stores. The honest framing is that brownies trades interoperability with raw storage for type retention and a single API, and that trade is worth it only when the application controls every access to those keys.
Maintenance, licence and the cost of upgrading
The repository is not archived, and the last push was on 2026-05-17. The package.json reports version 4.0.1 and lists MIT as the licence, which is a permissive licence that generally allows commercial use, modification and redistribution provided the copyright notice and permission notice are kept; this is a description of the licence text, not legal advice, and you should read the LICENSE file in the repository yourself since the top-level file listing shows a readme, index.d.ts, index.min.js, package.json, tsconfig.json, src/ and .github/ but no LICENSE entry. The published package contains only index.min.js and index.d.ts, so consumers get the built bundle and the type declarations, not the TypeScript source. That matters for upgrade cost: if you need to patch behaviour, you are patching a minified UMD bundle unless you vendor the src/ directory from the repository. The README documents no deprecation policy, no changelog and no migration guide, so a major version bump is something you would have to evaluate against your own usage. The build scripts use bun and rolldown, which means contributing back requires that toolchain rather than plain node and npm.
Editorial conclusion
brownies fits browser code that wants one typed interface over cookies, localStorage, sessionStorage and IndexedDB, and that can afford to route all storage access through its objects. Skip it if you need to read or write the same keys with plain localStorage.getItem or document.cookie, if you need cookie options that differ per key, or if you need a server-side store, since the README documents none. Before adopting, verify three things in your own codebase: that nothing else touches the same localStorage keys, that the 100-day default cookie expiry is what you want, and whether the db layer's IndexedDB dependency is available in your target environment.
Frequently asked questions
How do you use brownies to store a value?
Import the store object you want, then assign a property to write and read the same property to get the value back. For cookies, local and session this is synchronous, while db requires await on reads because it is backed by IndexedDB.
Can brownies read values that were written directly with localStorage.setItem?
No. Since version 2.0 brownies uses its own data format to keep types consistent, and the README states that you cannot read items set by brownies with localStorage.getItem. The same mismatch applies in reverse for values written outside the library.
How do you delete a value stored with brownies?
Call delete on the property, as you would with a normal object property. The README notes that a deleted key then reads back as null rather than undefined, matching other local storage technologies.
How do you subscribe to changes in brownies?
Import subscribe along with the store object and pass the store, the key name and a callback. The callback fires on each assignment and on delete, receiving the new value or null for a deletion.
Does brownies keep the JavaScript type of the stored value?
Yes. The README shows numbers, booleans, strings, arrays and objects coming back with their original types, which is possible because values are serialized with JSON.stringify and, for cookies, also encoded with encodeURIComponent.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/franciscop-brownies)