<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>ScrivaDB | Blog</title><description>A lightweight, append-only, file-based document database. Human-readable NDJSON storage, gRPC + REST from one binary, and an embeddable Go engine.</description><link>https://srjn45.github.io/</link><language>en</language><item><title>Embedding ScrivaDB in your Go program</title><link>https://srjn45.github.io/scriva/blog/embedding-scrivadb-in-your-go-program/</link><guid isPermaLink="true">https://srjn45.github.io/scriva/blog/embedding-scrivadb-in-your-go-program/</guid><description>Most people meet ScrivaDB as a server on :5433. But if your program is written in Go and is the only writer, you can skip the server entirely — go get the engine, call Open, and get a real database in-process. No daemon, no network, no gRPC in your binary.

</description><pubDate>Mon, 27 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Most databases make you run a server. You install a daemon, open a port, manage
credentials, pool connections — and then talk to it over a socket, even when
“it” is running on the same laptop as your program.&lt;/p&gt;
&lt;p&gt;If your program is written in Go and is the only thing writing the data, that’s
a lot of ceremony for no benefit. ScrivaDB’s storage engine is a plain Go
library, so you can skip all of it and run the database &lt;strong&gt;in-process&lt;/strong&gt; — no
gRPC, no network, no separate daemon. Same durability, same query model, living
directly inside your binary.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;two-lines-to-a-database&quot;&gt;Two lines to a database&lt;/h2&gt;&lt;/div&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;go&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;get&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;github.com/srjn45/scriva&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div aria-live=&quot;polite&quot;&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;import&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;github.com/srjn45/scriva&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;db&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;_&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;:=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;scriva&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;Open&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;./data&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;)   &lt;/span&gt;&lt;span&gt;// embedded durability defaults (fsync ~1s)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;defer&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;db&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;Close&lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div aria-live=&quot;polite&quot;&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;&lt;code dir=&quot;auto&quot;&gt;Open&lt;/code&gt; points at a directory. That directory &lt;em&gt;is&lt;/em&gt; the database — the NDJSON
segment files, the indexes, the metadata. There’s nothing else to stand up.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;keyed-crud-with-your-own-keys&quot;&gt;Keyed CRUD with your own keys&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;You bring the keys. ScrivaDB stores records under caller-supplied string keys, so
you’re not forced to round-trip a generated id back into your own model:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;sessions&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;:=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;db&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;MustCollection&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;sessions&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;id&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;_&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;_&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;:=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;sessions&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;InsertWithKey&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;sess-1&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;map&lt;/span&gt;&lt;span&gt;[&lt;/span&gt;&lt;span&gt;string&lt;/span&gt;&lt;span&gt;]any{&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;status&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;open&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;})&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;rec&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;_&lt;/span&gt;&lt;span&gt;   &lt;/span&gt;&lt;span&gt;:=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;sessions&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;GetByKey&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;sess-1&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div aria-live=&quot;polite&quot;&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;Collections are created on demand — &lt;code dir=&quot;auto&quot;&gt;MustCollection&lt;/code&gt; hands you one, materializing
it if it’s new. No migration step, no &lt;code dir=&quot;auto&quot;&gt;CREATE TABLE&lt;/code&gt;.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;compare-and-swap-not-locks&quot;&gt;Compare-and-swap, not locks&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Concurrency is handled optimistically. Every record carries a revision, and you
can make an update conditional on the revision you last read — a compare-and-swap.
If someone else wrote in between, your update is rejected and you retry, instead
of two writers silently clobbering each other:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;rec&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;_&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;:=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;sessions&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;GetByKey&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;sess-1&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// only succeeds if sess-1 is still at rec.Rev&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;_&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;err&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;:=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;sessions&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;UpdateIfRev&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;sess-1&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;rec&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;Rev&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;map&lt;/span&gt;&lt;span&gt;[&lt;/span&gt;&lt;span&gt;string&lt;/span&gt;&lt;span&gt;]any{&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;status&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;closed&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;})&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;if&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;err&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;!=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;nil&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;// someone updated it first — re-read and decide&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div aria-live=&quot;polite&quot;&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;This is the right primitive for the embedded case: no lock to hold across a
network, no deadlock to untangle — just “write only if the world hasn’t moved
under me.”&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;the-rest-of-the-surface&quot;&gt;The rest of the surface&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Embedding isn’t a stripped-down mode. You get the real engine:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Secondary indexes&lt;/strong&gt; for fast equality and range queries — the same query
engine the server exposes.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Upsert&lt;/strong&gt;, &lt;strong&gt;count&lt;/strong&gt;, and &lt;strong&gt;exists&lt;/strong&gt; for the common one-liners.&lt;/li&gt;
&lt;li&gt;In-process &lt;strong&gt;&lt;code dir=&quot;auto&quot;&gt;Watch&lt;/code&gt;&lt;/strong&gt; subscriptions, so your program can react to changes
without polling (with a documented overflow contract so a slow consumer can’t
wedge a writer).&lt;/li&gt;
&lt;li&gt;A &lt;strong&gt;&lt;code dir=&quot;auto&quot;&gt;LoadJSONL&lt;/code&gt;&lt;/strong&gt; bulk-import path for seeding.&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;&lt;h2 id=&quot;the-part-that-matters-for-your-binary&quot;&gt;The part that matters for your binary&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Here’s the detail that makes embedding actually pleasant: the &lt;code dir=&quot;auto&quot;&gt;engine&lt;/code&gt; package
pulls in &lt;strong&gt;no gRPC, no protobuf, no Prometheus, no cobra, no OpenTelemetry&lt;/strong&gt;.
A CI gate enforces it on every commit. Embedding ScrivaDB won’t quietly drag a
server framework and a metrics stack into your dependency graph — you get a
storage engine, and that’s it.&lt;/p&gt;
&lt;p&gt;That’s the difference between “a database that also has a library mode” and a
library that happens to be a database. The dependency discipline is a feature,
and it’s tested like one.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;when-embedded-is-the-right-call&quot;&gt;When embedded is the right call&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Reach for the embedded engine when:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Your program is written in Go and is the &lt;strong&gt;sole writer&lt;/strong&gt; of the data.&lt;/li&gt;
&lt;li&gt;You want durability and a real query model without operating a separate
process.&lt;/li&gt;
&lt;li&gt;You’re building a CLI, a desktop app, an edge/IoT daemon, or a service that
wants local state without a database container beside it.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;And when you &lt;em&gt;don’t&lt;/em&gt; fit that shape — multiple processes need to write, or you
want to talk to the data from another language — the very same repo builds a
&lt;a href=&quot;https://srjn45.github.io/scriva/start/install/&quot;&gt;standalone server&lt;/a&gt; with gRPC + REST and
&lt;a href=&quot;https://srjn45.github.io/scriva/guides/clients/&quot;&gt;client SDKs in ten languages&lt;/a&gt;. The storage engine is
identical; only the front door changes.&lt;/p&gt;
&lt;p&gt;warden — a supervisor for fleets of AI coding agents — embeds ScrivaDB exactly
this way: its sessions, pipelines, and agent state live in an in-process store,
with no database to run alongside the daemon. That’s the embedded case in one
sentence.&lt;/p&gt;
&lt;hr&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://srjn45.github.io/scriva/guides/embedding/&quot;&gt;Embedding guide&lt;/a&gt; — the full API, durability modes, and the Watch overflow contract.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://srjn45.github.io/scriva/guides/data-model/&quot;&gt;Data model&lt;/a&gt; — keys, revisions, and CAS in depth.&lt;/li&gt;
&lt;/ul&gt;</content:encoded><category>scrivadb</category><category>go</category><category>embedded</category><category>tutorial</category></item><item><title>Encrypting a database you can cat</title><link>https://srjn45.github.io/scriva/blog/encrypting-a-database-you-can-cat/</link><guid isPermaLink="true">https://srjn45.github.io/scriva/blog/encrypting-a-database-you-can-cat/</guid><description>The selling point is that you can cat your database. Encryption at rest sounds like the opposite of that. Here&apos;s how ScrivaDB reconciles the two — by encrypting values at a single boundary and keeping compaction, indexing, checksums, and backups completely key-oblivious.

