Goal: make the one SQLite file safe to run in production — correct under concurrency, and backed up off the box. You'll have run:
rask new … --snapshots(continuous backup came with--data)
Everything so far — products, orders, jobs, mail, cache, outbox — lives in a single app.db. That's the
One Person Framework bet: SQLite is your production database. Two things make that safe: the right
connection pragmas, and backups that don't live on the same machine.
SQLite's defaults are tuned for a single embedded process — no WAL, no busy_timeout, foreign keys off —
so concurrent web requests hit database is locked. Rask.SQLite applies a tuned pragma set (WAL, a
busy-timeout, foreign_keys=ON, and more) to every connection.
You already have this. rask new --data writes UseRaskSqlite rather than UseSqlite:
builder.Services.AddDbContextFactory<AppDbContext>((sp, o) => o
.UseRaskSqlite(builder.Configuration.GetConnectionString("App") ?? "Data Source=app.db")
.AddInterceptors(sp.GetServices<ISaveChangesInterceptor>()));It is a drop-in for UseSqlite that also installs the pragma interceptor — one word, and every background
processor (jobs, mail, outbox) and every page shares a connection that won't spuriously fail under load.
Retrofitting an existing app is the same one-word change plus
dotnet add package Rask.SQLite.EntityFrameworkCore.
See production SQLite for the full pragma table, the load-test numbers, and the non-blocking write-retry story.
--snapshots wires scheduled point-in-time backups:
builder.Services.AddRaskSqliteSnapshots(o =>
{
o.DatabasePath = builder.Configuration["Sqlite:Path"] ?? "app.db";
o.DestinationDirectory = builder.Configuration["Sqlite:SnapshotDirectory"] ?? "snapshots";
o.Interval = TimeSpan.FromHours(6);
o.Retain = 7;
});These go through SQLite's Online Backup API, not a file copy. That distinction is the whole value: with
WAL on, cp app.db backup.db can capture a torn database, because the committed data you want is split
between the file and the -wal. The backup API reads through a connection and gets a consistent image of a
live database. No external binary, no credentials.
Snapshots on the same disk protect you from a bad migration, not from losing the disk. That's what
continuous backup is for, and --data already wired it in Chapter 1 — Rask.SQLite.Litestream runs
Litestream as a managed background service that streams every change off the
box to object storage (S3, GCS, Azure Blob, or a file target):
var replicaUrl = builder.Configuration["Litestream:ReplicaUrl"];
if (!string.IsNullOrWhiteSpace(replicaUrl))
{
builder.Services.AddRaskSqliteLitestream(o =>
{
o.DatabasePath = builder.Configuration["Sqlite:Path"] ?? "app.db";
o.ReplicaUrl = replicaUrl;
});
}
var app = builder.Build();
if (!string.IsNullOrWhiteSpace(replicaUrl))
{
// Restore BEFORE anything opens the database — a no-op when app.db is already there.
await app.Services.RestoreSqliteFromLitestreamAsync();
}Two details the scaffold gets right and are easy to get wrong by hand:
- The restore runs first, before the schema is created or any pillar's processor starts. Restore is skipped once the file exists, so putting it later means a fresh machine quietly starts with an empty database instead of your data.
- Both halves are gated on the same config. Litestream stays off until you set a replica URL, so
dotnet runworks on a laptop with nolitestreambinary and no cloud credentials. (The restore call throws when Litestream was never registered — useful for a real wiring mistake, fatal for a fresh scaffold, hence the guard.) The csproj also setsRaskLitestreamDownload=false: the binary belongs in the Docker image, which--dockercopies it into, rather than being fetched during everyone's build.
Set the replica when you deploy:
Litestream__ReplicaUrl=s3://my-bucket/shopNow the box is disposable: if it dies, a fresh box restores app.db from the replica on startup and
keeps going. That's what makes "one server" safe rather than scary — durability doesn't depend on that one
machine.
- A WAL file (
app.db-wal) appears next toapp.db, and under concurrent writes you no longer seedatabase is locked. - A
.dbfile lands in the snapshot directory on the configured interval. - With Litestream configured against a real bucket, deleting
app.dband restarting restores it from the replica (watch the startup log). - See it running:
samples/Rask.Example.Shop's/opspage readsjournal_modeandforeign_keysback from the live connection and counts the snapshots on disk.
Learn more: production SQLite · Rask.Data