Offline Storage in .NET MAUI Blazor Hybrid Apps with SQLite

Set up offline-first SQLite storage in a .NET MAUI Blazor Hybrid app — EF Core in the MAUI host, safe file paths, and repositories shared with your web app.

Register SQLite in the MAUI host via EF Core, store the database file in FileSystem.AppDataDirectory, and hide it behind the same repository interfaces your Blazor components already use. That’s the whole pattern — the rest of this guide is the setup, the platform pitfalls, and where the seams go so your web app can share the UI without sharing the storage.

Why offline-first matters in a Hybrid app

A .NET MAUI Blazor Hybrid app renders Blazor components inside a native shell. Users expect it to behave like a native app — which means working in airplane mode, on flaky mobile networks, and immediately on launch. A local SQLite database gives you that: reads and writes hit the device first, and the network becomes a sync concern instead of an availability concern.

Registering EF Core with SQLite in the MAUI host

Add the provider to the MAUI project (not the shared UI project):

<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="9.*" />

Then register the DbContext in MauiProgram.cs, pointing the connection at the app-data directory:

var dbPath = Path.Combine(FileSystem.AppDataDirectory, "app.db");

builder.Services.AddDbContextFactory<AppDbContext>(options =>
    options.UseSqlite($"Data Source={dbPath}"));

Two details matter here:

  • FileSystem.AppDataDirectory is the only safe location. Hard-coded paths that work on Android will fail on iOS or Windows. This MAUI API resolves to the right per-platform, per-app writable folder everywhere.
  • Use a DbContextFactory, not a scoped DbContext. Blazor component lifecycles don’t match request scopes; a factory gives each operation a short-lived context and avoids threading surprises.

Keeping the UI shared: repositories as the seam

The shared Razor Class Library — the components your web and mobile heads both render — should never reference EF Core directly. Define the interface in shared code:

public interface IAppointmentRepository
{
    Task<IReadOnlyList<Appointment>> GetUpcomingAsync();
    Task AddAsync(Appointment appointment);
}

The MAUI host implements it over SQLite; the web app implements the same interface over its own API or server-side data access. Components inject IAppointmentRepository and genuinely don’t know which platform they’re on — that’s what makes one UI run everywhere.

Migrations on the device

Applying EF Core migrations at app startup works, but do it explicitly and early:

using var db = await dbFactory.CreateDbContextAsync();
await db.Database.MigrateAsync();

Run it once during startup before the first component renders. Ship a new migration with every schema change — users upgrade the app with their data already on the device, and MigrateAsync walks them forward safely.

The pitfalls that cost real time

  • Forgetting batteries_included initialization is not needed with Microsoft.EntityFrameworkCore.Sqlite — but it is with sqlite-net-pcl. Mixing guidance from the two libraries is the most common broken-setup cause.
  • iOS file paths are case-sensitive. App.db and app.db are different files on device and the same file on your Windows dev machine.
  • Don’t share one DbContext across async component events. Two awaited handlers touching the same context throws InvalidOperationException — the factory pattern above prevents it.

Where this pattern comes from

This is the storage architecture behind the .NET MAUI Hybrid apps I’ve shipped for freelance clients — mobile apps with shared UI components and offline-friendly SQLite storage. The same shared-UI approach powers my Pet Vet Health Care System, where one set of Blazor components runs on both MAUI and WebAssembly. If you’re planning an offline-capable app like this, that’s exactly what my .NET services cover.