Stores
Persistent state stores for middleware
Stores are the persistence layer for stateful middleware. withRateLimit needs a counter store, withIdempotency needs a cache store. Better-Notify defines structural interfaces for each — you can use the built-in in-memory implementations for single-process apps or bring your own Redis-backed store for cross-worker coordination.
When to use stores
Middleware like withRateLimit and withIdempotency need to remember state across sends — how many sends happened in the current window, whether a key was already seen. Stores provide that memory through a clean interface that separates the middleware logic from the storage backend.
The built-in inMemory* stores work out of the box for development, testing, and single-process deployments. You need a custom store when:
- Multiple workers or instances — in-memory state is per-process. If you run two workers, each has its own counter and a recipient could receive double the intended rate limit. A Redis or database-backed store gives all instances a shared view.
- State that survives restarts — in-memory stores lose everything on process restart. If your idempotency window is 24 hours, a restart in the middle means duplicates slip through.
- Suppression across services — if bounces and complaints are handled by a different service, a shared suppression list lets the send pipeline check them without inter-service calls.
If you're running a single process and can tolerate state loss on restart, the in-memory stores are the right choice — no infrastructure to manage.
RateLimitStore
Used by withRateLimit to record and count sends per key within a time window.
Prop
Type
import { inMemoryRateLimitStore } from '@betternotify/core/stores';
const store = inMemoryRateLimitStore();Custom implementation
Use createRateLimitStore to build a store from any backend:
import { createRateLimitStore } from '@betternotify/core/stores';
const redisRateLimitStore = (redis: RedisClient) =>
createRateLimitStore({
record: async (key, windowMs, algorithm) => {
const redisKey = `rl:${key}`;
const count = await redis.incr(redisKey);
if (count === 1) await redis.pexpire(redisKey, windowMs);
const ttl = await redis.pttl(redisKey);
return { count, resetAtMs: Date.now() + Math.max(ttl, 0) };
},
});IdempotencyStore
Used by withIdempotency to cache send results and replay them for duplicate keys.
Prop
Type
import { inMemoryIdempotencyStore } from '@betternotify/core/stores';
const store = inMemoryIdempotencyStore();Custom implementation
Use createIdempotencyStore to build a store from get/set functions:
import { createIdempotencyStore } from '@betternotify/core/stores';
const redisIdempotencyStore = (redis: RedisClient) =>
createIdempotencyStore({
get: async (key) => {
const json = await redis.get(`idem:${key}`);
return json ? JSON.parse(json) : null;
},
set: async (key, result, ttlMs) => {
await redis.set(`idem:${key}`, JSON.stringify(result), 'PX', ttlMs);
},
});SuppressionList
Used to track suppressed email addresses — bounces, complaints, or manual blocks.
Prop
Type
import { inMemorySuppressionList } from '@betternotify/core/stores';
const list = inMemorySuppressionList();Custom implementation
Use createSuppressionList to build a list from any backend. The factory normalizes emails (trim + lowercase) before passing them to your functions:
import { createSuppressionList } from '@betternotify/core/stores';
const redisSuppressionList = (redis: RedisClient) =>
createSuppressionList({
get: async (email) => {
const json = await redis.get(`suppress:${email}`);
return json ? JSON.parse(json) : null;
},
set: async (email, entry) => {
await redis.set(`suppress:${email}`, JSON.stringify(entry));
},
del: async (email) => {
await redis.del(`suppress:${email}`);
},
});In-memory vs production
The inMemory* implementations are single-process and lose state on restart. They're ideal for development, testing, and single-instance deployments. For production with multiple workers, use the create* factories to wire up a shared backend (Redis, Postgres, DynamoDB) so all instances see the same state.