@vielzeug/deposit
v1.1.6
Published
**Deposit** is a type-safe browser storage utility that provides a unified API for IndexedDB and LocalStorage. Build robust offline-first applications with powerful querying, transactions, and schema migrations—all with minimal code and maximum flexibilit
Readme
@vielzeug/deposit
What is Deposit?
Deposit is a type-safe browser storage utility that provides a unified API for IndexedDB and LocalStorage. Build robust offline-first applications with powerful querying, transactions, and schema migrations—all with minimal code and maximum flexibility.
The Problem
Working with browser storage APIs is challenging:
- IndexedDB is powerful but has a complex, callback-based API
- LocalStorage is simple but limited to string key-value pairs
- No built-in TypeScript support or type safety
- No query capabilities beyond basic get/set
- Manual JSON serialization and error handling
- Schema migrations are manual and error-prone
The Solution
Deposit provides a clean, type-safe abstraction over both storage APIs:
import { Deposit, defineSchema } from '@vielzeug/deposit';
// Define your schema
const schema = defineSchema<{ users: User; posts: Post }>()({
users: { key: 'id', indexes: ['email', 'role'] },
posts: { key: 'id', indexes: ['userId', 'published'] },
});
// Create instance (works with both IndexedDB and LocalStorage!)
const db = new Deposit({
type: 'indexedDB', // or 'localStorage'
dbName: 'my-app',
version: 1,
schema,
});
// Type-safe operations with powerful querying
const admins = await db.query('users')
.where('role', '=', 'admin')
.orderBy('createdAt', 'desc')
.limit(10)
.toArray();✨ Features
- ✅ Type-Safe – Full TypeScript support with schema-based type inference
- ✅ Unified API – Switch between IndexedDB and LocalStorage without changing code
- ✅ Advanced Querying – Rich QueryBuilder with filters, sorting, grouping, and pagination
- ✅ Schema Validation – Early validation with clear error messages
- ✅ TTL Support – Native time-to-live for automatic record expiration
- ✅ Transactions – Atomic operations across multiple tables
- ✅ Migrations – Built-in schema versioning for IndexedDB
- ✅ Resilient – Graceful handling of corrupted entries
- ✅ Lightweight – 4.4 KB gzipped
- ✅ Zero Runtime Dependencies – Only development dependencies for utilities
🆚 Comparison with Alternatives
| Feature | Deposit | Dexie.js | LocalForage | Native IndexedDB | | -------------------- | -------------- | ----------- | ----------- | ---------------- | | TypeScript Support | ✅ First-class | ✅ Good | ⚠️ Limited | ❌ | | Query Builder | ✅ Advanced | ✅ Good | ❌ | ❌ | | Migrations | ✅ Built-in | ✅ Advanced | ❌ | ⚠️ Manual | | LocalStorage Support | ✅ Unified API | ❌ | ✅ | ❌ | | Bundle Size (gzip) | ~4.5 KB | ~20KB | ~8KB | 0KB | | TTL Support | ✅ Native | ❌ | ❌ | ❌ | | Transactions | ✅ Atomic* | ✅ Yes | ❌ | ✅ Complex | | Schema Validation | ✅ Built-in | ⚠️ Runtime | ❌ | ❌ | | Dependencies | 1 | 0 | 0 | N/A |
* Transactions are fully atomic for IndexedDB, optimistic for LocalStorage
📦 Installation
# pnpm
pnpm add @vielzeug/deposit
# npm
npm install @vielzeug/deposit
# yarn
yarn add @vielzeug/deposit🚀 Quick Start
Define Your Schema
import { Deposit, defineSchema } from '@vielzeug/deposit';
interface User {
id: string;
name: string;
email: string;
age: number;
role: 'admin' | 'user';
createdAt: number;
}
interface Post {
id: string;
userId: string;
title: string;
content: string;
published: boolean;
createdAt: number;
}
// Clean, type-safe schema definition
const schema = defineSchema<{ users: User; posts: Post }>()({
users: {
key: 'id', // Primary key field
indexes: ['email', 'role'], // Fields to index for fast lookups
},
posts: {
key: 'id',
indexes: ['userId', 'published', 'createdAt'],
},
});Create a Depot Instance
// Option 1: IndexedDB (recommended for production)
const db = new Deposit({
type: 'indexedDB',
dbName: 'my-app-db',
version: 1,
schema,
});
// Option 2: LocalStorage (simpler, smaller storage)
const db = new Deposit({
type: 'localStorage',
dbName: 'my-app-db',
version: 1,
schema,
});
// Option 3: Custom adapter
import { IndexedDBAdapter } from '@vielzeug/deposit';
const adapter = new IndexedDBAdapter('my-app-db', 1, schema);
const db = new Deposit(adapter);Basic CRUD Operations
// Create/Update
await db.put('users', {
id: 'u1',
name: 'Alice',
email: '[email protected]',
age: 30,
role: 'admin',
createdAt: Date.now(),
});
// Read
const user = await db.get('users', 'u1');
console.log(user?.name); // 'Alice'
// Read all
const allUsers = await db.getAll('users');
// Delete
await db.delete('users', 'u1');
// Bulk operations
await db.bulkPut('users', [user1, user2, user3]);
await db.bulkDelete('users', ['u1', 'u2', 'u3']);
// Clear table
await db.clear('users');
// Count
const count = await db.count('users');📚 Core Concepts
Schema Definition
Deposit uses a type-safe schema definition to validate your data structure:
const schema = defineSchema<{ users: User; posts: Post }>()({
users: {
key: 'id', // Primary key field
indexes: ['email'], // Optional indexed fields for fast lookups
},
posts: {
key: 'id',
indexes: ['userId', 'createdAt'],
},
});Adapters
Deposit supports two storage adapters:
- IndexedDBAdapter: Full-featured with transactions, migrations, and large storage capacity
- LocalStorageAdapter: Simple key-value storage with 5-10MB limit
Switch between them without changing your code!
Type Safety
All operations are fully type-safe based on your schema:
// ✅ TypeScript knows the shape of user
const user = await db.get('users', 'u1');
user?.name; // string
user?.age; // number
// ❌ TypeScript error – 'posts' table doesn't have 'email' field
const post = await db.get('posts', 'p1');
post?.email; // Error!🎯 API Reference
See the full API documentation for complete details.
Core Methods
get(table, key, defaultValue?)– Get a single recordgetAll(table)– Get all records from a tableput(table, value, ttl?)– Create or update a recorddelete(table, key)– Delete a recordclear(table)– Clear all records from a tablecount(table)– Count records in a tablebulkPut(table, values, ttl?)– Bulk insert/updatebulkDelete(table, keys)– Bulk deletequery(table)– Create a query buildertransaction(tables, fn, ttl?)– Atomic transactionpatch(table, operations)– Batch operations
🔥 Advanced Features
Query Builder
Build complex queries with a fluent, type-safe API:
// Simple filtering
const admins = await db.query('users').equals('role', 'admin').orderBy('name', 'asc').toArray();
// Complex filtering
const results = await db
.query('users')
.filter((user) => user.age > 18 && user.email.includes('example.com'))
.orderBy('createdAt', 'desc')
.limit(10)
.toArray();
// Range queries
const youngAdults = await db.query('users').between('age', 18, 30).toArray();
// Pagination
const page2 = await db
.query('users')
.orderBy('name', 'asc')
.page(2, 20) // Page 2, 20 items per page
.toArray();
// Aggregations
const avgAge = await db.query('users').average('age');
const oldestUser = await db.query('users').max('age');
const totalUsers = await db.query('users').count();
// Type-safe grouping (recommended)
const byRole = await db.query('users').toGrouped('role');
// Result: Array<{ key: 'admin' | 'user', values: User[] }>
for (const group of byRole) {
console.log(`${group.key}: ${group.values.length} users`);
}
// Search
const results = await db.query('users').search('alice').toArray();TTL (Time-To-Live)
Records automatically expire and are cleaned up:
// Session expires in 1 hour
await db.put(
'sessions',
{
id: 's1',
userId: 'u1',
token: 'abc123',
createdAt: Date.now(),
},
3600000, // TTL in milliseconds
);
// After 1 hour, this returns undefined
const session = await db.get('sessions', 's1'); // undefined
// TTL with bulk operations
await db.bulkPut('temp-data', records, 3600000);Transactions
Perform operations across multiple tables. Transactions are atomic for IndexedDB (all succeed or all fail) and optimistic for LocalStorage:
await db.transaction(['users', 'posts'], async (stores) => {
// Add a user
stores.users.push({
id: 'u5',
name: 'Eve',
email: '[email protected]',
age: 22,
role: 'user',
createdAt: Date.now(),
});
// Add their first post
stores.posts.push({
id: 'p1',
userId: 'u5',
title: 'Hello World',
content: 'My first post!',
published: true,
createdAt: Date.now(),
});
// For IndexedDB: Changes are committed atomically in a single transaction
// For LocalStorage: Changes are committed optimistically (non-atomic)
// If any error occurs, all changes are rolled back
});Schema Migrations (IndexedDB)
Handle schema changes gracefully:
const migrationFn = (db, oldVersion, newVersion, tx, schema) => {
if (oldVersion < 2) {
// Version 1 -> 2: Add default role to existing users
const store = tx.objectStore('users');
const request = store.getAll();
request.onsuccess = () => {
for (const user of request.result) {
if (!user.role) {
user.role = 'user';
store.put(user);
}
}
};
}
};
const db = new Deposit({
type: 'indexedDB',
dbName: 'my-app-db',
version: 2, // Increment version to trigger migration
schema,
migrationFn,
});Patch Operations
Apply multiple operations atomically:
await db.patch('users', [
{
type: 'put',
value: { id: 'u6', name: 'Frank', email: '[email protected]', age: 40, role: 'user', createdAt: Date.now() },
},
{
type: 'put',
value: { id: 'u7', name: 'Grace', email: '[email protected]', age: 33, role: 'admin', createdAt: Date.now() },
ttl: 3600000,
},
{ type: 'delete', key: 'u2' },
]);API Reference
Deposit Class
Methods
put(table, value, ttl?)– Insert or update a recordget(table, key, defaultValue?)– Retrieve a record by keygetAll(table)– Retrieve all records from a tabledelete(table, key)– Delete a recordclear(table)– Remove all records from a tablecount(table)– Count records in a tablebulkPut(table, values, ttl?)– Insert/update multiple recordsbulkDelete(table, keys)– Delete multiple recordsquery(table)– Create a QueryBuilder for advanced queriestransaction(tables, fn, ttl?)– Execute atomic operationspatch(table, operations)– Apply multiple operations atomically
QueryBuilder Methods
Filtering
equals(field, value)– Filter by exact matchbetween(field, lower, upper)– Filter by rangestartsWith(field, prefix, ignoreCase?)– Filter by string prefixwhere(field, predicate)– Filter with custom predicatefilter(fn)– Filter with predicate on entire recordnot(fn)– Negate a predicateand(...fns)– Combine predicates with ANDor(...fns)– Combine predicates with OR
Ordering & Pagination
orderBy(field, direction)– Sort resultslimit(n)– Limit number of resultsoffset(n)– Skip first n resultspage(pageNumber, pageSize)– Paginate resultsreverse()– Reverse order
Aggregations
count()– Count matching recordsfirst()– Get first recordlast()– Get last recordmin(field)– Find minimum valuemax(field)– Find maximum valuesum(field)– Sum numeric fieldaverage(field)– Calculate average
Transformations
modify(callback)– Transform recordsgroupBy(field)– Group by field (returns object)toGrouped(field)– Type-safe grouping (returns array) – Recommendedsearch(query, tone?)– Fuzzy search
Execution
toArray()– Execute query and return resultsreset()– Clear all operationsbuild(conditions)– Build query from condition objects
Schema Validation
Deposit validates your schema on initialization to catch errors early:
// ✅ Valid schema
const validSchema = {
users: {
key: 'id',
record: {} as User,
},
};
// ❌ Invalid schema – will throw immediately
const invalidSchema = {
users: {
record: {} as User, // Missing 'key' field
},
};
// Error: "Invalid schema: table "users" missing required "key" field.
// Schema entries must have shape: { key: K, record: T, indexes?: K[] }"Error Handling
Deposit gracefully handles errors and corrupted data:
// Corrupted localStorage entries are:
// 1. Skipped automatically
// 2. Deleted from storage
// 3. Logged as warnings
// 4. Don't break batch operations
const users = await db.getAll('users');
// ✅ Returns all valid users, skips corrupted onesTypeScript Support
Full type inference from your schema:
const schema = {
users: {
key: 'id',
record: {} as User,
},
} satisfies DepositDataSchema;
const db = new Deposit({ type: 'localStorage', dbName: 'app', version: 1, schema });
// ✅ Type-safe: knows user is User | undefined
const user = await db.get('users', 'u1');
// ✅ Type-safe: knows this returns User[]
const users = await db.query('users').toArray();
// ✅ Type-safe: knows result is Array<{ key: string, values: User[] }>
const grouped = await db.query('users').toGrouped('role');
// ❌ Type error: 'invalid' is not a valid table
await db.get('invalid', 'key');Best Practices
- Use IndexedDB for production – Better performance and larger storage
- Define schemas with TypeScript – Use
{} as YourTypefor full type safety - Index wisely – Only index fields you'll query frequently
- Batch operations – Use
bulkPut/bulkDeleteinstead of loops - Use
toGrouped()– Prefer it overgroupBy()for type safety - Handle errors – Wrap operations in try-catch for error handling
- Increment versions – For schema changes in IndexedDB
Examples
Todo App
interface Todo {
id: string;
text: string;
completed: boolean;
createdAt: number;
}
const schema = {
todos: {
key: 'id',
indexes: ['completed', 'createdAt'],
record: {} as Todo,
},
};
const db = new Deposit({
type: 'indexedDB',
dbName: 'todos-db',
version: 1,
schema,
});
// Add todo
await db.put('todos', {
id: crypto.randomUUID(),
text: 'Learn Deposit',
completed: false,
createdAt: Date.now(),
});
// Get active todos
const activeTodos = await db.query('todos').equals('completed', false).orderBy('createdAt', 'desc').toArray();
// Mark as completed
const todo = await db.get('todos', 'todo-id');
if (todo) {
await db.put('todos', { ...todo, completed: true });
}
// Delete completed
const completed = await db.query('todos').equals('completed', true).toArray();
await db.bulkDelete(
'todos',
completed.map((t) => t.id),
);Session Management with TTL
interface Session {
id: string;
userId: string;
token: string;
expiresAt: number;
}
const schema = {
sessions: {
key: 'id',
indexes: ['userId'],
record: {} as Session,
},
};
const db = new Deposit({
type: 'indexedDB',
dbName: 'auth-db',
version: 1,
schema,
});
// Create session with 1-hour TTL
await db.put(
'sessions',
{
id: crypto.randomUUID(),
userId: 'u1',
token: 'secure-token',
expiresAt: Date.now() + 3600000,
},
3600000, // Auto-delete after 1 hour
);
// Get current session
const session = await db.get('sessions', 'session-id');
if (!session) {
// Session expired or doesn't exist
console.log('Please log in');
}Browser Support
- Chrome/Edge 24+
- Firefox 29+
- Safari 10+
- All modern browsers with IndexedDB and LocalStorage support
📖 Documentation
📄 License
MIT © Helmuth Saatkamp
🤝 Contributing
Contributions are welcome! Check our GitHub repository.
🔗 Links
Part of the Vielzeug ecosystem – A collection of type-safe utilities for modern web development.
