Skip to content
Open source · MIT · v325.33.20 · net8.0 / net9.0 / net10.0

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
Home.razor · opening…
_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…

Real SQLite 3.53wa-sqlite in a dedicated worker, off the UI thread
Stock EF Core providerUseSqlite's SQL generation, unchanged
Four storage backendsOPFS, IndexedDB, Cache Storage and in-memory
Live queriesre-run on commit, across tabs too

Capabilities

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.

Catalog →

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.

SQL and live queries →

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.

Customers →

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.

Database files →

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.

Limits →

In practice

See it in code

01

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>();
02

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.

Orders →
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();
03

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.

BackendNamespacePersistsConcurrent readersUse 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.

ConcernIndexedDB by handBsqlite
QueriesCursors over object stores and their indexesSQL, through EF Core's LINQ or plain ADO.NET
Schema changesHand-written onupgradeneeded version stepsThe same EF Core migrations the server runs
RelationsYour code keeps them consistentForeign keys and unique indexes, enforced by SQLite
Reacting to writesEvents you raise yourselfAsLiveQuery(), re-run on commit, across tabs
Where the data livesIndexedDBOPFS, IndexedDB, Cache Storage or memory, chosen and remembered per database
Reaching it from .NETJavaScript interop, call by callOne 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.

Storage backends →

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.

Try the MCP server →

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
An unhandled error has occurred. Reload 🗙