IndexedDB storage provider for WitDatabase - enables Blazor WebAssembly support.
This package allows WitDatabase to run entirely in the browser with data persisted to IndexedDB.
Use 12.8.0 or later. Everything below is written around the asynchronous API, because in a browser there is no other kind - and from 12.0.0 to 12.7.0 that API did not reach storage.
BTreeConcurrentStore, which wraps every B+Tree store, implemented each async method by calling the synchronous one, so a database built on this provider could be created and never written to. Fixed in 12.8.0 with a lock that survives a continuation resuming on another thread.
<PackageReference Include="OutWit.Database.Core.IndexedDb" Version="14.0.1" />Add the JavaScript files to your index.html:
<script src="_content/OutWit.Database.Core.IndexedDb/witdb-indexeddb.js"></script>
<script src="_content/OutWit.Database.Core.IndexedDb/witdb-indexeddb-index.js"></script>@page "/database-demo"
@inject IJSRuntime JSRuntime
@using OutWit.Database.Core.Builder
@using OutWit.Database.Core.IndexedDb
<h3>Database Demo</h3>
@code {
private WitDatabase? _db;
protected override async Task OnInitializedAsync()
{
_db = new WitDatabaseBuilder()
.WithIndexedDbStorage("MyAppDatabase", JSRuntime)
.WithBTree()
.WithTransactions()
.Build();
// Initialize storage (opens IndexedDB)
await (_db.Store as StorageIndexedDb)?.InitializeAsync()!;
}
private async Task SaveData()
{
var key = Encoding.UTF8.GetBytes("user:1");
var value = Encoding.UTF8.GetBytes("{\"name\":\"John\"}");
await _db!.PutAsync(key, value);
}
private async Task LoadData()
{
var key = Encoding.UTF8.GetBytes("user:1");
var value = await _db!.GetAsync(key);
if (value != null)
{
var json = Encoding.UTF8.GetString(value);
Console.WriteLine(json);
}
}
public async ValueTask DisposeAsync()
{
if (_db != null)
{
await _db.DisposeAsync();
}
}
}var db = new WitDatabaseBuilder()
.WithIndexedDbStorage("SecureDatabase", JSRuntime)
.WithBTree()
.WithEncryption("user-password") // AES-GCM encryption
.WithTransactions()
.Build();var db = new WitDatabaseBuilder()
.WithIndexedDbStorage("MvccDatabase", JSRuntime)
.WithBTree()
.WithMvcc()
.WithDefaultIsolationLevel(IsolationLevel.Snapshot)
.Build();| Engine | Compatible | Notes |
|---|---|---|
| B+Tree (WithBTree) | Yes | Recommended, full support |
| LSM-Tree (WithLsmTree) | No | Requires file system |
| In-Memory | Yes | No persistence |
| Feature | Compatible | Notes |
|---|---|---|
| Basic CRUD | Yes | Full support |
| Transactions | Yes | Via TransactionalStore |
| MVCC | Yes | All isolation levels |
| Savepoints | Yes | Full support |
| Encryption | Yes | AES-GCM, BouncyCastle |
| Secondary Indexes | Yes | Full support via IndexedDB |
| File Locking | No | Not applicable |
| WAL/Journal | No | Single-file model |
| Browser | Supported |
|---|---|
| Chrome 80+ | Yes |
| Firefox 75+ | Yes |
| Edge 80+ | Yes |
| Safari 14+ | Yes |
Secondary indexes are automatically configured when using WithIndexedDbStorage(). Each index uses a separate object store within a dedicated IndexedDB database.
// Indexes are automatically configured
var db = new WitDatabaseBuilder()
.WithIndexedDbStorage("MyDatabase", JSRuntime)
.WithBTree()
.Build();
// Index factory is automatically set up for IndexedDB
// Indexes will use "MyDatabase_indexes" databasevar db = new WitDatabaseBuilder()
.WithIndexedDbStorage("MyDatabase", JSRuntime)
.WithIndexedDbIndexes(JSRuntime, "CustomIndexDb") // Override default
.WithBTree()
.Build();// Create index manager
var indexFactory = new SecondaryIndexFactoryIndexedDb(JSRuntime, "MyDatabase_indexes");
var indexManager = new IndexManager(indexFactory);
// Create a secondary index
var emailIndex = indexManager.CreateIndex("idx_email", isUnique: true);
// Add entries
emailIndex.Add(emailBytes, primaryKeyBytes);
// Find by index
var primaryKeys = emailIndex.Find(emailBytes);// Basic IndexedDB storage (auto-configures indexes)
builder.WithIndexedDbStorage(string databaseName, IJSRuntime jsRuntime)
// With custom page size
builder.WithIndexedDbStorage(string databaseName, IJSRuntime jsRuntime, int pageSize)
// Custom index database
builder.WithIndexedDbIndexes(IJSRuntime jsRuntime, string indexDatabaseName)// Create storage directly
var storage = new StorageIndexedDb("DatabaseName", jsRuntime);
// Async initialization (recommended)
await storage.InitializeAsync();
// Check if database exists
bool exists = await storage.DatabaseExistsAsync();
// Properties
storage.DatabaseName // IndexedDB database name
storage.PageSize // Page size in bytes
storage.PageCount // Total number of pages
storage.IsInitialized // Whether storage is initialized// Create index factory
var factory = new SecondaryIndexFactoryIndexedDb(jsRuntime, "DatabaseName");
// Create indexes
var uniqueIndex = factory.CreateIndex("idx_unique", isUnique: true);
var nonUniqueIndex = factory.CreateIndex("idx_category", isUnique: false);
// Factory properties
factory.ProviderKey // "indexeddb"Low-level IndexedDB operations (advanced usage):
var interop = new IndexedDbInterop(jsRuntime, "DatabaseName");
await interop.OpenAsync();
await interop.WritePageAsync(0, pageData);
var data = await interop.ReadPageAsync(0);
await interop.CloseAsync();-
No LSM-Tree Support
- LSM-Tree requires file system operations
- Use B+Tree instead
-
Single-Tab Access
- IndexedDB can be accessed from multiple tabs
- But WitDatabase does not coordinate between tabs
- Use single-tab or implement your own coordination
-
Storage Quota
- Browser limits IndexedDB storage (typically 50-100MB minimum)
- Check available quota with
navigator.storage.estimate()
-
Sync Operations
- Sync methods use async bridge internally
- Prefer async methods for better performance
try
{
var db = new WitDatabaseBuilder()
.WithIndexedDbStorage("MyDatabase", JSRuntime)
.WithLsmTree() // ERROR!
.Build();
}
catch (InvalidOperationException ex)
{
// "IndexedDB storage is not compatible with LSM-Tree engine..."
}try
{
// First open with 4096
var db1 = new WitDatabaseBuilder()
.WithIndexedDbStorage("MyDatabase", JSRuntime, pageSize: 4096)
.Build();
db1.Dispose();
// Try to open with different page size
var db2 = new WitDatabaseBuilder()
.WithIndexedDbStorage("MyDatabase", JSRuntime, pageSize: 8192) // ERROR!
.Build();
}
catch (InvalidOperationException ex)
{
// "Database was created with page size 4096, but 8192 was requested..."
}If you see errors about witDb or witDbIndex being undefined:
- Ensure both script tags are in
index.html - Make sure they are loaded before your Blazor app starts
- Check browser console for script loading errors
// Check if IndexedDB is available
if (window.indexedDB == null)
{
// IndexedDB not supported or blocked
}try
{
await db.PutAsync(key, largeValue);
}
catch (JSException ex) when (ex.Message.Contains("quota"))
{
// Storage quota exceeded
// Consider cleanup or ask user to allow more storage
}-
Use Async Methods
// Preferred await db.PutAsync(key, value); // Avoid when possible (uses sync bridge) db.Put(key, value);
-
Batch Operations
using var tx = db.BeginTransaction(); foreach (var item in items) { tx.Put(item.Key, item.Value); } tx.Commit();
-
Appropriate Page Size
- Default 4096 is good for small records
- Use 8192-16384 for larger records
- OutWit.Database.Core - Core database library
- OutWit.Database.Core.BouncyCastle - ChaCha20 encryption
- MDN: IndexedDB
- Blazor JS Interop
- ROADMAP.md - Planned features
Licensed under the Apache License, Version 2.0. See LICENSE.
If you use OutWit.Database.Core.IndexedDb in a product, a mention is appreciated (but not required), for example: "Powered by WitDatabase https://witdatabase.io/".
"WitDatabase" and the WitDatabase logo are used to identify the official project by Dmitry Ratner.
You may:
- refer to the project name in a factual way (e.g., "built with WitDatabase");
- use the name to indicate compatibility (e.g., "WitDatabase-compatible").
You may not:
- use "WitDatabase" as the name of a fork or a derived product in a way that implies it is the official project;
- use the WitDatabase logo to promote forks or derived products without permission.