Runtime Boundary Design — AKM 0.9.0
Implementation note (added later): both target files below now exist —
src/storage/database.tsandsrc/runtime.ts— so this design has landed. The "3 hard-import files" named in the original checklist (src/indexer/db.ts,src/core/state-db.ts,src/workflows/db.ts) no longer describes the current tree:src/indexer/db.tswas later split intosrc/storage/repositories/index-*.ts(itsDatabaseopener lives insrc/storage/repositories/index-connection.ts, which importsopenDatabase/Databasefromsrc/storage/database.tsas this design specifies).src/workflows/db.tsdid exist historically but has since been removed — workflow run persistence now lives insrc/storage/repositories/workflow-runs-repository.ts.src/core/state-db.tsstill exists and also imports fromsrc/storage/database.ts. The checklist items below are kept as the historical design record.
Problem
Runtime-specific dependencies (bun:sqlite, Bun.* APIs) leak directly into 33+ source files.
This creates unnecessary coupling, makes Node.js compatibility hard, and means every future
runtime change has a large blast radius.
Goal
Two files own the entire runtime boundary. Everything else is clean application code.
File 1: src/storage/database.ts
Single source of truth for SQLite. The rest of the app never imports from bun:sqlite or
better-sqlite3 directly — only from here.
Responsibilities:
- Detect runtime (bun vs node) once at module load
- Load the appropriate SQLite driver (
bun:sqliteon Bun,better-sqlite3on Node) - Export
Databasetype — the common structural type both drivers share - Export
openDatabase(path, opts?)factory
Design constraints:
- No adapter classes, no interface hierarchies — the driver APIs are already nearly identical
- The exported
Databasetype should be the structural intersection of what both drivers expose openDatabase()should be a simple function, not a factory classdb.query()(Bun-specific) must be normalised — replace withdb.prepare().all()at this boundary
Architect: investigate and fill in:
- [ ] Read
src/indexer/db.ts,src/core/state-db.ts,src/workflows/db.ts— the 3 hard-import files - [ ] Identify every method AKM actually calls on a
Databaseinstance (exec, prepare, transaction, close, query, etc.) - [ ] Check whether
better-sqlite3's type definitions can be used as the exportedDatabasetype directly, or if a small structural type alias is needed - [ ] Check
sqliteVec.load(db)— confirm whether it accepts both driver handles or needs a raw handle escape hatch - [ ] Write the complete file
File 2: src/runtime.ts
~60 lines. Named exports for every Bun.* API called outside of the SQLite layer.
Each export works on both runtimes.
Known Bun. call sites to cover (architect: verify these are complete):*
| Export name | Bun API | Node equivalent |
|---|---|---|
spawnSync(cmd, opts) |
Bun.spawnSync |
child_process.spawnSync |
spawn(cmd, opts) |
Bun.spawn |
child_process.spawn |
readStdin(limitBytes) |
Bun.stdin.stream() |
for await (chunk of process.stdin) |
writeResponseToFile(path, res) |
Bun.write(path, response) |
stream/promises pipeline |
sha256Hex(data) |
new Bun.CryptoHasher('sha256') |
node:crypto createHash |
md5Hex(data) |
new Bun.CryptoHasher('md5') |
node:crypto createHash |
semverOrder(a, b) |
Bun.semver.order |
semver.compare (add dep) |
getDirname(importMetaUrl) |
import.meta.dir |
path.dirname(new URL(url).pathname) |
resolveModule(spec, from) |
Bun.resolveSync |
require.resolve (already has fallback) |
sleepSync(ms) |
Bun.sleepSync |
Atomics.wait (already has fallback) |
mainPath |
Bun.main |
process.argv[1] |
Design constraints:
- Runtime detection:
const isBun = !!process.versions?.bunat top of file, used inline - No class, no factory — just named function exports
- Each export branches once on
isBun, no per-call overhead node:cryptois available in both Bun and Node — the crypto exports can skip theisBunbranch entirely and just usecreateHashalways (simpler, same perf for a CLI)
Architect: investigate and fill in:
- [ ]
grep -rn "Bun\." src --include="*.ts"— confirm the table above is complete - [ ]
grep -rn "import\.meta\.dir" src --include="*.ts"— list all sites, note which have fallbacks - [ ] Confirm
Readable.toWeb()is the right Node 18 approach forwriteResponseToFile - [ ] Write the complete file
Call Site Changes
Once both files exist, the remaining work is mechanical:
SQLite type-only imports (~30 files):
// Before
import type { Database } from 'bun:sqlite'
// After
import type { Database } from '../storage/database'
SQLite hard imports (3 files — indexer/db.ts, core/state-db.ts, workflows/db.ts):
- Replace
import { Database } from 'bun:sqlite'withimport { openDatabase, type Database } from '../storage/database' - Replace
new Database(path)withopenDatabase(path) - Replace
db.query<T>(sql).all(params)withdb.prepare(sql).all(params)(Bun-specific API) - Remove
SQLQueryBindingsspread casts
Bun. call sites (~10 files):*
// Before
Bun.spawnSync(['git', 'ls-files', ...])
// After
import { spawnSync } from '../runtime'
spawnSync(['git', 'ls-files', ...])
Architect: investigate and fill in:
- [ ] List every call site file and the exact import line change needed
- [ ] Flag any call site where the shim return shape differs from the Bun API (e.g. stdout as string vs Buffer)
package.json Changes
- Add
better-sqlite3asoptionalDependency(prebuilt binaries, no compile step on common platforms) - Add
@types/better-sqlite3todevDependencies - Add
semvertodependencies(tiny, zero-deps) - Add
node: ">=22"toengines(raised from 20.12 when Node 20 support was dropped, 2026-07) (@clack/coreusesnode:util.styleText, which was added in Node 20.12) - Require Node >= 22 in the npm
preinstallguard
What This Is NOT
- Not an adapter pattern with interface hierarchies
- Not a ports-and-adapters architecture
- Not a plugin system
- Not a DI container
Just two modules that own the runtime details so the rest of the codebase doesn't have to.
Child Issues to Create (after design is complete)
src/storage/database.ts+ migrate 3 hard-import files- Mechanical: update 30 type-only imports (sed pass + tsc verify)
src/runtime.ts+ update ~10 call sitespackage.json+ relax preinstall guard + CI Node matrix
Plus: audit open GitHub issues on itlackey/akm and close any that are superseded by this work or otherwise stale (milestone 0.9.0 items already addressed, etc.).