</description><pubDate>Mon, 27 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;ScrivaDB’s whole personality is that your data is &lt;strong&gt;just text&lt;/strong&gt;. One JSON object
per line, in a file you can &lt;code dir=&quot;auto&quot;&gt;cat&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;grep&lt;/code&gt;, and &lt;code dir=&quot;auto&quot;&gt;tar&lt;/code&gt;. That transparency is the
feature.&lt;/p&gt;
&lt;p&gt;Now add a requirement: &lt;em&gt;sensitive fields must be unreadable on disk without a
key.&lt;/em&gt; At first glance it reads like a contradiction — encryption is, definitionally,
making bytes unreadable. So the design question isn’t “which cipher?” It’s:
&lt;strong&gt;how do you encrypt the sensitive parts without breaking the transparency,
durability, and query model that make the thing worth using?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;The answer turned out to be less about cryptography and more about &lt;em&gt;where&lt;/em&gt; you
put it.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;the-threat-youre-actually-defending-against&quot;&gt;The threat you’re actually defending against&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Be precise about the threat model, because it decides the whole design.
ScrivaDB is an embedded, local-storage engine — the app calls &lt;code dir=&quot;auto&quot;&gt;Open(...)&lt;/code&gt; and
reads and writes in the same process. There’s no separate untrusted caller to
bypass. So the thing you’re defending is &lt;strong&gt;data at rest&lt;/strong&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;A stolen laptop or disk.&lt;/li&gt;
&lt;li&gt;A leaked backup tarball or filesystem snapshot.&lt;/li&gt;
&lt;li&gt;Another OS user &lt;code dir=&quot;auto&quot;&gt;cat&lt;/code&gt;-ing your &lt;code dir=&quot;auto&quot;&gt;data/&lt;/code&gt; directory.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;What’s explicitly &lt;em&gt;out&lt;/em&gt; of scope: a compromised running process. While the DB
is open, the key and the decrypted plaintext are in memory — at-rest encryption
can’t help you there, and pretending otherwise just produces false confidence.
Naming that boundary out loud is what keeps the design honest.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;one-boundary-and-everything-else-stays-dumb&quot;&gt;One boundary, and everything else stays dumb&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Here’s the core move. Encrypt and decrypt happen at &lt;strong&gt;exactly one place&lt;/strong&gt;: the
collection API boundary — &lt;code dir=&quot;auto&quot;&gt;Insert&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;Update&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;Upsert&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;Get&lt;/code&gt;, and the scan
path. The map of data that gets serialized to a segment file always holds
&lt;strong&gt;ciphertext&lt;/strong&gt; for encrypted fields. The plaintext exists only above that line,
in your process.&lt;/p&gt;
&lt;p&gt;The payoff is everything &lt;em&gt;below&lt;/em&gt; that line never learns a key exists:&lt;/p&gt;





























