ADR-0006: Runtime API Separation
Date: 2026-07-28 Status: Accepted
Context
Code in a monorepo can be categorized by what runtime APIs it depends on:
| Category | APIs | Example |
|---|---|---|
| Pure | None — just JavaScript | SingletonPromise, regex validators, binary transformers |
| Browser | DOM, Web Crypto, window, navigator, IndexedDB | aesGcmEncrypt, downloadByUrl, getCPUCoreCount |
| Node | fs, path, crypto (Node), process | File system operations, OS-level utilities |
| Framework | Vue, React, NestJS decorators | useState, @Injectable(), defineComponent |
Mixing these categories in one package causes:
- Backend can't consume browser code (no DOM)
- Frontend can't consume Node code (no
fs) - Pure utilities get buried under framework dependencies
Decision
Packages are organized by their maximum runtime API dependency:
@walnut/utils ← Pure only (zero runtime APIs)
@walnut/client ← Browser APIs + Vue framework
@walnut-server/utils ← Node APIs only (backend-internal)
@walnut-server/* ← Node + NestJS framework (backend-internal)Within @walnut/client, code is further organized by API level:
client/src/
browser/ ← Browser APIs only (no Vue dependency)
crypto/ ← Web Crypto API
file/ ← DOM FileReader, Blob, Canvas
window/ ← window.btoa/atob
shared.ts ← navigator, device detection
hooks/ ← Vue composables (stateful, reactive)
persistent/ ← Storage subsystem (browser + Vue)This makes it immediately clear whether a function:
- Can run in any JS runtime (→
@walnut/utils) - Needs a browser (→
@walnut/client/browser/) - Needs Vue (→
@walnut/client/hooks/)
Alternatives considered
- One omnibus shared package that ignores runtime API categories —— the backend cannot consume browser code (no DOM) and the frontend cannot consume Node code (no
fs), and pure utilities get buried under framework dependencies. - A flat
@walnut/clientthat keeps all code at one level —— thebrowser//hooks//persistent/split is what makes it immediately clear whether a function can run in any JS runtime, needs a browser, or needs Vue.
Consequences
@walnut/utilshas zerovue,@vueuse,idb, or DOM dependencies. Onlyjs-base64.- Adding a new pure utility: goes to
@walnut/utils. Adding a browser crypto wrapper: goes to@walnut/client/browser/crypto/. - A backend developer can
require('@walnut/utils')without pulling in browser globals. - Future: if the project adds React/SSR/Electron, the separation maps cleanly — pure utils are reusable, browser utils need browser, Vue composables stay in client.