accelerando.wiki ↗ app ↗ github

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.

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.

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:

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.