Two runtimes, one core
vitest runs in Node. The production deployment runs on Cloudflare Workers. Neither runtime sees the other's APIs. The same GitStore.create, GitStore.edit, Projection.query code path runs in both.
How? A small interface boundary.
StorageBackend
interface StorageBackend {
init(): Promise<void>;
read(path: string): Promise<string | null>;
write(path: string, content: string): Promise<void>;
list(prefix: string): Promise<string[]>;
commit(message: string, paths: string[]): Promise<CommitInfo>;
log(limit?: number): Promise<CommitInfo[]>;
// Optional — backends that support ETag/If-Match implement these.
readWithVersion?(path: string): Promise<VersionedRead | null>;
writeIfVersion?(path: string, content: string, expectedVersion: string): Promise<boolean>;
// Optional — rich audit feed.
activity?(opts?: { tenantId?: string; limit?: number }): Promise<ActivityEntry[]>;
}
That's the whole surface. Six required methods, three optional.
NodeFsBackend
Used in tests and local dev.
read/write→node:fs/promiseslist→readdircommit→isomorphic-gitagainst the local working treelog→isomorphic-git(returns[]cleanly on a fresh repo with no commits)
It does NOT implement the optional optimistic-concurrency methods — NodeFs is single-process, you don't need them. GitStore.edit detects their absence and falls back to read-then-write.
It does NOT implement activity — the local dev story doesn't need an audit feed; if you want one, run git log. The activity route on the Worker handles production.
R2Backend
Used in production on Cloudflare Workers.
read/write→bucket.get/bucket.putagainst R2list→bucket.listwith prefixcommit→ writes a JSON commit-log entry undercommits/, returns a synthetic oid. Real git happens later via the GitHub reconciliation cron.log→ reads the JSON entries backreadWithVersion/writeIfVersion→ uses R2's strong ETag +onlyIf.etagMatches. Real optimistic concurrency. See Optimistic concurrency.activity→ mergescommits/(pending) +synced/(already-reconciled) entries, sorted newest-first, filtered by tenant path prefix.
The R2 implementation is a hundred lines of TypeScript. It bundles into the Worker. No native modules.
ProjectionBackend
The other half:
interface ProjectionBackend {
exec(sql: string): Promise<void>;
run(sql: string, params: readonly unknown[]): Promise<void>;
all<T>(sql: string, params: readonly unknown[]): Promise<T[]>;
}
SqliteBackend wraps better-sqlite3 with a thin promise shim (better-sqlite3 is sync; the projection API is async). WAL mode for concurrent reads.
D1Backend wraps env.PROJECTION.prepare(sql).bind(...).all(). D1's smart prepared-statement caching means the same SQL repeated for many tenants is fast.
Why the boundary is small
I deliberately did NOT abstract over the differences between R2 and the filesystem. The boundary is at the level of "store a thing at a path" rather than "run a put operation with options." Pushing the complex bits into the implementation classes means:
- The interface is testable. The auth tests use an in-memory R2 mock that's about thirty lines.
- New runtimes are cheap. A browser backend that uses OPFS for the tree and sql.js for the projection? Two files, a few hundred lines.
- The
GitStoreandProjectioncode never has to know which runtime it's in. That's where the bugs would live if it did.
What this enables
Try running the production code locally:
git clone https://github.com/Trikulture-Kyokai-Synthetica/accelerando
cd accelerando && npm install && npm test
Fifty-something tests, three seconds. The R2 paths get mocked, the D1 paths get sqlite. The actual ERP logic — entity validation, atomic commits, optimistic concurrency on the abstract interface — runs identically.
When you deploy, you swap two constructors and the whole thing now lives on Workers + R2 + D1. No "but does it work in production" gap.