Gathering detailed insights and metrics for fullstack-simple-persist
Gathering detailed insights and metrics for fullstack-simple-persist
Gathering detailed insights and metrics for fullstack-simple-persist
Gathering detailed insights and metrics for fullstack-simple-persist
Tiny self-hosted state store for Express + React. Realtime sync, last‑write‑wins, no need for CRUD boilerplate.
npm install fullstack-simple-persist
Typescript
Module System
Node Version
NPM Version
62.8
Supply Chain
97.4
Quality
87.7
Maintenance
100
Vulnerability
100
License
TypeScript (98.36%)
JavaScript (1.64%)
Built with Next.js • Fully responsive • SEO optimized • Open source ready
Total Downloads
0
Last Day
0
Last Week
0
Last Month
0
Last Year
0
MIT License
7 Commits
1 Branches
1 Contributors
Updated on Aug 17, 2025
Latest Version
1.0.0-rc.7
Package Id
fullstack-simple-persist@1.0.0-rc.7
Unpacked Size
130.80 kB
Size
20.79 kB
File Count
36
NPM Version
10.9.2
Node Version
22.15.0
Published on
Aug 17, 2025
Cumulative downloads
Total Downloads
Last Day
0%
NaN
Compared to previous day
Last Week
0%
NaN
Compared to previous week
Last Month
0%
NaN
Compared to previous month
Last Year
0%
NaN
Compared to previous year
1
Tiny self-hosted framework-agnostic state store with extremely simple to use Express and React adapters. Realtime sync, last‑write‑wins, no CRUD boilerplate.
Simple Persist gives you two primitives—KeyValue and Collection—that you can drop into any Express app and bind to React with one Provider + one hook. Under the hood, it’s split into lightweight adapters (Express/React) on top of vanilla cores (server/client) so you can wire it to any framework.
persistKeyValue
, persistCollection
).createPersistKeyValue
, createPersistCollection
).KeyValueService
, CollectionService
, and UpdateHub
for building your own adapter (Fastify, Hono, Koa, native http
, etc.).KeyValueClient
, CollectionClient
, SyncSession
if you want to use Svelte/Vue/Vite SSR or plain JS.fetch
+ EventSource
on the client1npm i fullstack-simple-persist node-persist
1import express from 'express'; 2import { persistCollection, persistKeyValue } from 'fullstack-simple-persist/express'; 3 4const app = express(); 5 6// KeyValue store (optional validation + multi-tenant) 7app.use('/api/kv', persistKeyValue('settings', { 8 validation: (key, value) => typeof key === 'string', 9 getTenant: (req) => (req as any).user?.id || 'default', 10})); 11 12// Collection store (array of objects with { id: string, ... }) 13app.use('/api/todos', persistCollection('todos', { 14 validation: (item) => typeof item?.id === 'string' && typeof item?.text === 'string', 15})); 16 17app.listen(3000);
1import React from 'react'; 2import { createPersistCollection, createPersistKeyValue } from 'fullstack-simple-persist/react'; 3 4const { PersistKeyValue, useKeyValue } = createPersistKeyValue('/api/kv'); 5const { PersistCollection, useCollection } = createPersistCollection('/api/todos'); 6 7export default function App() { 8 return ( 9 <PersistKeyValue> 10 <PersistCollection> 11 <UI /> 12 </PersistCollection> 13 </PersistKeyValue> 14 ); 15} 16 17function UI() { 18 // KeyValue 19 const [username, setUsername] = useKeyValue('username'); 20 const [kvAll, { setAll: setKvAll, setMany, setKey, deleteKey }] = useKeyValue(); 21 22 // Collection 23 const [items, { setItems, setItem, updateItem, deleteItem, addItem }] = useCollection(); 24 25 return ( 26 <div> 27 <input value={username ?? ''} onChange={(e) => setUsername(e.target.value)} /> 28 <pre>{JSON.stringify(kvAll, null, 2)}</pre> 29 30 <button onClick={() => addItem({ text: 'New', done: false })}>Add Todo</button> 31 <pre>{JSON.stringify(items, null, 2)}</pre> 32 </div> 33 ); 34}
key -> value
.id: string
(UUID by default). Non-object items are wrapped as { id, value }
.getTenant
, everything goes into the default
tenant./__events
(Server‑Sent Events). Any write triggers an event.Simple Persist is split into vanilla cores and adapters:
fullstack-simple-persist/server
— framework‑agnostic backend primitives:
KeyValueService
, CollectionService
(business logic + storage)UpdateHub
(per‑tenant event emitter)NodePersistAdapter
(default storage; swappable)fullstack-simple-persist/express
— Express router that wires services + SSE.
fullstack-simple-persist/client
— framework‑agnostic client:
KeyValueClient
, CollectionClient
(HTTP helpers)SyncSession
(SSE + polling)randomId
, ensureIdsClient
helpersfullstack-simple-persist/react
— React Provider + hooks built on the vanilla client.
This makes it trivial to add adapters for Fastify/Hono/Koa or Vue/Svelte/etc. without touching core logic.
persistKeyValue(name, options)
Mounts a router that stores key–value pairs under a tenant‑scoped directory.
1import { persistKeyValue } from 'fullstack-simple-persist/express'; 2app.use('/api/kv', persistKeyValue('settings', { 3 validation?: (key: string, value: any) => boolean, 4 getTenant?: (req, res) => string | Error, 5 baseDir?: string, // default '.data' 6}));
Routes exposed
GET /
→ { data: Record<string, any>, version }
GET /:key
→ { key, value, version }
or 404PUT /:key
body: { value }
→ upsertDELETE /:key
→ deletePOST /_bulk
body: { upsert?: Record<string, any>, delete?: string[] }
→ per‑key merge without clobbering unrelated keysGET /__events
→ SSE stream (internal; used by the client)persistCollection(name, options)
Mounts a router that stores an array of objects with id
.
1import { persistCollection } from 'fullstack-simple-persist/express'; 2app.use('/api/todos', persistCollection('todos', { 3 validation?: (item: any) => boolean, 4 getTenant?: (req, res) => string | Error, 5 baseDir?: string, // default '.data' 6}));
Routes exposed
GET /
→ { data: any[], version }
(auto‑migrates legacy items to include id
)PUT /
body: { data: any[] }
→ replace full array (last‑write‑wins)POST /item
body: { item }
→ add/upsert single item (auto‑id if missing)PUT /item/:id
body: { item }
→ replace item by id (no merge)PATCH /item/:id
body: { patch }
→ shallow merge into item by id (upsert if missing)DELETE /item/:id
→ delete item by idGET /__events
→ SSE streamStorage engine: [node‑persist] under the hood; per‑store, per‑tenant directories. You don’t need to configure it unless you want to change
baseDir
.
createPersistKeyValue(endpoint)
Creates a Provider + hook pair bound to your KeyValue endpoint.
1const { PersistKeyValue, useKeyValue } = createPersistKeyValue('/api/kv');
Provider
1<PersistKeyValue>{children}</PersistKeyValue>
Hook
useKeyValue(key)
→ [value, setValue]
useKeyValue()
→ [map, { setAll, setMany, setKey, deleteKey }]
Notes
setAll(map)
merges keys on the server using /_bulk
upserts (no mass delete by default).setMany(map)
is an alias of setAll
.setKey(key, value)
and deleteKey(key)
target one key.createPersistCollection(endpoint)
Creates a Provider + hook pair bound to your Collection endpoint.
1const { PersistCollection, useCollection } = 2 createPersistCollection('/api/todos');
Provider
1<PersistCollection>{children}</PersistCollection>
Hook
1const [items, { setItems, setItem, updateItem, deleteItem, addItem }] = 2 useCollection();
setItems(next[])
→ replace the whole array (LWW)setItem(id, item)
→ replace object by id (no merge)updateItem(id, patch)
→ shallow merge into object by iddeleteItem(id)
→ remove by idaddItem(item)
→ add (auto‑id if missing)You can build your own backend adapter on top of the vanilla services.
KeyValueService
1import { KeyValueService, UpdateHub } from 'fullstack-simple-persist/server'; 2 3const hub = new UpdateHub(); 4const kv = new KeyValueService('settings', { baseDir: '.data' }, hub); 5 6// Example within any HTTP handler 7await kv.put(tenant, 'theme', 'dark'); 8const map = await kv.getAll(tenant); 9await kv.bulk(tenant, { locale: 'en-GB' });
CollectionService
1import { CollectionService, UpdateHub } from 'fullstack-simple-persist/server'; 2 3const hub = new UpdateHub(); 4const todos = new CollectionService('todos', {}, hub); 5 6const all = await todos.getAll(tenant); 7const saved = await todos.add(tenant, { text: 'New' }); 8await todos.patch(tenant, saved.id, { done: true }); 9await todos.put(tenant, saved.id, { text: 'Replace entirely' }); 10await todos.del(tenant, saved.id);
UpdateHub
(SSE helper)Use hub.on(scope, cb)
to subscribe and hub.emit(scope, payload)
to notify. For SSE, the scope is typically ${type}:${name}:${tenant}
(e.g., kv:settings:alice
).
1import { UpdateHub } from 'fullstack-simple-persist/server'; 2 3const hub = new UpdateHub(); 4const off = hub.on('kv:settings:alice', (payload) => { 5 // write to SSE response: `event: update` + `data: ${JSON.stringify(payload)}` 6}); 7// call off() when connection closes
If you’re not on React, use the vanilla client.
KeyValueClient
1import { KeyValueClient } from 'fullstack-simple-persist/client'; 2 3const kv = new KeyValueClient('/api/kv'); 4await kv.setKey('username', 'alice'); 5await kv.bulk({ theme: 'dark' });
CollectionClient
1import { CollectionClient } from 'fullstack-simple-persist/client'; 2 3const todos = new CollectionClient('/api/todos'); 4const item = await todos.add({ text: 'A' }); 5await todos.updateItem(item.id, { done: true }); 6await todos.setItem(item.id, { text: 'B' });
SyncSession
1import { SyncSession } from 'fullstack-simple-persist/client'; 2 3const session = new SyncSession('/api/kv', (data) => { 4 // update your UI state with new data 5}); 6session.fetchAll(); 7session.startSSE(); 8// session.stop() on teardown
endpoint/__events
; any write triggers a refresh. There’s also a polling fallback./_bulk
for multi‑key updates so other clients’ keys aren’t clobbered.PUT
is still available when you intend to replace everything.updateItem
, addItem
for fewer conflicts).Validation is optional and runs on the server after IDs are ensured:
1persistKeyValue('settings', { 2 validation: (key, value) => typeof key === 'string' && value != null, 3}); 4 5persistCollection('todos', { 6 validation: (item) => 7 typeof item?.id === 'string' && typeof item?.text === 'string', 8});
Responds with 422 { error: 'validation failed' }
if a value doesn’t pass.
Provide getTenant(req, res)
to scope data per user/org. Return a string or an Error
(which results in 401
). If omitted, tenant is 'default'
.
1persistCollection('todos', { 2 getTenant: (req) => { 3 const user = (req as any).user; 4 return user ? user.id : new Error('Unauthenticated'); 5 }, 6});
1// Replace entire object
2await setItem(id, { text: 'Buy milk', done: true });
3
4// Shallow merge
5await updateItem(id, { done: false });
6
7// Add auto-id
8await addItem({ text: 'Read docs', done: false });
1// Merge without deleting other keys 2await setMany({ theme: 'dark', locale: 'en-GB' }); 3 4// Single key 5await setKey('username', 'alice'); 6await deleteKey('oldKey');
You can add type annotations to your stores for autocompletion and type safety.
1// KeyValue store 2const { PersistKeyValue, useKeyValue } = createPersistKeyValue<{ 3 setting: 'foo' | 'bar'; 4}>('/api/keyvalue'); 5 6const [setting, setSetting] = useKeyValue('setting'); // ['foo' | 'bar', (next: 'foo' | 'bar') => Promise<void>] 7 8setSetting('baz'); // error: Argument of type '"baz"' is not assignable to parameter of type '"foo" | "bar"' 9 10// Collection store 11type Todo = { id: string; foo: string; bar: number }; 12const { PersistCollection, useCollection } = 13 createPersistCollection<Todo>('/api/todos'); 14 15const [todos, { setItems, setItem, updateItem, deleteItem, addItem }] = 16 useCollection(); 17 18addItem({ id: '1', foo: 'bar', bar: 1 }); // ok 19addItem({ id: '1', foo: 2, bar: 1 }); // error: type 'number' is not assignable to type 'string'
What about auth? Use your existing Express auth (cookies/sessions/JWT). If getTenant
returns an Error
, writes are rejected with 401
.
Does it work offline? Not yet. You’ll still get polling + SSE when online.
Can I bring my own storage? Yes. The server core uses an adapter interface. We ship NodePersistAdapter
by default; you can implement your own adapter with the same methods (init
, keys
, getItem
, setItem
, removeItem
).
What about filtering/pagination for collections? Intentionally omitted for simplicity—GET /
returns the whole array.
CORS? Configure it on your framework as usual.
MIT
No vulnerabilities found.