---
url: 'https://adk.nht.io/assembly/batteries-storage.md'
description: >-
  Bundled storage implementations for SpooledArtifact persistence: in-memory,
  flydrive, and OPFS.
---

# Storage batteries

## LLM summary — Storage batteries

* Three bundled storage implementations. All three implement `SpoolStore` (i.e. `ByteStore<SpoolReader>`) for `SpooledArtifact` persistence — not the 27-callback `TurnRunnerConfig` storage contract.
* `ByteStore<R>` is the unified low-level "give bytes, get a reader" contract. `write(id, bytes)` accepts `string | Uint8Array | ReadableStream<Uint8Array>` (string input is UTF-8-encoded). `SpoolStore = ByteStore<SpoolReader>` (text, line-indexed); `MediaStore = ByteStore<MediaReader>` (opaque binary).
* `@nhtio/adk/batteries/storage/in_memory` — `InMemorySpoolStore` and `InMemorySpoolReader`. Environment-neutral. Stores bytes byte-faithfully (a `Map<string, Uint8Array>`); the reader decodes on demand for line/text reads. Binary round-trips intact. Stream input is drained to a buffer (in-memory can't stream to disk).
* `@nhtio/adk/batteries/storage/flydrive` — `FlydriveSpoolStore` and `FlydriveSpoolReader`. Node/server runtime; not browser. Backed by a flydrive `Disk`. Peer dependency: `flydrive`. Stream input is forwarded to `disk.putStream` (streams to disk/object storage without buffering).
* `@nhtio/adk/batteries/storage/opfs` — `OpfsSpoolStore` and `OpfsSpoolReader`. Browser only. Backed by the Origin Private File System. The constructor accepts `{ directory, keyPrefix, streamThresholdBytes }` where `directory` is a callback returning a directory handle. The `write()` and `read()` methods do NOT accept a directory handle. Stream input is written chunk-by-chunk.
* Inject a store into a battery via the `spoolStore` option (`new OpenAIChatCompletionsAdapter({ ..., spoolStore })`); defaults to a per-dispatch `InMemorySpoolStore`. An injected durable store persists across turns — tool-call ids used as keys must be globally unique (or use the store's `keyPrefix`).
* Storage batteries are SPOOL implementations — they handle `SpooledArtifact` byte persistence. They are NOT a replacement for the 27 `TurnRunnerConfig` storage callbacks (Messages, Memories, ToolCalls, etc.). You still wire all 27 callbacks yourself.
* Use `InMemorySpoolStore` only in tests, prototypes, or where artifact loss on exit is acceptable. Use `FlydriveSpoolStore` or `OpfsSpoolStore` for durable spool-byte persistence.

::: danger These batteries persist artifact bytes only — not conversation state
The storage batteries implement the spool reader/writer pattern for [`SpooledArtifact`](https://adk.nht.io/api/@nhtio/adk/spooled_artifact/classes/SpooledArtifact) persistence. They are NOT a replacement for the 27 [`TurnRunnerConfig`](https://adk.nht.io/api/@nhtio/adk/turn_runner/interfaces/TurnRunnerConfig) storage callbacks. They do not persist Messages, Memories, or ToolCalls. Session state still requires all 27 callbacks. See [Bring Your Own Storage](./byo-storage).
:::

## They store artifact bytes. Nothing else.

When a tool handler returns a string or `Uint8Array`, the bundled OpenAI/WebLLM adapters spool it under the tool call's id and hand the model a [`SpooledArtifact`](https://adk.nht.io/api/@nhtio/adk/spooled_artifact/classes/SpooledArtifact). By default this goes to a fresh per-dispatch [`InMemorySpoolStore`](https://adk.nht.io/api/@nhtio/adk/batteries/storage/in_memory/classes/InMemorySpoolStore); pass the `spoolStore` option to back it with a durable store instead:

```typescript
import { OpfsSpoolStore } from '@nhtio/adk/batteries/storage/opfs'

const adapter = new OpenAIChatCompletionsAdapter({
  model: 'gpt-5.5',
  spoolStore: new OpfsSpoolStore({ directory: async () => navigator.storage.getDirectory() }),
})
```

If you are running in production, bytes must survive across requests, server restarts, or browser sessions. The storage batteries provide the persistence layer for these artifact bytes.

They do not persist `Message` records, `Memory` records, `ToolCall` metadata, or any of the other primitives in the 27-callback surface. Those are yours to implement. See [Bring Your Own Storage](./byo-storage).

::: code-group

```typescript [In-Memory]
import { InMemorySpoolStore } from '@nhtio/adk/batteries/storage/in_memory'

const store = new InMemorySpoolStore()
```

```typescript [Flydrive]
import { Disk } from 'flydrive'
import { FSDriver } from 'flydrive/drivers/fs'
import { FlydriveSpoolStore } from '@nhtio/adk/batteries/storage/flydrive'

const disk = new Disk(new FSDriver({ location: '/var/app/artifacts', visibility: 'public' }))
const store = new FlydriveSpoolStore(disk)
```

```typescript [OPFS]
import { OpfsSpoolStore, type OpfsDirectoryHandle } from '@nhtio/adk/batteries/storage/opfs'

const store = new OpfsSpoolStore({
  directory: async () => {
    const root = await navigator.storage.getDirectory()
    return (await root.getDirectoryHandle('agent-artifacts', { create: true })) as OpfsDirectoryHandle
  },
  keyPrefix: 'agent-runs-'
})
```

:::

***

## In-Memory (`@nhtio/adk/batteries/storage/in_memory`)

Environment-neutral. Works in Node, browsers, edge runtimes, and workers. Stores bytes byte-faithfully in a `Map<string, Uint8Array>`.

::: tip Binary is stored faithfully — but it lives in process memory
[`InMemorySpoolStore`](https://adk.nht.io/api/@nhtio/adk/batteries/storage/in_memory/classes/InMemorySpoolStore) stores raw bytes intact (binary payloads round-trip without corruption); [`InMemorySpoolReader`](https://adk.nht.io/api/@nhtio/adk/batteries/storage/in_memory/classes/InMemorySpoolReader) decodes on demand for line/text reads. The catch is durability, not fidelity: everything lives in process memory and is lost on exit. For artifacts that must survive restarts, use the Flydrive or OPFS batteries.
:::

```typescript
import { InMemorySpoolStore } from '@nhtio/adk/batteries/storage/in_memory'

const store = new InMemorySpoolStore()

// Write artifact bytes and get a reader
const reader = await store.write(callId, artifactBytes)

// Read previously written bytes
const existingReader = store.read(callId)  // returns InMemorySpoolReader | undefined
```

`write()` accepts a `string` (UTF-8-encoded), a `Uint8Array` (stored verbatim), or a `ReadableStream<Uint8Array>` (drained to a buffer — the stream form resolves asynchronously, so `await` the result), and returns an [`InMemorySpoolReader`](https://adk.nht.io/api/@nhtio/adk/batteries/storage/in_memory/classes/InMemorySpoolReader) bound to the stored bytes. `read()` returns a reader for a previously stored call ID, or `undefined` if not found.

Each call to `write()` or `read()` returns a fresh reader instance.

**Use this for:**

* Unit and integration tests
* Prototypes and quick CLI spikes
* Ephemeral environments where artifact loss on process exit is irrelevant

**Do not use this for:**

* Production deployments where artifacts must survive restarts or scale across multiple server instances

***

## Flydrive (`@nhtio/adk/batteries/storage/flydrive`)

Node/server runtime; not browser. Requires `flydrive` as a peer dependency. Backed by a flydrive `Disk` — which can point at the local filesystem or remote object storage backends (S3, GCS, etc.) via any supported flydrive driver.

Install the peer dependency first:

```bash
npm install flydrive
```

```typescript
import { Disk } from 'flydrive'
import { FSDriver } from 'flydrive/drivers/fs'
import { FlydriveSpoolStore } from '@nhtio/adk/batteries/storage/flydrive'

const disk = new Disk(new FSDriver({ location: '/var/app/artifacts', visibility: 'public' }))
const store = new FlydriveSpoolStore(disk)

// Write artifact bytes to the backing store
const reader = await store.write(callId, artifactBytes)

// Read previously written bytes
const existingReader = await store.read(callId)  // FlydriveSpoolReader | undefined
```

[`FlydriveSpoolStore`](https://adk.nht.io/api/@nhtio/adk/batteries/storage/flydrive/classes/FlydriveSpoolStore) is stateless — it owns no in-memory cache. Multiple store instances sharing the same `Disk` are safe.

[`FlydriveSpoolReader`](https://adk.nht.io/api/@nhtio/adk/batteries/storage/flydrive/classes/FlydriveSpoolReader) supports line access, byte-length reporting, line counts, and full decoded string reads. Large artifacts use streaming mode for line/index access, but `readAll()`/`asString()` loads the full decoded string into memory.

**Use this for:**

* Server-side production deployments
* Multi-process setups where artifacts need to be shared via a cloud storage driver

***

## OPFS (`@nhtio/adk/batteries/storage/opfs`)

Browser only. Uses the Origin Private File System — a sandboxed filesystem API available in modern browsers. No network required. Data persists in the browser's origin-private storage.

```typescript
import { OpfsSpoolStore, type OpfsDirectoryHandle } from '@nhtio/adk/batteries/storage/opfs'

const store = new OpfsSpoolStore({
  directory: async () => {
    const root = await navigator.storage.getDirectory()
    return (await root.getDirectoryHandle('agent-artifacts', { create: true })) as OpfsDirectoryHandle
  },
  keyPrefix: 'agent-runs-'
})

// Write artifact bytes
const reader = await store.write(callId, artifactBytes)

// Read previously written bytes
const existingReader = await store.read(callId) // OpfsSpoolReader | undefined
```

::: warning Correct API Usage
The constructor accepts the `directory` callback options configuration. Passing a directory handle to `write()` or `read()` is not supported; those methods do not accept it.
:::

[`OpfsSpoolStore`](https://adk.nht.io/api/@nhtio/adk/batteries/storage/opfs/classes/OpfsSpoolStore) works on the main thread and in Web Worker threads, using different internal code paths for each context. The public API is identical in both.

**Use this for:**

* Browser-embedded agents that need artifact persistence across page reloads
* Progressive web apps or browser extension-based agents

***

## Choosing the Right Battery

These recommendations apply **only for spool-byte persistence in production**. For the 27-callback storage adapter, go to [Bring Your Own Storage](./byo-storage).

| Environment | Battery |
| :--- | :--- |
| Tests and prototypes | [`InMemorySpoolStore`](https://adk.nht.io/api/@nhtio/adk/batteries/storage/in_memory/classes/InMemorySpoolStore) |
| Node server (development) | [`InMemorySpoolStore`](https://adk.nht.io/api/@nhtio/adk/batteries/storage/in_memory/classes/InMemorySpoolStore) |
| Node server (production) | [`FlydriveSpoolStore`](https://adk.nht.io/api/@nhtio/adk/batteries/storage/flydrive/classes/FlydriveSpoolStore) |
| Browser | [`OpfsSpoolStore`](https://adk.nht.io/api/@nhtio/adk/batteries/storage/opfs/classes/OpfsSpoolStore) |
| Edge / serverless | [`InMemorySpoolStore`](https://adk.nht.io/api/@nhtio/adk/batteries/storage/in_memory/classes/InMemorySpoolStore) (if artifact loss is acceptable) |

If your production setup needs artifact bytes to survive across requests or processes, use [`FlydriveSpoolStore`](https://adk.nht.io/api/@nhtio/adk/batteries/storage/flydrive/classes/FlydriveSpoolStore). If your agent runs in the browser, use [`OpfsSpoolStore`](https://adk.nht.io/api/@nhtio/adk/batteries/storage/opfs/classes/OpfsSpoolStore). If bytes are ephemeral and you are okay losing them on restart, [`InMemorySpoolStore`](https://adk.nht.io/api/@nhtio/adk/batteries/storage/in_memory/classes/InMemorySpoolStore) is valid for production too — it stores binary faithfully; the only caveat is durability.