&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Subsystem&lt;/th&gt;&lt;th&gt;What it sees&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Compaction&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;Copies opaque ciphertext blobs when it rewrites entries. Never needs the key.&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Index rebuild&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;Reads ciphertext from segments — safe, because encrypted fields are never indexed.&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;CRC32C checksum&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;Computed over the stored (ciphertext) form. Still catches bit-rot.&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Backup / snapshot&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;A tar of the segments is &lt;em&gt;already ciphertext&lt;/em&gt; — encrypted for free.&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Replication&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;Ships the stored ciphertext; a follower needs no key to replicate.&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;That table is the whole design. By choosing &lt;em&gt;one&lt;/em&gt; choke point, encryption
becomes a local concern instead of a cross-cutting one — no subsystem-by-subsystem
retrofit, no key threaded through the compactor.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;what-a-value-looks-like-on-disk&quot;&gt;What a value looks like on disk&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Each encrypted value is a self-describing string: a reserved marker, an envelope
version, the id of the key that sealed it, and the payload.&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&amp;#x3C;marker&gt;:v1:&amp;#x3C;key-id&gt;:&amp;#x3C;base64url( nonce || ciphertext+tag )&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div aria-live=&quot;polite&quot;&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;The marker isn’t the human-readable &lt;code dir=&quot;auto&quot;&gt;enc:v1:&lt;/code&gt; you might guess — it’s a
distinctive, rare magic string, and &lt;strong&gt;any user write that begins with it is
rejected&lt;/strong&gt;. That single rule turns “is this value encrypted?” into an infallible
check on read, with no side-table and nothing to drift out of sync.&lt;/p&gt;
&lt;p&gt;The cipher is &lt;strong&gt;XChaCha20-Poly1305&lt;/strong&gt;. Two properties earn its place:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;It’s an &lt;strong&gt;AEAD&lt;/strong&gt; — confidentiality &lt;em&gt;and&lt;/em&gt; tamper-detection in one primitive. A
modified ciphertext is &lt;em&gt;rejected&lt;/em&gt;, never quietly decrypted to garbage. That
composes perfectly with ScrivaDB’s existing “detect corruption on read” stance.&lt;/li&gt;
&lt;li&gt;Its &lt;strong&gt;192-bit random nonce&lt;/strong&gt; needs no counter bookkeeping. Append-only writes
mean every insert or update is a fresh write with a fresh nonce, so reuse
simply can’t happen.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Passphrase-derived keys go through &lt;strong&gt;Argon2id&lt;/strong&gt; (64 MiB / 3 iterations), with a
per-DB random salt stored — safely — in cleartext in &lt;code dir=&quot;auto&quot;&gt;meta.json&lt;/code&gt;. The salt
reveals nothing without the passphrase.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;the-one-real-tradeoff&quot;&gt;The one real tradeoff&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;An encrypted field is opaque, so the engine can’t filter, range, sort, or index
on it — and the design &lt;em&gt;enforces&lt;/em&gt; that rather than letting you discover it as a
silent bug. Try to build an index on an encrypted field and you get a typed
error, not a &lt;code dir=&quot;auto&quot;&gt;sidx_*.json&lt;/code&gt; file quietly leaking the plaintext back onto disk. A
filter that references an encrypted field is rejected at query-planning time, not
left to never-match.&lt;/p&gt;
&lt;p&gt;For the fields you’d actually encrypt — passwords, tokens, SSNs, secrets — this
costs exactly nothing, because you never query &lt;em&gt;by&lt;/em&gt; them. You look them up by
some &lt;em&gt;other&lt;/em&gt; key and read them back. Naming that as the one tradeoff, instead of
hand-waving it, is what makes the feature trustworthy.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;reads-dont-care-about-your-policy&quot;&gt;Reads don’t care about your policy&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;The detail I find most satisfying: &lt;strong&gt;reads are driven by the per-value marker,
not by the current configuration.&lt;/strong&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;A value that begins with the marker is decrypted (the key chosen by the id it
carries). Anything else is passed through as plaintext.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Because every value self-describes, a collection in &lt;em&gt;any&lt;/em&gt; mixed state — legacy
plaintext from before you turned encryption on, new ciphertext, a half-migrated
tail — reads correctly with zero coordination. The policy governs only what new
writes encrypt; it never governs reads. That’s what makes migration a non-event:
flip the policy, new writes conform immediately, and a background re-encrypting
compaction pass backfills the old records on its own schedule. Reads are correct
the entire time.&lt;/p&gt;
&lt;p&gt;Key rotation falls out of the same mechanism for free — every blob names its own
key, so old segments keep decrypting under the old key while new writes use the
new one, and a compaction pass retires the old key when you’re ready.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;the-lesson&quot;&gt;The lesson&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;The interesting part of adding encryption to ScrivaDB wasn’t the cipher — modern
AEAD is close to a solved problem. It was &lt;strong&gt;placing the boundary&lt;/strong&gt; so that a
human-readable, append-only, self-compacting store could gain confidential
fields without any other part of it having to change. Encrypt at one line; keep
everything below it key-oblivious; let each value say what it is. You end up with
a database you can still &lt;code dir=&quot;auto&quot;&gt;cat&lt;/code&gt; — you just can’t read the parts that matter
without the key.&lt;/p&gt;
&lt;hr&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://srjn45.github.io/scriva/start/what-is-scriva/&quot;&gt;What is ScrivaDB?&lt;/a&gt; — the transparency-first design.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://srjn45.github.io/scriva/guides/durability-and-backup/&quot;&gt;Durability &amp;#x26; backup&lt;/a&gt; — why an encrypted tar is a free encrypted backup.&lt;/li&gt;
&lt;/ul&gt;</content:encoded><category>scrivadb</category><category>encryption</category><category>security</category><category>design</category></item><item><title>How append-only survives a crash</title><link>https://srjn45.github.io/scriva/blog/how-append-only-survives-a-crash/</link><guid isPermaLink="true">https://srjn45.github.io/scriva/blog/how-append-only-survives-a-crash/</guid><description>The interesting moment in any storage engine is kill -9 in the middle of a write. ScrivaDB&apos;s crash story is three unglamorous rules — append-only, truncate the torn tail on open, and CRC32C every record — that together mean a crash can cost you the last write but never a record that was already there.

