Core concepts
How Storage thinks — in plain language first, then the precise terms.
Previous: Tutorial · Next: Core
Beginner tip
If you have not run the Tutorial yet, do that first. This page explains why the APIs behave the way they do.
The big idea
Without Storage, apps usually do this:
- Pick a string key
JSON.stringifya value- Hope nobody else used the same key
- Hand-roll expiry and migrations later
Storage does steps 2–4 for you. You still pick a namespace (like a folder) and an adapter (where the bytes live). On every set, Storage wraps your value in an envelope (value + metadata) and writes that string to the adapter.
sequenceDiagram
participant App
participant Storage
participant Adapter
App->>Storage: set("theme", "dark")
Storage->>Storage: build envelope
Storage->>Adapter: setItem("app:theme", json)
App->>Storage: get("theme")
Storage->>Adapter: getItem("app:theme")
Adapter-->>Storage: json
Storage->>Storage: check expiry / migrate?
Storage-->>App: "dark" or nullGlossary (quick)
| Word | Meaning | Beginner need? |
|---|---|---|
| Namespace | Prefix for all keys (app:theme) | Yes — pick one per feature |
| Adapter | Backend: memory / local / session | Yes — choose where data lives |
| Envelope | Stored JSON: your value + savedAt / expiresAt / schemaVersion | Skim; use peek when curious |
| TTL | How long a write should live | Optional at first |
| Policy | Named TTL preset (preferences, cache) | Optional — convenience |
| Migrate | Upgrade old shapes on get | Advanced / when you ship breaking value changes |
Write path (what set does)
storage.set("theme", "dark", { policy: "preferences" });- Resolve TTL: per-write
ttl→ policy → instance default → none - Build envelope (
value,savedAt, optionalexpiresAt,schemaVersion) - Serialize and
adapter.setItem(${namespace}😒{key}, …)
You almost never touch the envelope on write — Storage builds it.
Read path (what get / peek / has do)
| Call | Returns | Migrates old schemas? | Drops expired? | When to use |
|---|---|---|---|---|
get(key) | Your value or null | Yes | Yes | Normal app reads |
peek(key) | Full envelope or null | No | Yes | Debug / show expiry |
has(key) | boolean | No | Yes | “Is anything there?” |
Soft null is normal
Missing, expired, or “migrate said drop” → null / false. That is not an exception. See Errors for what does throw.
Adapters (where data lives)
| Adapter | Survives reload? | Typical use |
|---|---|---|
createMemoryAdapter() | No | Unit tests, SSR |
createLocalStorageAdapter() | Yes | Prefs, cache |
createSessionStorageAdapter() | Until tab closes | Wizard / tab-scoped |
There is no auto-select — you pass the adapter on purpose.
Policies (optional shortcut)
Instead of repeating { ttl: { days: 365 } } everywhere:
policies: {
preferences: {
ttl: {
days: 365;
}
}
}
// …
storage.set("theme", "dark", { policy: "preferences" });Same as passing that TTL by hand. Unknown policy names throw (programmer error).
Subpaths (advanced tools)
Core stays small. Import extras only when you need them:
| Need | Import |
|---|---|
| Sweep expired keys + report | @jayoncode/storage/maintenance |
| Backup / restore namespace | @jayoncode/storage/snapshots |
| React to changes in-process | @jayoncode/storage/observable |
| DEV size / activity report | @jayoncode/storage/diagnostics |
| Multi-key write with rollback | @jayoncode/storage/transactions |
| IndexedDB / async API | @jayoncode/storage/async |
| Cross-tab notify | @jayoncode/storage/cross-tab |
| Soft quota (approx bytes) | @jayoncode/storage/quota |
| Compress / encrypt hooks | @jayoncode/storage/transforms |
Next
| Level | Go to |
|---|---|
| Still learning | Recipes |
| Need every option | Core |
| Handling failures | Errors |
