A physical store is the minimal interface for persisting a collection of entities — create, read, filter, update, delete, count. Stores don’t know about authorization, relations, or business logic. That lives one layer up, in DataSets.
A StoreToken is a DI token that resolves to a physical store and carries the store’s model and primaryKey as metadata. Backend adapter packages ship dedicated helpers that mint the right token for their flavour.
Declaring a store
import { defineStore, InMemoryStore } from '@furystack/core';
class TodoItem {
declare id: string;
declare title: string;
declare completed: boolean;
}
export const TodoStore = defineStore<TodoItem, 'id'>({
name: 'my-app/TodoStore',
model: TodoItem,
primaryKey: 'id',
factory: () => new InMemoryStore({ model: TodoItem, primaryKey: 'id' }),
});
defineStore wraps defineService({ lifetime: 'singleton' }) and registers an onDispose so the store cleans itself up when the injector is disposed.
Tip: Pass the generics explicitly (
<TodoItem, 'id'>). Inferred generics inside helper wrappers tend to widen'id'back tokeyof T, which loses the literal primary-key type.
Resolving the store
import { createInjector } from '@furystack/inject';
const injector = createInjector();
const store = injector.get(TodoStore);
await store.add({ id: '1', title: 'first', completed: false });
Heads up: Resolving a
StoreTokendirectly in application code is a smell — data should flow through DataSets, not raw stores. Thefurystack/no-direct-store-tokenlint rule flags this. See Repository.
Backend adapters
Each adapter exports a defineXxxStore<T, PK>(opts) helper:
| Package | Helper | Use when… |
|---|---|---|
@furystack/core | InMemoryStore factory | Tests, demos, ephemeral state. |
@furystack/filesystem-store | defineFileSystemStore | Development, prototypes, low-volume persistence. |
@furystack/sequelize-store | defineSequelizeStore | Any SQL DB via Sequelize (Postgres, MySQL, SQLite). |
@furystack/mongodb-store | defineMongoDbStore | MongoDB document storage. |
@furystack/redis-store | defineRedisStore | Redis key-value storage (e.g. shared sessions). |
defineFileSystemStore
import { defineFileSystemStore } from '@furystack/filesystem-store';
export const TodoStore = defineFileSystemStore<TodoItem, 'id'>({
name: 'my-app/TodoStore',
model: TodoItem,
primaryKey: 'id',
fileName: './data/todos.json',
tickMs: 5000, // Optional: throttle disk writes to once per N ms
});
defineMongoDbStore
import { defineMongoDbStore } from '@furystack/mongodb-store';
export const TodoStore = defineMongoDbStore<TodoItem, 'id'>({
name: 'my-app/TodoStore',
model: TodoItem,
primaryKey: 'id',
url: process.env.MONGODB_URL!,
db: 'my-app',
collection: 'todos',
});
The shared MongoClientFactory token pools clients per URL and closes them all on injector teardown — no connection leaks across tests.
defineRedisStore
import { createClient } from 'redis';
import { defineRedisStore } from '@furystack/redis-store';
const redisClient = await createClient({ url: process.env.REDIS_URL }).connect();
export const SessionStore = defineRedisStore<Session, 'sessionId'>({
name: 'my-app/SessionStore',
model: Session,
primaryKey: 'sessionId',
client: redisClient,
});
The caller owns the redis client lifecycle — connect at startup, quit() on shutdown.
defineSequelizeStore
import { DataTypes, Model } from 'sequelize';
import { defineSequelizeStore } from '@furystack/sequelize-store';
class TodoModel extends Model {}
export const TodoStore = defineSequelizeStore<TodoItem, typeof TodoModel, 'id'>({
name: 'my-app/TodoStore',
model: TodoItem,
sequelizeModel: TodoModel,
primaryKey: 'id',
options: { dialect: 'postgres' /* ... */ },
initModel: ({ model }) => {
model.init(
{
id: { type: DataTypes.STRING, primaryKey: true },
title: DataTypes.STRING,
completed: DataTypes.BOOLEAN,
},
{ sequelize: model.sequelize!, tableName: 'todos' },
);
},
});
The shared SequelizeClientFactory token pools clients per JSON.stringify(options) key and disposes them on teardown.
Throw-by-default stores
Some packages ship StoreTokens that throw when resolved without a binding. This is intentional — they represent stores the framework needs but cannot pick on the application’s behalf. Examples: UserStore and SessionStore from @furystack/rest-service; RefreshTokenStore from @furystack/auth-jwt; PasswordCredentialStore and PasswordResetTokenStore from @furystack/security.
Bind a concrete implementation at app bootstrap:
import { UserStore } from '@furystack/rest-service';
import { defineSequelizeStore } from '@furystack/sequelize-store';
const AppUserStore = defineSequelizeStore<User, typeof UserModel, 'username'>({
name: 'my-app/AppUserStore',
model: User,
sequelizeModel: UserModel,
primaryKey: 'username',
options: { dialect: 'postgres' /* ... */ },
});
injector.bind(UserStore, ctx => ctx.inject(AppUserStore));
In tests, bind an InMemoryStore per scope:
injector.bind(UserStore, () => new InMemoryStore({ model: User, primaryKey: 'username' }));
Where to look next
- Repository — wrap a
StoreTokenin aDataSetwith authorization and events. - Dependency Injection — how
defineServiceand tokens work in general.