Custom Storage

Swap storage backends behind small app-owned interfaces

Most v3 apps should use the dedicated service clients directly: KeyValueClient, VectorClient, StreamClient, and @agentuity/storage. When a feature needs another backend, hide that decision behind a small app-owned interface.

import { KeyValueClient } from '@agentuity/keyvalue';
 
interface Session {
  readonly userId: string;
  readonly email: string;
}
 
interface SessionStore {
  get(id: string): Promise<Session | undefined>;
  set(id: string, session: Session): Promise<void>;
}
 
class AgentuitySessionStore implements SessionStore {
  readonly #kv = new KeyValueClient();
 
  async get(id: string): Promise<Session | undefined> {
    const result = await this.#kv.get<Session>('sessions', id);
    return result.exists ? result.data : undefined;
  }
 
  async set(id: string, session: Session): Promise<void> {
    await this.#kv.set('sessions', id, session, {
      ttl: 60 * 60 * 24 * 30,
    });
  }
}

Keep the app interface as small as the feature needs. A session store usually does not need search, stats, namespace deletion, or the full storage API.

When to customize

NeedPattern
use Agentuity-managed storageinstantiate the dedicated client directly
swap Redis, Postgres, or another backenddefine an app-owned interface and provide another implementation
share the complete Agentuity method shapeimplement the exported storage interface
use a different S3-compatible object storepass explicit credentials to @agentuity/storage or a runtime-native S3 client
test without managed credentialsuse fakes, mocks, or Bun-only local implementations

Dedicated Clients

Direct clients are the default v3 path. They read AGENTUITY_SDK_KEY, then AGENTUITY_CLI_KEY, and also accept explicit apiKey, url, orgId, and logger options.

import { KeyValueClient } from '@agentuity/keyvalue';
import { StreamClient } from '@agentuity/stream';
import { VectorClient } from '@agentuity/vector';
 
const kv = new KeyValueClient();
const streams = new StreamClient();
const vector = new VectorClient();
 
const kvForAnotherOrg = new KeyValueClient({
  orgId: 'org_123',
});

Use constructor options when you are writing admin scripts, tests, or multi-org tools. Most application code should let the client read project environment variables.

Full Storage Interfaces

The dedicated packages re-export the complete service interfaces from @agentuity/core. Use these when you want a drop-in adapter with the same method names as Agentuity storage.

import type { KeyValueStorage } from '@agentuity/keyvalue';
import type { StreamStorage } from '@agentuity/stream';
import type { VectorStorage } from '@agentuity/vector';
InterfaceCore methods
KeyValueStorageget, set, delete, search, getKeys, getStats, namespace methods
VectorStorageupsert, get, getMany, search, delete, exists, stats and namespace methods
StreamStoragecreate, get, download, list, delete

Only implement these full interfaces when callers need full compatibility. For most apps, a smaller interface is easier to test and harder to misuse.

Object Storage

Object storage is not part of the key-value, vector, or stream interface set. Use @agentuity/storage for the Agentuity bucket linked to your project. In Bun-only code, use S3Client when you need Bun-specific helpers or another S3-compatible provider.

import { S3Client } from 'bun';
 
function requireEnv(name: string): string {
  const value = process.env[name];
 
  if (!value) {
    throw new Error(`${name} is required`);
  }
 
  return value;
}
 
const assets = new S3Client({
  accessKeyId: requireEnv('ASSETS_ACCESS_KEY'),
  secretAccessKey: requireEnv('ASSETS_SECRET_KEY'),
  bucket: 'assets',
  endpoint: 'https://example.r2.cloudflarestorage.com',
});
 
await assets.write('healthcheck.txt', 'ok', { type: 'text/plain' });

Use a runtime-native S3-compatible client instead when the code does not run on Bun.

Local Tests

For unit tests, prefer a small fake that matches your app-owned interface. That keeps tests independent from managed credentials and avoids pulling in storage behavior your feature does not use.

class MemorySessionStore implements SessionStore {
  readonly #sessions = new Map<string, Session>();
 
  async get(id: string): Promise<Session | undefined> {
    return this.#sessions.get(id);
  }
 
  async set(id: string, session: Session): Promise<void> {
    this.#sessions.set(id, session);
  }
}

Next Steps