Deposit Usage Guide
Complete guide to installing and using Deposit in your projects.
💡 API Reference
This guide covers API usage and basic patterns. For complete application examples, see Examples.
Table of Contents
Installation
pnpm add @vielzeug/depositnpm install @vielzeug/deposityarn add @vielzeug/depositImport
import { Deposit, LocalStorageAdapter, IndexedDBAdapter } from '@vielzeug/deposit';
// Optional: Import types
import type { DepositDataSchema, DepositMigrationFn } from '@vielzeug/deposit';Basic Usage
Define a Schema
The schema defines your tables, their primary keys, indexes, and record types:
import { defineSchema } from '@vielzeug/deposit';
// Define your record types
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;
createdAt: number;
}
// Define the schema with type-safe helper
const schema = defineSchema<{ users: User; posts: Post }>()({
users: {
key: 'id', // Primary key field
indexes: ['email', 'role'], // Indexed fields for fast lookups
},
posts: {
key: 'id',
indexes: ['userId', 'createdAt'],
},
});Initialize Deposit
Using LocalStorage
LocalStorage is simpler but has a smaller storage limit (~5-10MB):
const adapter = new LocalStorageAdapter('my-app-db', 1, schema);
const db = new Deposit(adapter);
// Or use the shorthand config
const db = new Deposit({
type: 'localStorage',
dbName: 'my-app-db',
version: 1,
schema,
});Using IndexedDB
IndexedDB is more powerful with larger storage and index support:
const adapter = new IndexedDBAdapter('my-app-db', 1, schema);
const db = new Deposit(adapter);
// With migration function
const adapter = new IndexedDBAdapter('my-app-db', 1, schema, (db, oldVersion, newVersion, tx, schema) => {
// Migration logic here
});
// Or use the shorthand config
const db = new Deposit({
type: 'indexedDB',
dbName: 'my-app-db',
version: 1,
schema,
migrationFn: (db, oldVersion, newVersion, tx, schema) => {
// Migration logic
},
});Basic Operations
Insert/Update Records
// Insert a single user
await db.put('users', {
id: 'u1',
name: 'Alice',
email: 'alice@example.com',
age: 30,
role: 'admin',
createdAt: Date.now(),
});
// Update (just put with same id)
await db.put('users', {
id: 'u1',
name: 'Alice Smith', // Updated name
email: 'alice@example.com',
age: 31, // Updated age
role: 'admin',
createdAt: Date.now(),
});Retrieve Records
// Get a single user
const user = await db.get('users', 'u1');
if (user) {
console.log(user.name);
}
// Get with default value
const user = await db.get('users', 'u1', {
id: 'u1',
name: 'Guest',
email: '',
age: 0,
role: 'user',
createdAt: Date.now(),
});
// Get all users
const allUsers = await db.getAll('users');
console.log(`Found ${allUsers.length} users`);Delete Records
// Delete a single user
await db.delete('users', 'u1');
// Clear all users
await db.clear('users');Count Records
const userCount = await db.count('users');
console.log(`Total users: ${userCount}`);Bulk Operations
Bulk Insert/Update
const newUsers = [
{ id: 'u2', name: 'Bob', email: 'bob@example.com', age: 25, role: 'user', createdAt: Date.now() },
{ id: 'u3', name: 'Carol', email: 'carol@example.com', age: 28, role: 'user', createdAt: Date.now() },
{ id: 'u4', name: 'Dave', email: 'dave@example.com', age: 35, role: 'admin', createdAt: Date.now() },
];
await db.bulkPut('users', newUsers);Bulk Delete
await db.bulkDelete('users', ['u2', 'u3', 'u4']);Advanced Features
TTL (Time-To-Live)
Records can automatically expire after a specified time:
// Session expires in 1 hour (3600000 ms)
await db.put(
'sessions',
{
id: 's1',
userId: 'u1',
token: 'abc123',
createdAt: Date.now(),
},
3600000,
);
// 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);Query Builder
Build complex queries with a fluent API:
// Find all admin users sorted by name
const admins = await db.query('users').equals('role', 'admin').orderBy('name', 'asc').toArray();
// Find users between ages 20-30
const youngUsers = await db.query('users').between('age', 20, 30).toArray();
// Complex filtering
const special = await db
.query('users')
.filter((user) => user.age > 18 && user.email.includes('example.com'))
.orderBy('createdAt', 'desc')
.limit(10)
.toArray();
// Pagination
const page2 = await db
.query('users')
.orderBy('name', 'asc')
.page(2, 10) // Page 2, 10 items per page
.toArray();
// Aggregations
const avgAge = await db.query('users').average('age');
const oldest = await db.query('users').max('age');
const youngest = await db.query('users').min('age');
const totalUsers = await db.query('users').count();
// Grouping (type-unsafe – returns object)
const byRole = await db.query('users').groupBy('role').toArray();
// Result: { admin: User[], user: User[] }
// Type-safe grouping (recommended)
const byRoleTyped = await db.query('users').toGrouped('role');
// Result: Array<{ key: 'admin' | 'user', values: User[] }>
for (const group of byRoleTyped) {
console.log(`${group.key}: ${group.values.length} users`);
}💡 Type-Safe Grouping
Use toGrouped() instead of groupBy().toArray() for better type safety. The toGrouped() method returns Array<{ key: T[K], values: T[] }> with correct typing, while groupBy() returns an object that requires manual type casting.
Transactions
Perform operations across multiple tables with automatic atomicity for IndexedDB:
await db.transaction(['users', 'posts'], async (stores) => {
// Add a user
stores.users.push({
id: 'u5',
name: 'Eve',
email: 'eve@example.com',
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!',
createdAt: Date.now(),
});
// For IndexedDB: All changes committed atomically in a single transaction
// For LocalStorage: Changes committed optimistically (non-atomic)
// If any error occurs, all changes are rolled back
});⚡ Atomicity Guarantees
- IndexedDB: Transactions are fully atomic using a single
IDBTransaction– all changes succeed together or all fail together (ACID properties) - LocalStorage: Transactions are optimistic and NOT atomic – tables are updated sequentially. For critical data integrity, use IndexedDB
Patch Operations
Apply multiple operations atomically:
await db.patch('users', [
{
type: 'put',
value: { id: 'u6', name: 'Frank', email: 'f@example.com', age: 40, role: 'user', createdAt: Date.now() },
},
{
type: 'put',
value: { id: 'u7', name: 'Grace', email: 'g@example.com', age: 33, role: 'admin', createdAt: Date.now() },
ttl: 3600000,
},
{ type: 'delete', key: 'u2' },
{ type: 'clear' }, // Clears all, then applies puts
]);Schema Validation
Deposit automatically validates your schema on initialization to catch configuration errors early:
// ✅ Valid schema
const validSchema = {
users: {
key: 'id', // Required: primary key field
indexes: ['email'], // Optional: indexed fields
record: {} as User, // Required: type definition
},
};
// ❌ Invalid schema – missing key field
const invalidSchema = {
users: {
record: {} as User, // Missing 'key' field
},
};
// This will throw immediately with a clear error message:
// "Invalid schema: table "users" missing required "key" field.
// Schema entries must have shape: { key: K, record: T, indexes?: K[] }"
const db = new Deposit({
type: 'localStorage',
dbName: 'my-app',
version: 1,
schema: invalidSchema, // ❌ Throws error
});💡 Early Error Detection
Schema validation happens in the constructor, so you'll catch configuration errors immediately rather than at runtime when accessing data. This makes debugging much easier.
Safe Storage Keys
Deposit uses encodeURIComponent for storage keys, safely handling special characters including colons in database and table names:
// These all work correctly, even with special characters
const db1 = new Deposit({
type: 'localStorage',
dbName: 'my:app:db', // ✅ Colons are safely encoded
version: 1,
schema,
});
const schema2 = {
'user:data': {
// ✅ Colons in table names work too
key: 'id',
record: {} as User,
},
};Corrupted Entry Handling
Deposit gracefully handles corrupted localStorage entries without breaking batch operations:
// If a single entry is corrupted in localStorage
// getAll() will:
// 1. Skip the corrupted entry
// 2. Delete it automatically
// 3. Log a warning
// 4. Continue processing other entries
// 5. Return all valid entries
const users = await db.getAll('users');
// ✅ Returns all valid users, skips corrupted onesSchema Migrations
When using IndexedDB, you can migrate data when the schema changes:
const migrationFn: DepositMigrationFn<typeof schema> = (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);
}
}
};
}
if (oldVersion < 3) {
// Version 2 -> 3: Add createdAt to posts
const store = tx.objectStore('posts');
const request = store.getAll();
request.onsuccess = () => {
for (const post of request.result) {
if (!post.createdAt) {
post.createdAt = Date.now();
store.put(post);
}
}
};
}
};
const adapter = new IndexedDBAdapter('my-app-db', 3, schema, migrationFn);
const db = new Deposit(adapter);Environment-Specific Configuration
Development
const db = new Deposit({
type: 'localStorage', // Faster for development
dbName: 'my-app-dev',
version: 1,
schema,
});Production
const db = new Deposit({
type: 'indexedDB', // More robust for production
dbName: 'my-app-prod',
version: 1,
schema,
migrationFn,
});Testing
beforeEach(async () => {
const db = new Deposit({
type: 'localStorage',
dbName: `test-${Date.now()}`, // Unique per test
version: 1,
schema,
});
await db.clear('users');
await db.clear('posts');
});Best Practices
- Define schemas with TypeScript: Use
{} as YourTypefor full type safety - Use indexes wisely: Only index fields you'll query frequently
- Batch operations: Use
bulkPut/bulkDeleteinstead of loops - Handle errors: Wrap operations in try-catch for error handling
- Clean up expired data: Regularly query and delete old records
- Use TTL for temporary data: Sessions, caches, temporary files
- Version your schemas: Increment version when structure changes
- Test migrations: Always test migration logic thoroughly
- Monitor storage quota: Check available space before large operations
- Clear on logout: Remove sensitive data when user logs out
Common Patterns
Caching API Responses
// Cache with 5-minute TTL
async function fetchWithCache(url: string) {
const cached = await db.get('api-cache', url);
if (cached) return cached.data;
const response = await fetch(url);
const data = await response.json();
await db.put(
'api-cache',
{
id: url,
data,
fetchedAt: Date.now(),
},
300000,
); // 5 minutes
return data;
}Offline Queue
// Queue offline actions
async function saveForLater(action: any) {
await db.put('offline-queue', {
id: crypto.randomUUID(),
action,
createdAt: Date.now(),
});
}
// Process queue when online
async function processQueue() {
const queue = await db.getAll('offline-queue');
for (const item of queue) {
try {
await processAction(item.action);
await db.delete('offline-queue', item.id);
} catch (err) {
console.error('Failed to process', item.id, err);
}
}
}Search with Autocomplete
async function searchUsers(query: string) {
return await db
.query('users')
.search(query) // Fuzzy search
.limit(10)
.toArray();
}Next Steps
💡 Continue Learning
- API Reference – Complete API documentation
- Examples – Practical code examples
- Interactive REPL – Try it in your browser