</description><pubDate>Mon, 27 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Every storage engine looks correct until the power goes out mid-write. That’s
the moment that separates “a file I write JSON to” from “a database.” So it’s
worth asking of ScrivaDB directly: &lt;strong&gt;what actually happens if the process is
killed halfway through a write?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;The answer is deliberately unglamorous. There’s no journal replay, no
multi-phase recovery, no fsck that walks the whole store. There are three simple
rules that compose into a strong guarantee.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;rule-1-never-modify-a-byte-in-place&quot;&gt;Rule 1: never modify a byte in place&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;ScrivaDB is append-only. An insert, an update, and a delete are all the &lt;em&gt;same&lt;/em&gt;
physical operation — a new line appended to the end of the current segment file.
An update doesn’t seek back and overwrite the old record; it writes a new one
with a higher revision. A delete writes a tombstone line.&lt;/p&gt;
&lt;p&gt;This is the load-bearing decision for crash safety. If you never overwrite
existing bytes, a crash &lt;strong&gt;cannot corrupt a record that was already on disk&lt;/strong&gt; —
there is no in-place mutation for it to interrupt. The worst a crash can do is
damage the write that was in flight, and that write is always at the &lt;em&gt;tail&lt;/em&gt; of
the file, never in the middle.&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;seg_000001.ndjson&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;{&quot;id&quot;:&quot;01J8...&quot;,&quot;op&quot;:&quot;put&quot;,&quot;rev&quot;:1,&quot;data&quot;:{...},&quot;crc&quot;:&quot;a1b2&quot;}   ← safe, sealed by later writes&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;{&quot;id&quot;:&quot;01J8...&quot;,&quot;op&quot;:&quot;put&quot;,&quot;rev&quot;:2,&quot;data&quot;:{...},&quot;crc&quot;:&quot;c3d4&quot;}   ← safe&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;{&quot;id&quot;:&quot;01J8...&quot;,&quot;op&quot;:&quot;put&quot;,&quot;rev&quot;:3,&quot;data&quot;:{...},&quot;cr             ← crash hit here&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div aria-live=&quot;polite&quot;&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;Everything above that torn last line is untouched and intact. The blast radius
of a crash is exactly one record: the one you hadn’t finished.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;rule-2-on-open-truncate-the-torn-tail&quot;&gt;Rule 2: on open, truncate the torn tail&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;So when ScrivaDB opens a segment, it doesn’t trust that the file ends cleanly.
It seeks backward from the end to find the last complete line — the last one
terminated by a newline — and &lt;strong&gt;truncates anything after it&lt;/strong&gt;. That half-written
final record from the crash simply disappears, and the segment is left at a known-good
boundary, ready to append to again.&lt;/p&gt;
&lt;p&gt;This is why recovery is O(seek), not O(scan): the only place damage can live is
the very end, so that’s the only place recovery has to look. A clean shutdown and
a &lt;code dir=&quot;auto&quot;&gt;kill -9&lt;/code&gt; converge to the same valid state on the next open — the crash just
costs you the one unacknowledged write, which the caller never got a success for
anyway.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;rule-3-checksum-every-record-verify-on-read&quot;&gt;Rule 3: checksum every record, verify on read&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Truncation handles the write that was &lt;em&gt;interrupted&lt;/em&gt;. But there’s a subtler
failure: a record that was written fine and then &lt;strong&gt;rotted&lt;/strong&gt; — a flipped bit from
a bad disk, a dodgy cable, a cosmic ray. The bytes are all there and the line is
newline-terminated, so truncation won’t catch it. Left alone, the engine would
hand you silently-wrong data, which is worse than an error.&lt;/p&gt;
&lt;p&gt;So every entry carries a &lt;strong&gt;CRC32C (Castagnoli)&lt;/strong&gt; checksum, computed over its
identifying fields and its data and stored on the line itself. On read, the
checksum is recomputed and compared. A mismatch is surfaced as a corruption
error — the read &lt;em&gt;fails loudly&lt;/em&gt; instead of returning data that looks valid but
isn’t.&lt;/p&gt;
&lt;p&gt;CRC32C specifically because it’s cheap (hardware-accelerated on modern CPUs, so
it’s nearly free on the read path) and it’s the right tool for the job it’s
doing: catching accidental bit-rot, not defending against a malicious tamperer.
(For that threat you’d reach for &lt;a href=&quot;https://srjn45.github.io/scriva/blog/encrypting-a-database-you-can-cat/&quot;&gt;the AEAD tag that comes with
encryption-at-rest&lt;/a&gt; — a
different primitive for a different problem.)&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;why-the-three-compose&quot;&gt;Why the three compose&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Put together, the guarantee is easy to state:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;A crash costs you at most the last in-flight write&lt;/strong&gt; — and nothing you
received a success for. (Append-only + tail truncation.)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Silent corruption of a record that was already durable is caught, not
returned.&lt;/strong&gt; (Per-record CRC32C.)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;None of these three rules is clever on its own. Truncating a partial line is
obvious. CRC32C is decades old. Append-only is the oldest trick in the log-structured
book. The engineering is in choosing all three &lt;em&gt;together&lt;/em&gt; and letting a
background compactor reclaim the space from superseded records later — so you get
durable, crash-safe writes without a write-ahead log, and a store whose recovery
path is short enough to reason about in a single sitting.&lt;/p&gt;
&lt;p&gt;Boring is the goal. When the power comes back, you want your database to come up
in a known state and get on with it — and to do that by truncating one line, not
by replaying a journal you have to trust.&lt;/p&gt;
&lt;hr&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://srjn45.github.io/scriva/start/what-is-scriva/&quot;&gt;What is ScrivaDB?&lt;/a&gt; — the append-only model end to end.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://srjn45.github.io/scriva/guides/durability-and-backup/&quot;&gt;Durability &amp;#x26; backup&lt;/a&gt; — fsync policies and point-in-time recovery.&lt;/li&gt;
&lt;/ul&gt;</content:encoded><category>scrivadb</category><category>storage</category><category>durability</category><category>internals</category></item><item><title>When a file is your database</title><link>https://srjn45.github.io/scriva/blog/when-a-file-is-your-database/</link><guid isPermaLink="true">https://srjn45.github.io/scriva/blog/when-a-file-is-your-database/</guid><description>Half the time you reach for a database, what you actually needed was a file you could trust. ScrivaDB is the space between json.Marshal to disk and standing up Postgres — durable, queryable, and still just text you can cat.

</description><pubDate>Mon, 27 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Most projects start with the same small lie: &lt;em&gt;“I’ll just write it to a JSON
file for now.”&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;It works until it doesn’t. The file grows. Two goroutines write at once and you
get half a record. A crash mid-write truncates the whole thing. You add a
mutex, then a temp-file-and-rename, then a backup copy, then an index because a
linear scan got slow — and somewhere along the way you’ve hand-rolled a bad
database. So you give up and stand up PostgreSQL: a daemon, a schema, a
connection pool, a migration tool, and a container in every environment — to
store a few thousand records that fit in RAM.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;ScrivaDB is the thing that belongs in the gap between those two.&lt;/strong&gt; Durable and
queryable like a database, but on disk it’s still just text you can read.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;the-problem-it-solves&quot;&gt;The problem it solves&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;The gap is real and most stacks ignore it. On one side, a flat file is honest
and inspectable but has no concurrency safety, no crash durability, no query
engine, and no integrity checks. On the other, a full RDBMS gives you all of
that — and an operational tax you pay forever: a separate process to run and
monitor, a wire protocol, credentials, backups, version upgrades.&lt;/p&gt;
&lt;p&gt;ScrivaDB’s bet is that a large class of software — CLI tools, local services,
IoT daemons, desktop apps, small web backends, embedded state stores — needs the
&lt;em&gt;guarantees&lt;/em&gt; of a database without the &lt;em&gt;operational footprint&lt;/em&gt; of one. So it
keeps the guarantees and throws the footprint away:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Append-only writes.&lt;/strong&gt; Inserts, updates, and deletes are always new lines
appended to a segment file. Nothing is ever modified in place, so a crash
can’t corrupt an existing record — the worst case is a torn final line, which
is detected and skipped on load. A background compactor later merges and
deduplicates sealed segments.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;End-to-end integrity.&lt;/strong&gt; Every entry carries a CRC32C checksum. Silent
bit-rot on disk is caught on &lt;em&gt;read&lt;/em&gt; and surfaced as an error, instead of
quietly handing you wrong data.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;A real query engine.&lt;/strong&gt; Secondary indexes give O(1) equality and range
lookups; &lt;code dir=&quot;auto&quot;&gt;Find&lt;/code&gt; streams results with keyset pagination; aggregations run
server-side. It’s not “load everything and filter in a loop.”&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;More than key-value.&lt;/strong&gt; Caller-supplied keys, per-record revisions with
compare-and-swap, upsert, TTL, optimistic transactions, and live &lt;code dir=&quot;auto&quot;&gt;Watch&lt;/code&gt;
subscriptions.&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;&lt;h2 id=&quot;how-it-simplifies-local-storage&quot;&gt;How it simplifies local storage&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;The part that changes how it &lt;em&gt;feels&lt;/em&gt; to operate is the on-disk format. A
collection is a set of &lt;strong&gt;NDJSON segment files&lt;/strong&gt; — one JSON object per line,
appended in order. There is no binary blob, no page format, no WAL you can’t
read. Your data is text:&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;$&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;scriva-cli&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;insert&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;users&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;{&quot;name&quot;:&quot;alice&quot;,&quot;age&quot;:30}&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;--api-key&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;dev-key&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;✓&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;id=01J8...&lt;/span&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;key=01J8...&lt;/span&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;rev=&lt;/span&gt;&lt;span&gt;1&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;$&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;tail&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;-1&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;data/users/000001.ndjson&lt;/span&gt;&lt;span&gt;      &lt;/span&gt;&lt;span&gt;# your data is just text&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;span&gt;&quot;id&quot;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt;&quot;01J8...&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt;&quot;key&quot;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt;&quot;01J8...&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt;&quot;rev&quot;&lt;/span&gt;&lt;span&gt;:1,&lt;/span&gt;&lt;span&gt;&quot;data&quot;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;name&lt;/span&gt;&lt;span&gt;&quot;:&quot;&lt;/span&gt;&lt;span&gt;alice&lt;/span&gt;&lt;span&gt;&quot;,&quot;&lt;/span&gt;&lt;span&gt;age&lt;/span&gt;&lt;span&gt;&quot;:30},&quot;&lt;/span&gt;&lt;span&gt;crc&lt;/span&gt;&lt;span&gt;&quot;:&quot;&lt;/span&gt;&lt;span&gt;a1b2&lt;/span&gt;&lt;span&gt;&quot;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;div&gt;&lt;div aria-live=&quot;polite&quot;&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;That one property collapses a surprising number of operational chores into
tools you already know:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Inspect&lt;/strong&gt; with &lt;code dir=&quot;auto&quot;&gt;cat&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;grep&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;jq&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;tail -f&lt;/code&gt;. No client, no REPL, no
&lt;code dir=&quot;auto&quot;&gt;SELECT&lt;/code&gt; required to see what’s in there.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Back up&lt;/strong&gt; by streaming the directory through &lt;code dir=&quot;auto&quot;&gt;gzip&lt;/code&gt;. &lt;strong&gt;Restore&lt;/strong&gt; is
&lt;code dir=&quot;auto&quot;&gt;tar xzf&lt;/code&gt;. Your backup is a file, not a &lt;code dir=&quot;auto&quot;&gt;pg_dump&lt;/code&gt; ritual.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Debug&lt;/strong&gt; by reading. When something looks wrong, the source of truth is a
text file you can open — not an opaque store you have to interrogate through
the very layer you suspect.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Migrate and diff&lt;/strong&gt; with line-oriented Unix tools and version control
intuition, because every write is a line.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;And it’s &lt;strong&gt;one small Go binary&lt;/strong&gt;. No JVM, no Python runtime, no daemon zoo, no
config file required to get started. &lt;code dir=&quot;auto&quot;&gt;scriva serve&lt;/code&gt; gives you gRPC on &lt;code dir=&quot;auto&quot;&gt;:5433&lt;/code&gt;
and REST on &lt;code dir=&quot;auto&quot;&gt;:8080&lt;/code&gt; from a single process. Or skip the server entirely:
&lt;code dir=&quot;auto&quot;&gt;go get&lt;/code&gt; the engine and run the whole database &lt;strong&gt;in-process&lt;/strong&gt;, with no network
and no separate daemon — keyed CRUD, CAS, secondary indexes, and in-process
&lt;code dir=&quot;auto&quot;&gt;Watch&lt;/code&gt;, embedded directly in your Go program.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;where-to-use-it&quot;&gt;Where to use it&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;ScrivaDB is the right tool when:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;You need persistence without operating a database.&lt;/strong&gt; A CLI, a desktop app,
an IoT or edge daemon, a small internal service — anything where “install and
run Postgres” is heavier than the problem deserves.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Your working set fits on one machine.&lt;/strong&gt; Single-node data that you’re happy
to keep on local disk.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;You value being able to read your own data.&lt;/strong&gt; Auditability, debuggability,
and “just look at the file” are features you’ll actually use.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;You want one API from any language.&lt;/strong&gt; REST and gRPC come from the same
binary, and there are client SDKs across ten languages.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;You’re writing Go and want it embedded.&lt;/strong&gt; In-process storage with real
indexes and &lt;code dir=&quot;auto&quot;&gt;Watch&lt;/code&gt;, and zero gRPC/Prometheus/OTel dependencies pulled into
your binary.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;warden — a fleet supervisor for AI coding agents — embeds ScrivaDB as exactly
this: the local store for sessions, pipelines, and agent state, with no external
database to run alongside it.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;where-not-to-use-it&quot;&gt;Where &lt;em&gt;not&lt;/em&gt; to use it&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Being honest about the edges is the whole point of choosing a small tool
deliberately. Reach for something else when:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Your dataset is too large to compact on a single machine.&lt;/strong&gt; ScrivaDB is
single-node by design. If you need horizontal sharding, it’s the wrong shape.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;You need multi-node consensus and automatic failover.&lt;/strong&gt; Replication is
leader→follower with &lt;strong&gt;manual&lt;/strong&gt; failover — deliberately simple. There’s no
Raft, no automatic leader election. That’s a feature for small deployments and
a dealbreaker for high-availability ones.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;You need complex relational queries.&lt;/strong&gt; There are secondary indexes,
ranges, and aggregations — but no joins and no SQL planner. If your access
pattern is a five-table join, use a relational database.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;You have very high-cardinality, write-heavy churn&lt;/strong&gt; where compaction can’t
keep up, or you need the maturity and ecosystem of a decades-old engine for a
system of record at scale.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Picking a database is mostly picking your constraints. ScrivaDB’s constraints
are “one machine, readable on disk, no daemon to babysit” — and in exchange you
get durability, integrity, and a real query engine without the operational
weight. If that trade matches your problem, it’s a very comfortable place to
live.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;try-it&quot;&gt;Try it&lt;/h2&gt;&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://srjn45.github.io/scriva/start/what-is-scriva/&quot;&gt;What is ScrivaDB?&lt;/a&gt; — the two-minute tour.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://srjn45.github.io/scriva/start/install/&quot;&gt;Install&lt;/a&gt; the server and CLI.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://srjn45.github.io/scriva/start/quickstart/&quot;&gt;Quickstart&lt;/a&gt; — from empty directory to first query.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://srjn45.github.io/scriva/guides/embedding/&quot;&gt;Embedding guide&lt;/a&gt; — run it in-process from Go.&lt;/li&gt;
&lt;/ul&gt;</content:encoded><category>scrivadb</category><category>storage</category><category>local-first</category><category>go</category></item></channel></rss>