SQLite in the browser for Blazor, with EF Core on top. The same entities and the same migrations run on the client as on the server, and the database lives in the user's browser tab - in OPFS, IndexedDB, Cache Storage or memory.
// Program.cs
builder.Services.AddBitBsqliteServices(sqlite => sqlite
.AddStorageProvider<BsqliteOpfsStorageProvider>()
.AddStorageProvider<BsqliteIndexedDbStorageProvider>()
.UseStorage(s => s.Prefer("opfs").Fallback("indexeddb"))
.AddDatabase("app.db"));
builder.Services.AddBitBsqliteDbContext<AppDbContext>();
// A component
@inject IBsqliteDbContextFactory<AppDbContext> Contexts
await using var db = await Contexts.CreateDbContextAsync();
var open = await db.Orders.Where(o => o.Status == OrderStatus.Open).ToListAsync();That query runs in a dedicated worker in the browser: SQLite 3.53 (via wa-sqlite 1.1.2), the stock EF Core SQLite provider, and a storage backend chosen and remembered per database.
Targets .NET 8, 9, and 10 with the matching EF Core · Runs on Blazor WebAssembly, and in a Blazor Web App on Interactive WebAssembly or Interactive Auto once the page runs on WebAssembly. Prerendered pages render on the server and open their database once interactive.
Bsqlite's documentation website lives in Demo - run the
Demo/Server project, or visit bsqlite.bitplatform.dev.
- Why Bsqlite
- Packages
- Installation
- Composition without dependency injection
- Live queries
- Where it runs
- What this release does not claim
- Moving off Bit.Besql
- MCP server (for AI agents)
- Tests
- Documentation
- License
- EF Core, unmodified.
UseBitBsqliteis the stock SQLite provider over a Bsqlite connection. Migrations scaffolded against the server apply in the browser. Decimal arithmetic,REGEXP, and theEF_DECIMALcollation are provided in the worker so query results match the server's. - Fast interop. SQL batches and result sets cross the worker and Blazor boundaries as one binary buffer each, transferred rather than copied. No JSON, no base64, 64-bit integers intact. Prepared statements are cached in the worker, so EF's repeated statement texts are parsed once.
- Honest storage. Selection never substitutes a backend silently: existing data outranks preference, a non-persistent fallback needs an opt-in, and a failed open names every candidate and why it was rejected. Cross-backend migration verifies the copy before flipping the binding.
- Live queries. A write re-runs any live query that reads the table, including writes from another tab. Change tracking comes from the engine's update hook, so triggers and cascades count too.
- Complete ADO.NET. Commands, batches (
DbBatch), transactions with savepoints and rollback-on-dispose, multi-result readers, typed exceptions by SQLite result code, and in-place export of an open database.
| Package | Role |
|---|---|
Bit.Bsqlite |
AddBitBsqliteServices, AddBitBsqliteDbContext, UseBitBsqlite(connection), ADO.NET and live queries, the worker transport, storage selection and migration, the storage provider contract, the storage backends, and the browser assets (engine, worker, host, VFSes) under _content/Bit.Bsqlite |
Bit.Bsqlite.Testing |
An in-process transport for browser-free unit tests, plus the MSTest conformance kit every storage provider must pass |
The storage backends, each in its own namespace and each registered with AddStorageProvider<T>():
| Backend | Provider (namespace) | Role |
|---|---|---|
opfs |
BsqliteOpfsStorageProvider (Bit.Bsqlite.Storage.Opfs) |
Persistent, synchronous engine build (the smallest download), several connections |
indexeddb |
BsqliteIndexedDbStorageProvider (Bit.Bsqlite.Storage.IndexedDb) |
Persistent, JSPI or Asyncify engine build, widest reach |
cache-storage |
BsqliteCacheStorageProvider (Bit.Bsqlite.Storage.CacheStorage) |
Persistent, and the migration path off Bit.Besql |
in-memory |
BsqliteInMemoryStorageProvider (Bit.Bsqlite.Storage.InMemory) |
Volatile: tests, demos, and a declared last resort |
dotnet add package Bit.BsqliteThat is the whole install - every backend is in it. Browser-free unit tests add Bit.Bsqlite.Testing to the test project.
Register the services in the project that runs on WebAssembly - and, in a Blazor Web App whose pages are prerendered, in the server project too, so the prerender pass can resolve what the pages inject:
using Bit.Bsqlite.Storage.IndexedDb;
using Bit.Bsqlite.Storage.InMemory;
using Bit.Bsqlite.Storage.Opfs;
builder.Services.AddBitBsqliteServices(sqlite => sqlite
.AddStorageProvider<BsqliteOpfsStorageProvider>()
.AddStorageProvider<BsqliteIndexedDbStorageProvider>()
.AddStorageProvider<BsqliteInMemoryStorageProvider>()
.UseStorage(s => s
.Prefer(BsqliteOpfsStorageProvider.ProviderName)
.Fallback(BsqliteIndexedDbStorageProvider.ProviderName)
.Fallback(BsqliteInMemoryStorageProvider.ProviderName)
.AllowNonPersistentFallback())
.AddDatabase("app.db"));
builder.Services.AddBitBsqliteDbContext<AppDbContext>();The engine (.wasm), the worker and the VFSes are static web assets under _content/Bit.Bsqlite/, each imported on
demand: registering a backend costs nothing until it is selected, and a session downloads one engine build and the VFS of
the backend it selected. Nothing needs to be added to the host page.
AddBitBsqliteServices registers everything above. Without dependency injection, the same pieces compose by hand:
var session = await factory.OpenAsync("app.db"); // a BsqliteSessionFactory
options.UseBitBsqlite(session.Connection);Selection is sticky: existing data outranks preference. BsqliteStorageMigrationMode.AutomaticOnOpen copies, checks the
SQLite header, then flips the binding. KeepExisting is the default.
Table-level. A committed write re-runs any live query that reads that table, including across tabs. The worker learns
what changed from SQLite's own update hook - cascades and triggers included - and reports it once the transaction
commits, so a rolled-back write is never shown and a multi-statement SaveChangesAsync re-runs a query once, not per
statement.
await using var live = ctx.Orders.Where(o => o.Open).AsLiveQuery();
live.Changed += (_, rows) => InvokeAsync(StateHasChanged);| Host | Support |
|---|---|
| Blazor WebAssembly | Everything |
| Blazor Web App, Interactive WebAssembly | Everything |
| Blazor Web App, Interactive Auto | Everything once the page runs on WebAssembly |
| Prerendering | Register the services on the server as well; they resolve there, and the database opens once the page runs in the browser (RendererInfo.IsInteractive on Interactive WebAssembly, OperatingSystem.IsBrowser() on Interactive Auto) |
| Interactive Server | Not supported: the engine runs in the user's tab, and EF Core on the server would need a native SQLite of its own |
Every service that reaches the browser, or holds a database session, is registered as scoped: one for the whole application on WebAssembly, and one per circuit or request on a server, so nothing is ever shared between users.
EF Core's own feature set follows the target framework. The SQLite provider translates SUM over a decimal from EF
Core 9 on and ORDER BY a decimal from EF Core 10 on; an application on .NET 8 gets the same NotSupportedException
from those queries in the browser as from the stock provider on its server.
- A synchronous tier (
SaveChanges(),ToList()). Synchronous members throwBsqliteSynchronousApiNotSupportedExceptionnaming the async overload. - WAL journal mode. No web VFS can provide the shared memory it needs.
- Server synchronization. The demo's two sides share a model, not data.
- A synchronous worker bridge. Browsers forbid
Atomics.waiton the main thread, a spin-wait would freeze the UI, and multithreaded WebAssembly is not Blazor's default. in-memoryhas no admin view of live data. It registers no VFS, so a database opened on it lives in the engine's own heap inside the worker;Admindescribes only images passed to it directly. Export, list, and migration away from this backend do not see what a connection wrote. Persistent backends do not have this gap.indexeddbserializes readers.IDBBatchAtomicVFSis registered withWebLocksMixin's defaultexclusivelock policy, so a read holds the same lock a write does.SupportsConcurrentReadsreports that honestly.
Bit.Besql keeps a whole SQLite file in Cache Storage
and syncs it from Blazor's in-memory file system. The cache-storage backend (BsqliteCacheStorageProvider) reads that
file: a database Bit.Besql left in its bit-Besql cache is imported losslessly into Bsqlite's own page layout the first
time it is opened, so an application that switches keeps its users' data.
The demo website hosts a Model Context Protocol server at
https://bsqlite.bitplatform.dev/mcp. Point your coding agent at it to let it search the Bsqlite API, read the guides
and the demo's source, and plan a storage setup. See the MCP server page of the demo website for the full tool list.
The wa-sqlite engine and the upstream VFS files are vendored at restore, verified against the checksums in
Bit.Bsqlite/engine/wa-sqlite.lock.props, and git-ignored, so restore before building.
cd src/Utilities/Bsqlite/Tests/Bit.Bsqlite.Tests && dotnet test # ADO.NET, EF Core and storage selection, every target framework
cd src/Utilities/Bsqlite/Tests/Bit.Bsqlite.Tests.E2E && dotnet test # the worker, the engine and every VFS in a real browser, plus the demo
cd src/Utilities/Bsqlite/Tests/Bit.Bsqlite.Tests.Mcp && dotnet test # the MCP server, hosted in-memory
cd src/Utilities/Bsqlite/Tests/bit-bsqlite-js && npm ci && npm test # the JavaScript that needs no browserSee Tests/Bit.Bsqlite.Tests.E2E/README.md for the browser matrix, the soak suite and the benchmark page.
- Getting started - registration, contexts, raw ADO.NET, the shared database
- Storage - backends, selection rules, migration, admin, writing a provider
- Live queries - EF and ADO.NET live queries, cross-tab notifications
- Architecture and performance - the worker, the binary wire format, the statement cache, change tracking
- Limits - what the browser cannot do and how the library tells you
- Extending - SQL functions from JavaScript, custom transports
MIT, as is the bundled wa-sqlite, whose license ships next to the engine under _content/Bit.Bsqlite/engine. See
LICENSE.