DEV Community

Cover image for A deep dive into LacertaDB
Matias Affolter
Matias Affolter

Posted on

A deep dive into LacertaDB

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'
});
Enter fullscreen mode Exit fullscreen mode

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!');
Enter fullscreen mode Exit fullscreen mode

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 }
});
Enter fullscreen mode Exit fullscreen mode

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 }
]);
Enter fullscreen mode Exit fullscreen mode

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' });
Enter fullscreen mode Exit fullscreen mode

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'
});
Enter fullscreen mode Exit fullscreen mode

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, ... }]
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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' });
Enter fullscreen mode Exit fullscreen mode

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);
  }
}
Enter fullscreen mode Exit fullscreen mode

The private keys are:

  1. Encrypted with AES-GCM-256
  2. Key derived from user PIN via PBKDF2
  3. Never exposed in plaintext
  4. 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']
Enter fullscreen mode Exit fullscreen mode

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
});
Enter fullscreen mode Exit fullscreen mode

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');
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode
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);
Enter fullscreen mode Exit fullscreen mode

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

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)

Collapse
 
marin_edaj_8ebba7860ed4 profile image
Marián Šedaj

This looks nice