mpb-localkit
v1.4.18
Published
Type-driven, offline-first SDK for TypeScript developers
Maintainers
Readme
MPB LocalKit
Type-driven, offline-first SDK for TypeScript developers. Define your data schema once — get local storage, sync, auth, and error tracking out of the box.
Quick Start
npm install mpb-localkitimport { createApp, collection, z } from 'mpb-localkit'
const app = createApp({
collections: {
brews: collection(z.object({
brewDate: z.date(),
style: z.string(),
og: z.number(),
fg: z.number().optional(),
notes: z.string().optional(),
})),
},
})
// Fully typed CRUD — works offline immediately
const brew = await app.brews.create({ brewDate: new Date(), style: 'IPA', og: 1.065 })
const allBrews = await app.brews.findMany()
await app.brews.update(brew._id, { fg: 1.012 })
await app.brews.delete(brew._id)API Reference
createApp(config)
Creates a typed app instance with CRUD methods for each collection.
const app = createApp({
collections: { ... }, // Required: collection definitions
sync: { // Optional: sync configuration
endpoint: 'https://...',
interval: 30000,
enabled: true,
},
errorTracking: { // Optional: error tracking config
enabled: true,
snapshot: true,
maxLocalErrors: 100,
},
})collection(schema)
Wraps a Zod schema into a typed collection descriptor. TypeScript infers the document type automatically.
const todos = collection(z.object({
text: z.string(),
done: z.boolean().default(false),
}))
// todos._inferredType is { text: string; done: boolean }z
Re-exported from Zod. Use it to define your schemas.
Collection CRUD
Every collection in app.collections gets these methods:
// Create — returns the document with metadata fields
const doc = await app.todos.create({ text: 'Buy hops', done: false })
// doc._id, doc._collection, doc._updatedAt, doc._deleted are added automatically
// Read
const all = await app.todos.findMany()
const one = await app.todos.findOne(doc._id)
// Update — merges partial data
await app.todos.update(doc._id, { done: true })
// Delete — soft delete (sets _deleted: true for sync propagation)
await app.todos.delete(doc._id)Auth
// Sign up
await app.auth.signUp({ email: '[email protected]', password: 'secret' })
// Sign in
await app.auth.signIn({ email: '[email protected]', password: 'secret' })
// Sign out
await app.auth.signOut()
// Current user
const user = app.auth.currentUser() // { id, email } | nullPasswords are hashed client-side (PBKDF2) before transmission. Sessions are JWTs cached locally for offline access.
Sync
Sync happens automatically when configured with an endpoint. LocalKit uses a Last-Write-Wins (LWW) protocol — the document with the highest _updatedAt timestamp wins conflicts.
// Manual sync
await app.sync()Sync triggers automatically on:
- Tab focus (returning to the app)
- Network reconnect (coming back online)
- Periodic interval (configurable, default 30s)
All reads and writes hit local storage immediately — sync never blocks the UI.
React Hooks
import { useCollection, useAuth, useSync } from 'mpb-localkit/react'
import { app } from './schema'
function BrewList() {
const { data: brews, isLoading } = useCollection(app.brews)
const { user, signIn, signOut } = useAuth(app)
const { status, lastSyncAt } = useSync(app)
if (isLoading) return <div>Loading...</div>
return (
<ul>
{brews.map(brew => <li key={brew._id}>{brew.style}</li>)}
</ul>
)
}CLI
# Start local dev (SDK works without a Worker)
npx mpb-localkit dev
# Generate Cloudflare Worker from your schema
npx mpb-localkit build --name my-app
# Build + deploy to Cloudflare
npx mpb-localkit deployThe generated Worker handles auth, sync, and error storage via Cloudflare R2 + KV.
Architecture
Your App (React/Vue/Svelte)
↓
Framework Bindings
(useCollection, useAuth)
↓
Core SDK
┌──────────────────────────────┐
│ Schema │ Sync │ Auth │
│ Engine │ Engine │ Module │
├──────────────────────────────┤
│ Local Storage │
│ (IndexedDB, via idb) │
└──────────────────────────────┘
↓ (when online)
Cloudflare Worker (generated)
┌──────────────────────────────┐
│ Auth │ Sync │ Errors │
│ Routes │ Routes │ Routes │
├──────────────────────────────┤
│ Cloudflare R2 + KV │
└──────────────────────────────┘All writes hit IndexedDB first — your app works fully offline. The sync engine pushes/pulls changes to the Cloudflare Worker when connectivity is available.
Document Shape
Every document has these metadata fields added automatically:
{
_id: string // UUIDv7 — sortable by creation time
_collection: string // Collection name
_updatedAt: number // Unix ms — the sync cursor
_deleted: boolean // Soft delete for sync propagation
// ...your fields
}Create a New App
The fastest way to start is with the scaffolder:
npm create mpb-localkit@latest
# or
pnpm create mpb-localkit@latestIt prompts for project name, framework, auth method, sync transport, and deploy target — then scaffolds a ready-to-run project and installs dependencies.
? Project name: my-app
? Framework: React
? Auth method: Email + Password
? Sync transport: Auto (WebSocket with HTTP fallback)
? Deploy target: Cloudflare Workers
Done! Your project is ready.
cd my-app
pnpm run devTransport Configuration
LocalKit supports three sync transports, configurable at createApp time:
HTTP Transport (default)
Polling-based sync. Works everywhere, no persistent connection required.
const app = createApp({
collections: { ... },
sync: {
transport: 'http',
endpoint: 'https://your-worker.workers.dev',
interval: 30000,
},
})WebSocket Transport
Persistent connection for real-time sync. The server pushes changes as they happen.
const app = createApp({
collections: { ... },
sync: {
transport: 'websocket',
endpoint: 'wss://your-worker.workers.dev',
},
})Auto Transport (recommended)
Starts a WebSocket connection and falls back to HTTP polling if WebSocket is unavailable. Best of both worlds.
const app = createApp({
collections: { ... },
sync: {
transport: 'auto',
endpoint: 'https://your-worker.workers.dev',
},
})With auto, LocalKit upgrades to WebSocket when the server supports it and degrades gracefully on restricted networks.
WebSocket Sync
When using transport: 'websocket' or transport: 'auto', LocalKit maintains a persistent WebSocket connection to your sync Worker. The Worker sends change events as they occur — no polling delay.
import { createApp, collection, z } from 'mpb-localkit'
const app = createApp({
collections: {
messages: collection(z.object({
text: z.string(),
author: z.string(),
})),
},
sync: {
transport: 'auto',
endpoint: 'https://chat.your-subdomain.workers.dev',
},
})
// Writes go to IndexedDB immediately (no await on network)
await app.messages.create({ text: 'Hello!', author: 'alice' })
// The Worker broadcasts the change to all connected clients via WebSocket
// Recipients see the new message arrive without pollingThe WebSocket connection is managed automatically:
- Reconnects with exponential backoff on disconnect
- Replays missed changes on reconnect using the LWW cursor
- Falls back to HTTP polling when WebSocket is unavailable (Auto mode)
Better Auth Integration
LocalKit integrates with Better Auth for full-featured server-side authentication.
Setup
import { createApp, collection, z } from 'mpb-localkit'
const app = createApp({
collections: {
notes: collection(z.object({ text: z.string() })),
},
auth: {
type: 'better-auth',
baseURL: 'https://auth.your-domain.com',
},
sync: {
transport: 'auto',
endpoint: 'https://sync.your-domain.com',
},
})Session handling
Better Auth sessions are cached locally so the app stays functional offline. The session is re-validated against the server on reconnect.
// Sign in — stores session locally for offline access
await app.auth.signIn({ email: '[email protected]', password: 'secret' })
// Current user is available immediately (from local cache)
const user = app.auth.currentUser() // { id, email, name } | null
// Sync automatically includes the session token in requests
await app.sync()
// Sign out clears the local session
await app.auth.signOut()React hooks with Better Auth
import { useAuth, useCollection } from 'mpb-localkit/react'
import { app } from './schema'
function App() {
const { user, signIn, signOut, isLoading } = useAuth(app)
const { data: notes } = useCollection(app.notes)
if (isLoading) return <div>Loading...</div>
if (!user) return <SignInForm onSignIn={signIn} />
return (
<div>
<p>Signed in as {user.email}</p>
<ul>{notes.map(n => <li key={n._id}>{n.text}</li>)}</ul>
<button onClick={signOut}>Sign out</button>
</div>
)
}License
MIT
