Storage Provider
Try this to switch storage backends:
Use SQLite for persistent team state
Try this to see where session data is stored:
Where is my squad data stored?
Try this to build a cloud storage backend:
Create a StorageProvider for Azure Blob Storage
All of Squad’s data — sessions, decisions, agent memories, event logs — flows through a pluggable storage interface. Pick the provider that matches your deployment: filesystem, database, or cloud.
What is StorageProvider?
StorageProvider is Squad’s I/O contract. Every read, write, delete, and directory operation goes through this interface. This decoupling means:
- Local development uses the filesystem. Sessions and state live in
.squad/. - Testing uses in-memory storage. No disk I/O, no test pollution.
- Production can use SQLite, cloud storage, or a database.
- Multi-team deployments can route different squads to different backends.
The interface is minimal — just 13 core async methods1:
read(filePath: string): Promise<string | undefined>
write(filePath: string, data: string): Promise<void>
createIfAbsent(filePath: string, data: string): Promise<void>
append(filePath: string, data: string): Promise<void>
exists(filePath: string): Promise<boolean>
list(dirPath: string): Promise<string[]>
delete(filePath: string): Promise<void>
deleteDir(dirPath: string): Promise<void>
isDirectory(targetPath: string): Promise<boolean>
mkdir(dirPath: string, options?: { recursive?: boolean }): Promise<void>
rename(oldPath: string, newPath: string): Promise<void>
copy(srcPath: string, destPath: string): Promise<void>
stat(targetPath: string): Promise<StorageStats | undefined>
createIfAbsent is the only conditional operation: it must create the key
atomically and only when absent, reject with StateKeyConflictError when the
key already exists, and reject with StateBackendUncertaintyError when the
outcome cannot be determined. It must never overwrite existing content.
See State Backends → Atomic Create-if-Absent.
Built-in Providers
FSStorageProvider
What it is: Node.js filesystem wrapper. Standard, portable, no setup.
When to use it:
- Local development
- Single-machine deployments
- Monorepo setups where squad data is part of the project
How it works: Maps all Squad paths to disk directories. Create parent directories on write. Returns undefined on ENOENT instead of throwing.
InMemoryStorageProvider
What it is: HashMap-backed, zero I/O.
When to use it:
- Unit tests for agent logic
- Ephemeral sessions that don’t need persistence
- CI environments where
.squad/is discarded
How it works: All paths stored in memory as POSIX strings. Fast, isolated, perfect for test fixtures.
SQLiteStorageProvider
What it is: SQLite-backed provider using sql.js (WASM). Single .db file, cross-platform.
When to use it:
- Small to medium teams
- Need a portable database file
- Windows/Linux/Mac without platform-specific binaries
How it works: Stores file content in a files(path, content, updated_at) table. sql.js runs entirely in WASM — no native compilation, no dependencies.
| Feature | FSStorageProvider | InMemoryStorageProvider | SQLiteStorageProvider |
|---|---|---|---|
| Persistence | Disk | None (ephemeral) | Single .db file |
| Setup | None | None | 1 import + init |
| Speed | Disk I/O latency | Instant (memory) | Query overhead |
| Portability | Windows/Linux/Mac | Yes | Yes |
| Suitable for | Development, production | Tests | Portable deployments |
Create a Custom Provider
Implement the StorageProvider interface to plug in any backend. Here’s a skeleton:
import type { StorageProvider, StorageStats } from '@bradygaster/squad-sdk';
export class MyCustomStorageProvider implements StorageProvider {
async read(filePath: string): Promise<string | undefined> {
// Fetch from your backend (S3, Cosmos, etc.)
// Return undefined if not found
}
async write(filePath: string, data: string): Promise<void> {
// Write to your backend
// Create parent directories as needed
}
async createIfAbsent(filePath: string, data: string): Promise<void> {
// Atomically create ONLY if absent — never overwrite.
// Throw StateKeyConflictError if the key already exists.
// Throw StateBackendUncertaintyError if the outcome is unknown.
// Use a native conditional primitive (O_EXCL open, INSERT OR IGNORE,
// If-None-Match: *, conditional PUT) — a read-then-write check is racy.
}
async append(filePath: string, data: string): Promise<void> {
// Append to a file, creating it if missing
}
async exists(filePath: string): Promise<boolean> {
// Check if path exists
}
async list(dirPath: string): Promise<string[]> {
// Return entry names in directory
// Return empty array if directory doesn't exist
}
async delete(filePath: string): Promise<void> {
// Delete a file (no-op if missing)
}
async deleteDir(dirPath: string): Promise<void> {
// Recursively delete directory (no-op if missing)
}
async isDirectory(targetPath: string): Promise<boolean> {
// Return true if path is a directory
}
async mkdir(dirPath: string, options?: { recursive?: boolean }): Promise<void> {
// Create directory
}
async rename(oldPath: string, newPath: string): Promise<void> {
// Move/rename file or directory
}
async copy(srcPath: string, destPath: string): Promise<void> {
// Copy file, creating parent dirs for destination
}
async stat(targetPath: string): Promise<StorageStats | undefined> {
// Return file metadata: size, mtime, isDirectory
// Return undefined if path doesn't exist
}
// Sync variants (deprecated, but still required for Wave 1 compat)
readSync(filePath: string): string | undefined { /* ... */ }
writeSync(filePath: string, data: string): void { /* ... */ }
appendSync(filePath: string, data: string): void { /* ... */ }
existsSync(filePath: string): boolean { /* ... */ }
listSync(dirPath: string): string[] { /* ... */ }
deleteSync(filePath: string): void { /* ... */ }
deleteDirSync(dirPath: string): void { /* ... */ }
isDirectorySync(targetPath: string): boolean { /* ... */ }
mkdirSync(dirPath: string, options?: { recursive?: boolean }): void { /* ... */ }
renameSync(oldPath: string, newPath: string): void { /* ... */ }
copySync(srcPath: string, destPath: string): void { /* ... */ }
statSync(targetPath: string): StorageStats | undefined { /* ... */ }
}
Pass it to the runtime:
import { SquadClient } from '@bradygaster/squad-sdk';
const client = new SquadClient({
storageProvider: new MyCustomStorageProvider(),
teamRoot: '.squad',
});
See storage-provider-azure and storage-provider-sqlite samples for complete, production-ready implementations.
Choose the Right Provider
| Goal | Provider |
|---|---|
| Local development | FSStorageProvider |
| Unit testing agents | InMemoryStorageProvider |
| Small team, portable DB | SQLiteStorageProvider |
| Scale across multiple machines | Custom provider (your database, blob store, or message queue) |
| Azure Blob Storage | Use storage-provider-azure sample as reference |
| DynamoDB, Firestore, etc. | Implement StorageProvider — the interface maps cleanly |
Sample Projects
- storage-provider-sqlite — Complete SQLite implementation using sql.js
- storage-provider-azure — Azure Blob Storage backend with connection pooling
Both live in /samples and demonstrate patterns for production providers.
Footnotes
-
The full interface also includes 12 deprecated synchronous variants (
readSync,writeSync,appendSync,existsSync,listSync,deleteSync,deleteDirSync,isDirectorySync,mkdirSync,renameSync,copySync,statSync) — 24 methods total. The sync methods exist for backward compatibility and will be removed in Wave 2. New code should use the async methods exclusively. ↩
