This article explores software preservation through browser-based applications.
Roman concrete is a metaphor for longevity here, not a verified report of a new archaeological finding. Software has no equivalent mechanism that automatically repairs obsolete APIs or lost data.
External services, dependencies and platforms can change over time. A useful preservation goal is to reduce dependencies and retain data, source, build instructions and compatible tools—not to promise unattended operation for a century.
We will examine a small SQLite/WASM journal design, including the gap between in-memory operation and durable storage. The example is illustrative and unexecuted, not a 100-year or self-healing system.
The Anatomy of Software Decay
Before we build, we have to understand why modern software decays. Concrete degrades due to chemical weathering, physical stress, and poor foundational mixtures. Software degrades due to:
- API Rot: External SaaS APIs change, deprecate endpoints, or go out of business.
- Runtime Deprecation: Language runtimes (Node.js, Python, Ruby) move forward, breaking backwards compatibility.
- Platform changes: operating systems, processors and host APIs evolve. A portable instruction set helps only when compatible imports and surrounding application facilities remain available.
- Service dependence: a server-dependent application needs continued operation or a migration path. Local capability can reduce that dependence, but backups and maintained runtimes are still necessary.
Aim for a small dependency set, documented execution requirements and exportable data in a documented format. SQLite and WebAssembly can help, but neither makes every host compatible or every file universally readable.
The stack: SQLite and WASM, with explicit limits
Why these two? Let's look at their credentials:
1. SQLite: a documented storage format
SQLite’s developers state an intention to support it through 2050, explicitly not an absolute promise. Its documented cross-platform file format supports preservation; reconstructing a correct reader is not guaranteed to be a weekend task, and active databases may also need journal/WAL files.
2. WebAssembly: portable instructions, host-dependent interfaces
WebAssembly defines portable instructions and an import mechanism. A module still requires its supported features and host interfaces; browser DOM and JavaScript dependencies do not automatically exist in a standalone runtime. No future device compatibility is guaranteed.
Architecting the Durable Ledger
This local-first journal keeps a small database in memory and offers an explicit SQLite-file export. Unexported entries are lost on reload or crash. An exported file is a snapshot, not immutable storage; preserve copies and verify recovery with compatible SQLite tools. No import UI or automatic persistence is implemented here.
Here is how the architecture looks conceptually:
Browser HTML and JavaScript UI ↕ official SQLite JS/WASM API In-memory SQLite database (volatile) ↓ explicit user export Downloaded SQLite snapshot Separate backup and restore verification required
Step-by-Step Implementation
CGO-free does not mean browser-compatible: the inspected modernc.org/sqlite platform list does not include js/wasm. The corrected browser example therefore uses SQLite’s official JS/WASM distribution. This replaces the source’s unsupported Go-driver build path, rather than claiming it compiled.
Step 1: Browser core (app.js)
After SQLite initializes, this educational JavaScript creates a table, binds input parameters, displays records as text and exports a database snapshot. Keep examples nonsensitive; the exported file is unencrypted. This code has not been run or visually tested.
// app.js — illustrative, unexecuted; uses matching local sqlite3.js/.wasm.
const output = document.getElementById("output");
const entry = document.getElementById("entry");
const save = document.getElementById("save");
const exportButton = document.getElementById("export");
let db;
function report(error) { output.textContent = String(error); }
sqlite3InitModule().then(sqlite3 => {
db = new sqlite3.oo1.DB(":memory:", "c");
db.exec(`CREATE TABLE journal (
id INTEGER PRIMARY KEY,
timestamp TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
entry TEXT NOT NULL
)`);
function refresh() {
const rows = [];
db.exec({ sql: "SELECT id,timestamp,entry FROM journal ORDER BY id DESC LIMIT 100",
rowMode: "object", resultRows: rows });
output.textContent = rows.map(row =>
`[${row.timestamp}] (#${row.id}): ${row.entry}`).join("\n") || "No entries";
}
save.addEventListener("click", () => {
try {
const text = entry.value.trim();
if (!text || text.length > 2000) throw new Error("Use 1–2000 characters");
if (db.selectValue("SELECT count(*) FROM journal") >= 1000) {
throw new Error("Demo limit reached; export your database");
}
db.exec({ sql: "INSERT INTO journal(entry) VALUES (?)", bind: [text] });
entry.value = "";
refresh();
} catch (error) { report(error); }
});
exportButton.addEventListener("click", () => {
try {
const bytes = sqlite3.capi.sqlite3_js_db_export(db);
const url = URL.createObjectURL(new Blob([bytes],
{ type: "application/vnd.sqlite3" }));
const link = document.createElement("a");
link.href = url;
link.download = "journal-snapshot.sqlite3";
document.body.append(link);
link.click();
link.remove();
setTimeout(() => URL.revokeObjectURL(url), 60000);
output.textContent = "Download requested. Verify the saved file and keep a backup.";
} catch (error) { report(error); }
});
save.disabled = exportButton.disabled = false;
refresh();
}).catch(report);
Step 2: Package matching SQLite/WASM assets
Use a pinned, verified SQLite WASM release and retain its matching JavaScript, WASM, source and licenses. This is a conceptual file layout, not a performed download, installation or build:
# Illustrative package layout, not an executed build # index.html # app.js # sqlite3.js (from the chosen SQLite WASM release) # sqlite3.wasm (from the same release)
The original main.wasm approach also required a matching Go wasm_exec.js and supported dependencies. The corrected example instead needs matching SQLite JS/WASM assets and compatible browser APIs. A WASM file alone is not the whole application.
Step 3: A small frontend (index.html)
The HTML uses native controls, a visible volatility warning and disabled buttons until initialization completes. Serve the package from a controlled local or HTTPS origin with correct JavaScript and WASM MIME types; test target browsers and accessibility rather than promising perfect future rendering.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>SQLite WASM Journal Demo</title>
<script src="sqlite3.js" defer></script>
<script src="app.js" defer></script>
</head>
<body>
<h1>SQLite WASM Journal Demo</h1>
<p>Memory only: reload or crash loses unexported entries.
Export a snapshot and verify the saved file. Do not enter sensitive data.</p>
<label for="entry">New entry (up to 2000 characters)</label>
<textarea id="entry" maxlength="2000"></textarea>
<button id="save" disabled>Add in memory</button>
<button id="export" disabled>Download SQLite snapshot</button>
<h2>Records and status</h2>
<pre id="output" role="status">Loading SQLite...</pre>
</body>
</html>
Durability benefits and limits
Each external dependency adds a maintenance obligation, but an upstream release or pricing change does not automatically break a pinned application. Compare hosting, updates, collaboration and recovery requirements rather than attributing inevitable failure to named vendors.
Three design questions remain after choosing SQLite and WASM:
1. Structural Inertia (Offline-First)
Once the required assets are available, database operations can remain local. Initial loading, module imports and the hosting origin still matter; offline startup is not implemented by the snippets. Preserve the original Go assets index.html, wasm_exec.js and main.wasm only when using a separately verified Go build; the corrected example has the SQLite file layout above.
2. Recovery is not automatic persistence
An in-memory database disappears when its connection or process ends. Rollback journals and WAL on persistent storage do not preserve this demo’s unsaved memory. SQLite crash recovery depends on storage and filesystem guarantees; it does not repair arbitrary corruption. OPFS is a separate persistence option with quotas, browser restrictions and backup requirements.
3. Extreme Portability
A future host may require changes to the UI, imports or storage integration even if it can execute main.wasm. Preserve source, toolchain versions, schema and sample exports, and periodically test restoration and migration.
Conclusion: Designing for Decades
This approach suits some small local tools, not every workload. Collaboration, security updates and critical archives need their own design and validation. Ask how the application and its data can be recovered when today’s runtime or service is unavailable.
SQLite and WASM can reduce some portability and deployment barriers. Longevity still requires maintained dependencies, verified exports, backups and migration planning; this journal is an educational starting point, not a preservation guarantee.
What are your thoughts? Have you experimented with compiling databases directly to the client side? How are you designing your applications to survive the next decade of tech shifts? Let me know in the comments below!
Until next time, keep coding smart.
End of article.