Lightweight, local-first, zero-backend database for single-user web applications with IndexedDB caching and Append-Only Delta Log (WAL) Google Drive cloud synchronization.
Modern web apps and single-user PWAs face a dilemma:
-
Set up backend databases (PostgreSQL, Supabase, Firebase)
$\rightarrow$ server costs, maintenance overhead, and storing user personal data on third-party servers. -
Browser-only storage (LocalStorage, IndexedDB)
$\rightarrow$ fast and private, but changes are trapped on a single device and vulnerable to cache eviction. -
Monolithic cloud file sync (e.g. uploading a single 10MB
database.json)$\rightarrow$ severe network bottlenecks, race conditions, and heavy mobile battery drain.
DriveDB implements a true database Write-Ahead Log (WAL) directly on Google Drive (Bring Your Own Drive / BYOD):
-
Zero Latency (
$0\text{ms}$ ): Reads and queries resolve synchronously from an in-memory cache. -
Durable IndexedDB Persistence: Writes persist asynchronously to local IndexedDB with browser eviction protection (
navigator.storage.persist()). -
Append-Only Cloud Sync: Instead of rewriting a giant file, DriveDB pushes small, immutable delta log batches (
wal/{timestamp}_{clientId}_{batchId}.json) to Google Drive. - Zero Overwrite Conflicts: Because cloud sync appends new immutable files instead of modifying existing files, simultaneous edits on phone and laptop never collide or overwrite each other.
-
Automatic Compaction: Older WAL files are periodically folded into a consolidated
snapshot.json.
flowchart TD
subgraph Client["π₯οΈ Client Browser"]
Action["User Action in UI"] -->|"Instant (0ms)"| MemCache["In-Memory Cache<br/>(Synchronous get / query / list)"]
MemCache -->|"Async Persistence"| IDB[("Materialized IndexedDB Store<br/>(Fast offline view)")]
MemCache -->|"Non-Blocking Mutation"| Outbox[("Local WAL Outbox<br/>(Queued SET & DELETE mutations)")]
Outbox -->|"Debounced Flush (e.g. 1000ms)"| Worker["Sync Worker"]
end
subgraph GDrive["βοΈ Google Drive (Cloud Truth)"]
direction TB
AppFolder["π DriveDB Data/"]
Snapshot["π snapshot.json<br/>(Compacted Base State)"]
WalFolder["π wal/"]
BatchA["π wal_1725350100_devicePhone_b1.json<br/>(Immutable Delta Batch)"]
BatchB["π wal_1725350200_deviceLaptop_b2.json<br/>(Immutable Delta Batch)"]
AppFolder --> Snapshot
AppFolder --> WalFolder
WalFolder --> BatchA
WalFolder --> BatchB
end
Worker -->|"Google Drive v3 REST API<br/>(Immutable Multipart Upload)"| WalFolder
Snapshot -.->|"Periodic Compaction"| WalFolder
- β‘ Zero-Latency (
$0\text{ms}$ ): Reads and queries resolve directly from in-memory state. - π Append-Only Delta Log (WAL): Only sends tiny mutation deltas over the wire instead of re-uploading the entire database.
- π‘οΈ Zero Overwrite Conflicts: Client syncs append immutable log files. HTTP 412/409 conflicts and race conditions are eliminated.
- π Deterministic Replay & LWW: New remote WAL batches replay in timestamp order with Last-Write-Wins (LWW) conflict convergence.
- πͺ¦ Tombstone Deletion: Deleting records registers a
DELETEmutation in the WAL, ensuring deletions propagate cleanly across devices. - ποΈ Automatic Snapshot Compaction: Periodically merges accumulated WAL logs into a single
snapshot.jsonand archives old logs. - π‘ Cross-Tab Synchronization: Native
BroadcastChannelsupport keeps multiple open tabs in sync without redundant network calls. - π¦ 1-Click JSON Backup & Restore: Direct export and import for complete user data sovereignty.
- π Direct Client-to-Google Communication: Zero intermediary servers. 100% of data flows directly between the browser and Google's official API.
npm install @aldian/drivedb
# or
pnpm add @aldian/drivedb
# or
yarn add @aldian/drivedbimport { DriveDB } from "@aldian/drivedb";
interface Note {
title: string;
content: string;
tags: string[];
}
// Create and initialize collection
const db = new DriveDB<Note>({
dbName: "my_notes_app",
tableName: "notes",
syncDebounceMs: 1000,
gdriveFolderName: "My Notes App",
// Optional: Supply Google OAuth Access Token (or token getter function)
accessToken: () => localStorage.getItem("gdrive_token"),
});
await db.init();// Create or Update (0ms in-memory update + async IndexedDB durability + WAL log)
await db.set("note_1", {
title: "Meeting Notes",
content: "Discuss roadmap and launch timeline.",
tags: ["work", "planning"],
});
// Synchronous 0ms retrieval
const note = db.get("note_1");
console.log(note?.data.title); // "Meeting Notes"
// Query with predicate filters
const workNotes = db.query((doc) => doc.data.tags.includes("work"));
// List all active documents
const allNotes = db.list();
// Delete (registers DELETE mutation in WAL for cloud propagation)
await db.delete("note_1");// Subscribe to sync status events
const unsubscribe = db.onSyncChange(({ status, error, mutationsSynced }) => {
console.log(`Sync status: ${status}`); // "pending" | "syncing" | "synced" | "error"
});
// Update Google OAuth token dynamically upon user sign-in
db.setAccessToken(userOAuthAccessToken);
// Trigger manual two-way sync (downloads remote WAL logs & flushes local outbox)
await db.sync();import { useState, useEffect } from "react";
import { DriveDB, Document, SyncStatus } from "@aldian/drivedb";
interface Habit {
name: string;
streak: number;
}
const habitsDb = new DriveDB<Habit>({
dbName: "habits_app",
tableName: "habits",
accessToken: () => localStorage.getItem("google_access_token"),
});
export function useHabits() {
const [habits, setHabits] = useState<Document<Habit>[]>([]);
const [syncStatus, setSyncStatus] = useState<SyncStatus>("synced");
useEffect(() => {
async function setup() {
await habitsDb.init();
setHabits(habitsDb.list());
}
setup();
return habitsDb.onSyncChange((event) => {
setSyncStatus(event.status);
setHabits(habitsDb.list());
});
}, []);
const addHabit = async (id: string, name: string) => {
await habitsDb.set(id, { name, streak: 0 });
setHabits(habitsDb.list());
};
const deleteHabit = async (id: string) => {
await habitsDb.delete(id);
setHabits(habitsDb.list());
};
return { habits, addHabit, deleteHabit, syncStatus };
}| Option | Type | Default | Description |
|---|---|---|---|
dbName |
string |
"drivedb_store" |
IndexedDB database name |
tableName |
string |
"documents" |
IndexedDB object store table name |
syncDebounceMs |
number |
1000 |
Milliseconds to debounce before uploading WAL to Google Drive |
autoSync |
boolean |
true |
Automatically trigger cloud sync on local writes |
gdriveFolderName |
string |
"${dbName}_drivedb_<uuid>" |
Custom target folder name in Google Drive (decided by client) |
gdriveFolderId |
string |
undefined |
Optional explicit Google Drive folder ID (e.g. from Drive Picker) |
appendFolderUuid |
boolean |
false |
Append short UUID suffix to folder name to guarantee uniqueness |
walFolderName |
string |
"wal" |
Subfolder name for immutable WAL batches |
snapshotFileName |
string |
"snapshot.json" |
Consolidated snapshot filename |
maxUncompactedLogs |
number |
50 |
Max WAL batches before auto-compaction |
enableBroadcastChannel |
boolean |
true |
Enable real-time cross-tab updates |
requestPersistence |
boolean |
true |
Request navigator.storage.persist() on initialization |
clientId |
string |
Auto-generated UUID | Unique client/device identifier |
accessToken |
string | (() => string | null) |
undefined |
Google OAuth access token or getter function |
Releases are published directly and securely from your local machine.
Ensure you are logged into your npm account in your local terminal:
npm loginVerify authentication:
npm whoami
# Should output your npm usernameπ‘ For Forks & Custom Builds: If you are forking this repository to customize or publish your own variation, update the
"name"field inpackage.jsonto your own npm scope (e.g."@<your-username>/drivedb").
If publishing for the first time:
npm publish --access publicFor subsequent updates, use semantic versioning:
# 1. Bump the version in package.json and generate a git tag
npm version patch # 0.1.0 -> 0.1.1 (Bug fixes / minor adjustments)
# or: npm version minor # 0.1.0 -> 0.2.0 (New backward-compatible features)
# or: npm version major # 0.1.0 -> 1.0.0 (Breaking API changes)
# 2. Publish to the public npm registry
npm publish --access public
# 3. Push the version bump commit and git tag to GitHub
git push origin main --follow-tagsπ‘οΈ Built-in Safety Check (
prepublishOnly): Thepackage.jsonincludes aprepublishOnlylifecycle hook. Whenever you runnpm publish, npm automatically executes:
tsc --noEmit(TypeScript typecheck)vitest run --coverage(All 31 tests & 92% coverage threshold)tsup build(Bundling clean ESM, CJS, and.d.tsoutputs)If any test or type error occurs, the publish is aborted immediately, preventing broken builds from reaching npm.
MIT Β© Aldian Fazrihady