LacertaDB: Why We Built a MongoDB-Like Database That Runs Entirely in Your Browser DEMO
How we created a full-featured document database with encryption, indexing, and aggregation pipelines — all running client-side with zero backend.
The Problem Nobody Was Solving
Picture this: You're building a Web3 wallet, an offline-first PWA, or a privacy-focused note-taking app. You need to store sensitive data locally. Your options?
| Solution | Problem |
|---|---|
localStorage |
5MB limit, no queries, plaintext only |
| Raw IndexedDB | Verbose API, no encryption, manual indexing |
| SQLite WASM | 2MB+ bundle, SQL syntax, no native encryption |
| PouchDB | Sync-focused, no built-in encryption |
| Dexie.js | Great queries, but no encryption or OPFS |
We needed something that:
- ✅ Stores encrypted documents with zero backend
- ✅ Supports MongoDB-style queries
- ✅ Handles binary files (images, PDFs)
- ✅ Has proper indexes for performance
- ✅ Works entirely offline
- ✅ Fits in a reasonable bundle size
Nothing existed. So we built LacertaDB.
What Makes LacertaDB Different
1. 🔐 Military-Grade Encryption, Zero Configuration
// That's it. Your entire database is now encrypted.
const db = await lacerta.getSecureDatabase('vault', '123456');
// Every document is automatically encrypted with AES-GCM-256
await db.createCollection('secrets').add({
apiKey: 'sk-live-xxx',
privateNotes: 'This is encrypted at rest'
});
Under the hood:
- AES-GCM-256 encryption (same as banks use)
- PBKDF2 key derivation with 100,000 iterations
- HMAC integrity verification
- Constant-time comparisons (no timing attacks)
Most browser databases bolt encryption on as an afterthought. In LacertaDB, it's a first-class citizen — and it actually works when you change your PIN:
// This re-encrypts EVERY document in the database
await db.changePin('oldPin', 'newStrongerPin!');
We've seen "encrypted" databases that lose all your data when you change the password. We fixed that.
2. 🔍 Real MongoDB-Style Queries
Not "inspired by MongoDB." Actually compatible:
// Find active premium users over 18, sorted by signup date
const users = await collection.query({
$and: [
{ status: 'active' },
{ plan: { $in: ['pro', 'enterprise'] } },
{ age: { $gte: 18 } },
{ email: { $regex: '@company\\.com$' } }
]
}, {
sort: { createdAt: -1 },
limit: 20,
projection: { name: 1, email: 1, plan: 1 }
});
20+ operators supported:
| Category | Operators |
|---|---|
| Comparison |
$eq, $ne, $gt, $gte, $lt, $lte, $in, $nin
|
| Logical |
$and, $or, $not, $nor
|
| Element |
$exists, $type
|
| Array |
$all, $elemMatch, $size
|
| String |
$regex, $text
|
Plus a full aggregation pipeline:
// Sales report grouped by category
const report = await orders.aggregate([
{ $match: { status: 'completed', date: { $gte: startOfMonth } } },
{ $group: {
_id: '$category',
revenue: { $sum: '$amount' },
avgOrder: { $avg: '$amount' },
count: { $count: 1 }
}},
{ $sort: { revenue: -1 } },
{ $limit: 10 }
]);
This runs entirely in the browser. No server. No network request.
3. ⚡ Four Index Types for Real Performance
Queries on 10,000 documents without indexes? Slow.
With indexes? Instant.
// B-Tree for range queries and sorting
await collection.createIndex('createdAt', { type: 'btree' });
// Hash for lightning-fast exact matches
await collection.createIndex('email', { type: 'hash', unique: true });
// Full-text search
await collection.createIndex('content', { type: 'text' });
// Geospatial for location queries
await collection.createIndex('location', { type: 'geo' });
The geo index supports real geospatial queries:
// Find coffee shops within 2km
const nearby = await places.query({
location: {
$near: {
coordinates: { lat: 47.3769, lng: 8.5417 },
maxDistance: 2 // kilometers
}
},
category: 'coffee'
});
4. 📁 Native Binary File Storage with OPFS
Most browser databases choke on files. LacertaDB uses the Origin Private File System (OPFS) — a modern API that gives you a real filesystem:
// Store a document with file attachments
const docId = await collection.add(
{
title: 'Project Proposal',
author: 'Alice'
},
{
attachments: [
proposalPDF, // File object
coverImage, // Blob
spreadsheet // Another File
]
}
);
// Retrieve with attachments
const doc = await collection.get(docId, { includeAttachments: true });
// doc._attachments = [{ name: 'proposal.pdf', data: Uint8Array, ... }]
Files are stored separately from document data, so your IndexedDB doesn't bloat. And yes, they're encrypted too.
5. 🏗️ Fractal Architecture
LacertaDB follows a "fractal" design where each layer encapsulates complexity:
LacertaDB
└── Database (encryption, settings, migrations)
└── Collection (CRUD, queries, indexes, events)
└── Document (serialization, compression, packing)
This means you can use just what you need:
// Full database with collections
const db = await lacerta.getDatabase('app');
const users = await db.createCollection('users');
// Or just fast key-value storage
const prefs = db.quickStore;
prefs.add('theme', { mode: 'dark', accent: '#00ff88' });
Real-World Use Case: Web3 Wallet
Here's how we use LacertaDB for secure key storage in a crypto wallet:
import { LacertaDB } from '@pixagram/lacertadb';
class WalletManager {
async initialize(userPin) {
this.lacerta = new LacertaDB();
this.db = await this.lacerta.getSecureDatabase('wallet', userPin);
this.wallets = await this.db.createCollection('wallets');
// Index for fast lookups
await this.wallets.createIndex('address', { unique: true });
}
async createWallet(name) {
const { privateKey, address } = generateKeyPair();
// Store encrypted private key in secure vault
await this.db.storePrivateKey(`${name}-pk`, privateKey);
// Store public metadata
await this.wallets.add({
name,
address,
createdAt: Date.now(),
balance: '0'
}, { id: name, permanent: true }); // permanent = protected from cleanup
return address;
}
async signTransaction(walletName, tx) {
// Retrieve decrypted private key
const privateKey = await this.db.getPrivateKey(`${walletName}-pk`);
return signWithKey(tx, privateKey);
}
async exportBackup(backupPassword) {
// Export everything, encrypted with a separate password
return await this.lacerta.createBackup(backupPassword);
}
}
The private keys are:
- Encrypted with AES-GCM-256
- Key derived from user PIN via PBKDF2
- Never exposed in plaintext
- Separate from document storage
This is production code we ship.
Performance That Scales
We benchmarked LacertaDB against common operations:
| Operation | 1,000 docs | 10,000 docs | 100,000 docs |
|---|---|---|---|
| Insert (batch) | 45ms | 380ms | 3.2s |
| Query (indexed) | 2ms | 3ms | 8ms |
| Query (full scan) | 12ms | 95ms | 890ms |
| Aggregation | 8ms | 65ms | 580ms |
With proper indexes, queries stay fast even at scale. The built-in performance monitor helps you optimize:
lacerta.performanceMonitor.startMonitoring();
// ... do operations ...
const stats = lacerta.performanceMonitor.getStats();
console.log(stats);
// {
// opsPerSec: 245,
// avgLatency: '4.12',
// cacheHitRate: '89.3',
// memoryUsageMB: '12.45'
// }
const tips = lacerta.performanceMonitor.getOptimizationTips();
// ['Consider adding an index on frequently queried fields']
The Caching Layer Nobody Talks About
Every query doesn't need to hit IndexedDB. LacertaDB includes three caching strategies:
// LRU (Least Recently Used) - default
collection.configureCacheStrategy({
type: 'lru',
maxSize: 200,
enabled: true
});
// LFU (Least Frequently Used) - for hot data
collection.configureCacheStrategy({
type: 'lfu',
maxSize: 100
});
// TTL (Time To Live) - for expiring data
collection.configureCacheStrategy({
type: 'ttl',
ttl: 60000 // 1 minute
});
Cache hit rates of 80%+ are common, meaning 4 out of 5 queries return instantly from memory.
Schema Migrations Done Right
Your data schema will evolve. LacertaDB has you covered:
const migration = new MigrationManager(db);
migration.addMigration({
version: '2.0.0',
name: 'Add user roles',
up: async (doc) => ({
...doc,
role: doc.isAdmin ? 'admin' : 'user',
permissions: doc.isAdmin ? ['read', 'write', 'delete'] : ['read']
}),
down: async (doc) => {
const { role, permissions, ...rest } = doc;
return { ...rest, isAdmin: role === 'admin' };
}
});
// Migrate forward
await migration.runMigrations('2.0.0');
// Oops, rollback
await migration.rollback('1.0.0');
Migrations run per-document across all collections, with full rollback support.
Event-Driven When You Need It
Hook into document lifecycle events:
collection.on('afterAdd', async (doc) => {
await syncToCloud(doc);
analytics.track('document_created', { id: doc._id });
});
collection.on('beforeDelete', async (docId) => {
const doc = await collection.get(docId);
await createBackup(doc);
});
// Available events:
// beforeAdd, afterAdd
// beforeUpdate, afterUpdate
// beforeDelete, afterDelete
// beforeGet, afterGet
Why "Lacerta"?
Lacerta is the Latin word for lizard — specifically the genus that includes the common wall lizard. Why a lizard?
🦎 Adaptable: Lives anywhere, eats anything
🦎 Fast: Moves in bursts of incredible speed
🦎 Resilient: Survives harsh conditions
🦎 Efficient: Cold-blooded = minimal energy waste
That's what we wanted for browser storage: adaptable to any use case, fast when it matters, resilient to failure, efficient with resources.
Plus, we're based in Zug, Switzerland 🇨🇭 — lizards love sunny Alpine rocks.
Getting Started
npm install @pixagram/lacertadb
import { LacertaDB } from '@pixagram/lacertadb';
const lacerta = new LacertaDB();
// Unencrypted database
const db = await lacerta.getDatabase('myapp');
// Or encrypted
const secureDb = await lacerta.getSecureDatabase('vault', 'myPin');
// Create collection and go
const todos = await db.createCollection('todos');
await todos.add({ text: 'Try LacertaDB', done: false });
const pending = await todos.query({ done: false });
console.log(pending);
What's Next
We're actively developing:
- 🔄 Sync adapters for optional cloud backup
- 📊 Query planner for automatic index selection
- 🧪 Schema validation with JSON Schema
- 🔌 Plugin system for custom index types
Try It Today
- 📦 npm:
@pixagram/lacertadb - 📚 Docs: github.com/pixagram/lacertadb
- 🐛 Issues: We actually respond!
If you're building anything that needs local-first, encrypted, queryable storage in the browser — give LacertaDB a try. We think you'll love it.
Built with 🦎 by Pixagram in Zug, Switzerland
What browser storage challenges have you faced? Drop a comment below! 👇
Top comments (1)
This looks nice