prorm ships a browser build, served from this site: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 77 server-side SQL dialects (PostgreSQL, MySQL, …) |
Every model decorator — @Table, @Column, @PrimaryKey, @HasMany, … | The 106 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#
https://prod-orm.io/cdn/prorm-2.1.0.min.js ← pinned
https://prod-orm.io/cdn/prorm.min.js ← latest
https://prod-orm.io/cdn/manifest.json ← what is published
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.To get the same engine in every browser, say Safari included, ask for it by
name. Your tests then run against one SQLite build rather than two: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:Serving the engine yourself#
wasmUrl points at a directory holding the three files of the SQLite
project's build. To serve them from your own origin:Copy the files#
$ mkdir -p public/vendor/sqlite
$ cd public/vendor/sqlite
$ curl -fsSLO https://prod-orm.io/cdn/sqlite/index.mjs
$ curl -fsSLO https://prod-orm.io/cdn/sqlite/sqlite3.wasm
$ curl -fsSLO https://prod-orm.io/cdn/sqlite/sqlite3-opfs-async-proxy.js
public/
vendor/
prorm.esm.min.js
sqlite/
index.mjs # text/javascript
sqlite3.wasm # application/wasm
sqlite3-opfs-async-proxy.js # the OPFS worker
index.html
Serve them with the right types#
Point the library at them#
[!IMPORTANT]
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.A complete offline page#
A notes page with no server: the notes live in SQLite, the last filter the
reader picked lives in localStorage, and the page says so when the browser
could not give it persistent storage.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: Modified at 2026-10-04 00:40:16