@frontmcp/storage-sqlite
SQLite storage for FrontMCP: sessions, background tasks, pending elicitations and stream events in a local file, so they survive a restart without running Redis. Install it next to @frontmcp/sdk and set @FrontMcp({ sqlite }); the SDK loads it. Use it on one machine: local development, a single VM or container with a volume, a CLI or desktop server, or the cli task runner, whose worker processes share the file. When several machines must see the same state, use Redis instead: a SQLite file can't be shared across machines.
Install
npm install @frontmcp/storage-sqlite
Requires Node.js 24 or later. better-sqlite3 is a regular dependency, so it's installed with the package. It's a native module that downloads or compiles its binary at install time, so install on the platform the server runs on (in a Docker image, run npm ci inside the image). The peer dependency @frontmcp/lazy-zod comes with @frontmcp/sdk.
@frontmcp/sdk loads this package only when a sqlite option is set, so importing the SDK never loads better-sqlite3. On Cloudflare Workers the stores run in a Durable Object, on its own SQLite database: see In a Cloudflare Durable Object. Other V8-isolate runtimes have no SQLite; use Redis or Upstash there.
Usage
Most servers never import this package. Point FrontMCP at a file and the SDK builds the stores:
import { App, FrontMcp } from '@frontmcp/sdk';
@App({ id: 'help-desk', name: 'Help Desk', tools: [] })
class HelpDeskApp {}
@FrontMcp({
info: { name: 'help-desk', version: '1.0.0' },
apps: [HelpDeskApp],
sqlite: { path: './data/desk.sqlite' },
})
export default class Server {}
The top-level sqlite block backs sessions, background tasks and pending elicitations. Without path, the file is <project>/dist/sessions.sqlite outside production and ~/.<info.name>/sessions.sqlite in production. FRONTMCP_SQLITE_PATH overrides both, and path too.
To give one subsystem its own file, set it there instead:
import { App, FrontMcp } from '@frontmcp/sdk';
@App({ id: 'help-desk', name: 'Help Desk', tools: [] })
class HelpDeskApp {}
@FrontMcp({
info: { name: 'help-desk', version: '1.0.0' },
apps: [HelpDeskApp],
transport: { persistence: { sqlite: { path: './data/sessions.sqlite' } } },
tasks: { enabled: true, runner: 'cli', sqlite: { path: './data/tasks.sqlite' } },
})
export default class Server {}
The cli task runner starts detached worker processes, so it needs a store they share: tasks.sqlite, a top-level sqlite, or tasks.redis.
Using a store directly
import { SqliteSessionStore } from '@frontmcp/storage-sqlite';
const store = new SqliteSessionStore({ path: './data/sessions.sqlite', defaultTtlMs: 3_600_000 });
await store.set('session-id', { userId: 'u1' }, 60 * 60_000); // TTL in milliseconds
const session = await store.get('session-id');
store.close();
Stores
| Export | Backs |
|---|---|
SqliteSessionStore | MCP transport sessions (keyPrefix, default 'mcp:session:'; defaultTtlMs, default 1 hour) |
SqliteTaskStore | Background tasks: status, outcome and cancellation |
SqliteElicitationStore | Pending elicitation requests (keyPrefix, default 'mcp:elicit:') |
SqliteEventStore | Stream events for Streamable HTTP resumption (maxEvents, default 10000; ttlMs, default 5 minutes) |
SqliteKvStore | A synchronous key/value store with TTL, which the stores above are built on |
SqliteStorageAdapter | The async StorageAdapter interface of @frontmcp/utils over the same file (no publish/subscribe) |
SqliteStorageAdapter.keys(pattern) and SqliteKvStore.keys(pattern) match letter case, as the memory and Redis storages do: keys('session:*') does not return Session:1. Up to 1.12.0 they ignored ASCII case, because they searched with SQL LIKE, and an adapter pattern lost its backslashes. In an adapter pattern a backslash makes the next character literal, as in the other storages, so escapeGlob() output finds the same keys everywhere; in 1.12.1 a backslash was matched as itself.
The package also exports openDatabase and withBusyRetry (helpers for opening a file and retrying on SQLITE_BUSY), useSqliteDriver and durableObjectSqlite (a SQLite other than better-sqlite3, below), deriveEncryptionKey, encryptValue, decryptValue and SqliteDecryptionError, and sqliteStorageOptionsSchema, the Zod schema for the options below.
Options
Every store takes these options, and so do @FrontMcp's sqlite, transport.persistence.sqlite and tasks.sqlite:
| Option | Default | Notes |
|---|---|---|
path | required | The database file. Its folder is created if it's missing. Optional in @FrontMcp({ sqlite }), as above |
encryption | none | { secret }: encrypt every stored value with AES-256-GCM, with a key derived from secret. Keys stay readable |
walMode | true | SQLite's write-ahead log, for readers that don't wait for writers |
ttlCleanupIntervalMs | 60000 | How often expired entries are deleted |
busyTimeoutMs | 5000 | How long a write waits for a lock another process holds before it fails with SQLITE_BUSY |
A lost encryption.secret loses the data, and a changed one makes stored values unreadable (SqliteDecryptionError).
Operational notes
- Back up the file. It's an ordinary SQLite database: copy it, or use
sqlite3 .backup. - Write-ahead logging adds two files next to it,
-waland-shm. Copy all three, or checkpoint first. - One writer at a time. Several processes can read the file; writes wait for each other, up to
busyTimeoutMs.
In a Cloudflare Durable Object
A Durable Object with SQLite storage has its own database, and the stores can run on it instead of better-sqlite3, which is a native module and doesn't load on Workers. Import this package in the Worker, hand it to the SDK, and set the driver in the Durable Object's constructor, before the server is created:
import { DurableObject } from 'cloudflare:workers';
import { FrontMcpInstance, registerOptionalModule } from '@frontmcp/sdk';
import * as storageSqlite from '@frontmcp/storage-sqlite';
registerOptionalModule('@frontmcp/storage-sqlite', storageSqlite);
export class HelpDeskServer extends DurableObject {
private readonly handler: ReturnType<typeof FrontMcpInstance.createFetchHandler>;
constructor(ctx: DurableObjectState, env: unknown) {
super(ctx, env);
storageSqlite.useSqliteDriver(storageSqlite.durableObjectSqlite(ctx.storage));
this.handler = FrontMcpInstance.createFetchHandler({
info: { name: 'help-desk', version: '1.0.0' },
apps: [HelpDeskApp],
events: { store: { sqlite: { path: 'help-desk.sqlite' } } },
});
}
async fetch(request: Request): Promise<Response> {
return (await this.handler)(request, this.ctx, this.env);
}
}
registerOptionalModule()gives the SDK the package from the Worker's bundle: on Workers,require()can't reach a bundled module.useSqliteDriver(durableObjectSqlite(ctx.storage))makes every store open the Durable Object's database.createFetchHandler()builds the server on its first request, so the constructor is early enough. Each instance of the Durable Object sets its own driver, and a store never gets a connection that an earlier instance opened.- One instance per isolate. The driver is set for the whole isolate, so use it in a Durable Object class that has one instance (one id, as
idFromName('main')). Instances with different ids can share an isolate, and each server opens its stores on its first request: the storage set last then opens every instance's stores, Cloudflare refuses the I/O (Cannot perform I/O on behalf of a different Durable Object), and the other instances' servers don't start. - Every
pathis the same database, the Durable Object's one. The option only names it, and no file or folder is created. - Writes run in
ctx.storage.transactionSync(), andwalModeandbusyTimeoutMsdo nothing: a Durable Object has one connection and refuses both pragmas.
useSqliteDriver() takes any function from a path to a database with the part of better-sqlite3's Database the stores call (the SqliteDriver and SqliteDatabase types), and useSqliteDriver(undefined) goes back to better-sqlite3. currentSqliteDriver() returns the driver that is set, if any; on an edge runtime the SDK opens a sqlite store only when one is.
Docs
- SQLite: the
sqliteoption, where the file goes, what's stored, locking and encryption - Redis: the store to use when several machines share state
- Background tasks: task runners and their stores
Related packages
@frontmcp/sdk: the framework that loads this package for itssqliteoptions@frontmcp/utils: theStorageAdapterinterfaceSqliteStorageAdapterimplements, and the other storage backends
Contributing
Source: libs/storage-sqlite. Issues and pull requests are welcome; see CONTRIBUTING.md.
License
Apache-2.0. See LICENSE.