EF Core on real SQLite,
inside the browser tab.
The same entities, the same DbContext and the same migrations you run on the server -
executing against SQLite in a Web Worker, over OPFS, IndexedDB, Cache Storage or memory. This site
is a Blazor WebAssembly app: once it has loaded, every query on it runs in your tab.
dotnet add package Bit.Bsqlite
_context = await contexts.CreateDbContextAsync(); // Re-runs whenever Products is written - by this tab, by another // tab, or by raw SQL that never touched EF. Nothing polls it. _live = _context.Products.AsNoTracking() .OrderBy(p => p.Sku).Take(4) .AsLiveQuery(); _live.Changed += (_, rows) => { _rows = rows; _ = InvokeAsync(StateHasChanged); }; _rows = await _live.RefreshAsync();
Opening this tab's database…
UseSqlite's SQL generation, unchangedCapabilities
A browser database that behaves like the one on your server
Not a re-implementation of SQL over IndexedDB. The engine is wa-sqlite - SQLite compiled to WebAssembly - and the provider is the stock EF Core SQLite provider with two services swapped, so nothing blocks the UI thread.
One model, both sides
UseSqlite on the server, UseBitBsqlite in the tab. Same entities, same
configuration classes, same migration set - migrations scaffolded against stock SQLite apply here
unchanged.
Live queries
AsLiveQuery() re-runs a query when a table it reads is written - by this tab, by
another tab, or by raw SQL. It learns what changed from SQLite's own update hook, after the
transaction commits.
Storage is a contract
OPFS, IndexedDB, Cache Storage and in-memory are interchangeable providers behind one interface. Selection follows the order you configure, but existing data always outranks preference.
Storage backends →Real SQLite semantics
Foreign keys, unique indexes and transactions are the engine's, not the UI's. A constraint
failure arrives as the DbUpdateException you would get on a server, with SQLite's
own message and result code.
Move the file, keep the data
Migrating between backends exports the image, imports it on the target, checks the SQLite header,
flips the binding, then deletes the source. Export a real .db, or seed a tab from one.
Honest limits
Synchronous APIs, WAL, ATTACH and page-size changes fail up front with a typed
exception that names the alternative, rather than half-working on a backend that cannot honour them.
In practice
See it in code
Register a preference order and a database
Order is preference, not a requirement: the first available backend wins, and the choice is remembered per database. Each backend's JavaScript is loaded only once it is selected.
Getting started →builder.Services.AddBitBsqliteServices(sqlite => sqlite .AddStorageProvider<BsqliteOpfsStorageProvider>() .AddStorageProvider<BsqliteIndexedDbStorageProvider>() .UseStorage(s => s.Prefer("opfs").Fallback("indexeddb")) .AddDatabase("app.db")); builder.Services.AddBitBsqliteDbContext<AppDbContext>();
Query it with the EF Core you know
The context factory opens the database and runs the migrations once per session. From there it
is LINQ, Include and SaveChangesAsync - the async API throughout.
await using var db = await Contexts.CreateDbContextAsync(); var drafts = await db.Orders .Include(o => o.Customer) .Include(o => o.Lines).ThenInclude(l => l.Product) .Where(o => o.Status == OrderStatus.Draft) .ToListAsync();
Treat the database as a file
Every backend can list, export, import and delete its databases outside any connection - which is how migration between backends, a downloadable backup and a pre-built seed database all work.
Database files →// The backend this database landed on: "opfs", "indexeddb", ... var provider = Database.Resolution!.Provider; // The database as a file: a plain SQLite image any desktop tool opens. byte[] image = await provider.Admin.ExportAsync("app.db");
Storage
Four backends, one interface
Each in a namespace of its own, each registered with AddStorageProvider<T>(). Move this
tab's database between them with the bar at the top of any demo page.
| Backend | Namespace | Persists | Concurrent readers | Use it when |
|---|---|---|---|---|
| opfs | Storage.Opfs | yes | yes | The default. Real files, the synchronous engine build, no COOP/COEP headers needed. |
| indexeddb | Storage.IndexedDb | yes | no | Widest reach. Survives browsers without OPFS; readers serialise. |
| cache-storage | Storage.CacheStorage | yes | no | The migration path off Bit.Besql. Persistence cost scales with database size. |
| in-memory | Storage.InMemory | no | yes | Trying the engine, or tests. Gone when the tab closes - and only with an explicit opt-in. |
Comparison
Why not IndexedDB directly?
IndexedDB is a key-value store with indexes. Bsqlite puts a relational engine - and EF Core - on top of the browser's storage, IndexedDB included, so the browser half of an app is written the way its server half is.
| Concern | IndexedDB by hand | Bsqlite |
|---|---|---|
| Queries | Cursors over object stores and their indexes | SQL, through EF Core's LINQ or plain ADO.NET |
| Schema changes | Hand-written onupgradeneeded version steps | The same EF Core migrations the server runs |
| Relations | Your code keeps them consistent | Foreign keys and unique indexes, enforced by SQLite |
| Reacting to writes | Events you raise yourself | AsLiveQuery(), re-run on commit, across tabs |
| Where the data lives | IndexedDB | OPFS, IndexedDB, Cache Storage or memory, chosen and remembered per database |
| Reaching it from .NET | JavaScript interop, call by call | One binary buffer per batch, transferred rather than copied |
Where to start
Choose your path
New to Bsqlite?
Install, register a preference order, open a context - EF Core in the tab in five minutes.
Getting started →Want to see it live?
Every page runs real EF Core calls against this tab's database, with the snippet behind each one.
Open the demos →Moving off Bit.Besql?
The cache-storage backend imports the database Bit.Besql left behind, losslessly, the first time it opens.
Building with an AI agent?
This site is an MCP server: point your agent at /mcp and it reads the shipped library instead of guessing.
Questions
Frequently asked
Is this a SQL engine written in JavaScript?
No. It is SQLite 3.53 compiled to WebAssembly (wa-sqlite 1.1.2), running in a dedicated worker, and the SQL comes from the stock EF Core SQLite provider. SQL batches and result sets cross the worker boundary as one binary buffer each, with 64-bit integers intact.
Which hosting models does it run on?
Blazor WebAssembly, and a Blazor Web App on Interactive WebAssembly - or Interactive Auto once the page runs on WebAssembly - on .NET 8, 9 and 10. Prerendered pages render on the server and open their database once interactive. Interactive Server is not supported: the engine runs in the user's tab.
Can I call SaveChanges() or ToList()?
No - use the async API. The engine runs in a worker the UI thread cannot block on, so synchronous
members throw BsqliteSynchronousApiNotSupportedException, naming the async overload to
call instead.
Do I need COOP/COEP headers or a special host page?
No. Nothing goes into the host page either: the engine, the worker and the VFSes are static web assets
under _content/Bit.Bsqlite/, imported on demand, and a session downloads only the engine
build and the VFS of the backend it selected.
What happens on a browser without OPFS?
Selection falls back in the order you configured. It never substitutes a backend silently: existing data
outranks preference, a non-persistent fallback needs AllowNonPersistentFallback(), and a
failed open names every candidate and why it was rejected.
Can I test it without a browser?
Yes. Bit.Bsqlite.Testing ships an in-process transport for browser-free unit tests, plus
the MSTest conformance kit every storage provider has to pass.
Does the browser database sync with the server?
No. This release shares a model and a migration set between the two sides, not data - the demo's server
holds its own copy of the catalog, seeded through the same AppDbContext.
Open a database in your own tab
One package, one registration - and EF Core runs where your users are.
dotnet add package Bit.Bsqlite