In the browser

Read this page in the documentation

In the browser prorm ships a browser build, served from this site: or as a module: Roughly 470 KB minified. It is hosted here rather than on a public CDN, so the library and the site it documents come from one origin and one deploy. What is in it Code written against the npm package reads the same here — same names, same shapes, same decorators: Available | Not available | --- | --- | Prorm, Model, ModelInstance, DataTypes, QueryTypes | The 27 SQL dialects (PostgreSQL, MySQL, …) | Every model decorator — @Table, @Column, @PrimaryKey, @HasMany, … | The ~100 store adapters (Redis, S3, Mongo, …) | Every schema decorator — @View, @Trigger, @Procedure, @SqlFunction | Migrations (they read the filesystem) | The query vocabulary — Op, fn(), col(), where, gte, between, … | Anything needing a socket | The bare type names — STRING, INTEGER, BOOLEAN, … | | The error hierarchy and its guards | | The stream transforms | | The 15 diagram generators | | SQLite (Web SQL or WebAssembly), localStorage, sessionStorage, cookies | PouchDB and RxDB — adapters ship, the libraries do not | That is 483 exports against the package's 1,064; the difference is almost entirely the dialects and store adapters, which reach for Node drivers. They are not silently missing — the bundler resolves each to a stub that names itself if used. The excluded modules are not merely absent: importing one gives a stub that names itself if you use it, so a mistake reads as a sentence rather than a mangled driver. Pinning a version prorm.VERSION reports the build's version at runtime. Web storage localStorage and sessionStorage are key-value stores every browser already has, so this is persistence with nothing installed and no server: localStore() survives the tab closing; sessionStore() is cleared when it does. That is the only difference between them. Method | Does | --- | --- | get(key) / set(key, value, ttl?) | Read and write, JSON-serialised | has(key) / del(key) | Test and remove | mget(keys) / mdel(keys) | Several at once | incr(key, by?) / decr(key, by?) | Counters, starting from zero | list(prefix?) / entries(prefix?) | Keys, or keys with values | clear() | Remove this namespace only | purgeExpired() | Drop expired entries now | size() | Approximate keys and bytes | For data the server must see on each request, use cookies instead — they are a different tool with a different cost. Three things that make web storage awkward Each is handled, but worth knowing because they explain the API. It can throw when you merely touch it. With cookies blocked for the site, or in some private modes, reading the localStorage property raises a SecurityError — before any write. connect() probes with a real write and raises WebStorageUnavailableError explaining why, and WebStorageStore.isAvailable() answers without throwing: It has no expiry. Nothing you write ever leaves on its own, so a ttl is stored with the value and enforced when the value is read. A key past its lifetime reads as null and is removed then. purgeExpired() sweeps eagerly when you would rather reclaim space up front. It is small, and throws when full. Around 5 MB per origin, and a write past that raises QuotaExceededError — which browsers spell four different ways. A full store first drops its own expired entries and retries; only if that fails does it raise WebStorageQuotaError, naming the key and the byte count, because "quota exceeded" on its own tells you nothing about what to shrink. Namespaces Every key is prefixed, so a store finds its own entries and leaves the rest of the origin alone — clear() removes only what this store wrote: Note: a page is single-threaded, so incr() needs no locking. Two tabs can still interleave writes to the same key, and nothing in the browser prevents that. Cookies The same shape as the storage API — get, set, has, del, list, entries, incr, decr, clear, size — over document.cookie, or the async Cookie Store API where the browser has it. Cookies are not a storage backend Warning: everything you put here is uploaded to the server with every request to this domain — every image, script and fetch. A megabyte in localStorage costs nothing per request; a kilobyte in a cookie is paid for thousands of times. Cookies are for what the server must read: a session id, a locale, a feature flag. Application data belongs in localStore(). size() reports that cost, including cookies this store did not write, since they ride along too: Where they differ from web storage | Cookies | Web storage | --- | --- | --- | Size | ~4 KB each, ~50 per domain | ~5 MB per origin | Sent to the server | Every request | Never | Expiry | Native, browser-enforced | Emulated on read | Overflow | Silently dropped | Throws | Readable by JS | Unless httpOnly | Always | Two of those need care, and both are handled: An oversized cookie is discarded without a word. Browsers do not raise anything — the write simply does not happen and the next read finds nothing. Every write is measured first and throws CookieTooLargeError naming the key and its size: httpOnly cookies are invisible here. That is the point of the flag: the cookies most worth protecting are the ones JavaScript must not touch. A get() returning null therefore means "not readable", which is not quite the same as "not there". Attributes path, domain, secure and sameSite are set per store and overridable per write. secure defaults to whether the page itself is secure, because a Secure cookie on an http: page is rejected silently; sameSite defaults to Lax. Note: deleting a cookie requires the same path and domain it was written with. A cookie set on /app is not removed by deleting at /, and that failure is also silent — so pass the same attributes to del() if you varied them on set(). PouchDB and RxDB For documents, queries over them, and — the reason most people reach for either — replication, so a page works offline and reconciles later. Neither is bundled. PouchDB is around 140 KB and RxDB more; carrying them would more than double this build for pages that use neither. The page loads the one it wants and passes the instance in: That also keeps the choice of adapter yours — IndexedDB, memory, or a remote CouchDB — since which one is in use is the page's business, not the store's. Note: PouchDB rejects a write that does not carry the document's current rev, so set() fetches the existing document first. That is one read per write; use setMany() when writing more than a few. id and rev are stripped on the way out — they are PouchDB's bookkeeping, not your data. find() needs the pouchdb-find plugin registered on the page; without it the call says so rather than failing on an undefined method. RxDB Schema-first and reactive. The same store surface, plus the observable that is the point of it: observe() hands back RxDB's own observable, unwrapped — subscribing, piping and unsubscribing all work exactly as RxDB documents them, which wrapping would only take away. Choosing | Web storage | Cookies | PouchDB / RxDB | Web SQL | --- | --- | --- | --- | --- | Size | ~5 MB | ~4 KB | Disk-bound | ~50 MB | Shape | Key-value | Key-value | Documents | Relational | Queries | By key | By key | Selectors | SQL | Replication | No | No | Yes | No | Bundled | Yes | Yes | No — page supplies | Yes | Works in Chrome/Firefox | Yes | Yes | Yes | No — use the WebAssembly engine | A real SQL database That is the ORM as documented everywhere else — models, finders, operators, views — running entirely in the page. openBrowserDatabase() picks the engine; you do not have to. Which engine you get | Web SQL | WebAssembly | --- | --- | --- | Where | Safari, Chrome < 119 | Everywhere | Download | Already there | ~1.5 MB, on first use | Persistence | Yes | Yes, via OPFS | Size limit | ~50 MB | Disk-bound | Web SQL is preferred where it exists purely because it is already loaded, which saves the download. Everywhere else — Chrome, Firefox, and so most visitors — the WebAssembly build answers. Deprecated: Web SQL was abandoned as a specification in 2010, never implemented by Firefox, and removed from Chrome in version 119. It is supported here because Safari still ships it, not because it has a future. WebAssembly This is the SQLite project's own build, so the SQL the ORM generates runs unchanged. It is served from this site — the same origin as the library — and fetched the first time a database is opened, so a page that never opens one downloads none of it. Persistence uses the OPFS SAHPool VFS: real files in the browser's Origin Private File System, surviving reloads. That mode is chosen deliberately — the other OPFS mode needs SharedArrayBuffer, which needs the page to be cross-origin isolated with COOP and COEP headers. Requiring two response headers on your HTML in order to use a database is a poor trade; SAHPool needs neither. Where OPFS cannot be opened — a sandboxed iframe, a second tab already holding the pool — it falls back to an in-memory database and warns, rather than appearing to persist and losing everything on reload: Note: serving the engine yourself requires two content types to be right — .mjs as text/javascript and .wasm as application/wasm. A module served as application/octet-stream cannot be imported, and the browser will not stream-compile a wasm file without the correct type. Streaming is not offered on either engine: both hand back a whole result set with no cursor, so a "stream" would be the full result buffered in memory pretending otherwise. Model.iterate() pages through findAll() and works on both. Diagrams in the browser The generators build SVG through a DOM. Node has none, so they use xmldom there; in the browser they are mapped onto the real DOMImplementation, which means diagrams render client-side with nothing extra loaded: Related reading Streaming results — Model.iterate(), which works here Schema objects — views and triggers, all SQL-free Diagram generators