The log server at the edge: a Cloudflare Worker
../log-server as a Cloudflare Worker: the same routes, POST /add and the
tlog-tiles read API, from the same file
(../log-server/src/log_server.ts, imported as a workspace
package), with the log kept in a SQLite-backed Durable Object. It is one deployment target among
several: the identical handler runs on Node, Bun and Deno over any SQLite, and nothing in it knows
which runtime serves it.
Source in examples/edge; runs on Cloudflare Workers
; its guide is At the edge .
How it works
client ──▶ Worker fetch ──▶ env.LOG.getByName("log") ──▶ LogObject (Durable Object)
openServerLog({
key: importLogKey(env.LOG_SKEY),
storage: { sqlite: fromDurableObjectStorage(ctx.storage) } })
newLogServer(log) ← ../log-server
other Workers ──▶ RPC: env.LOG.getByName("log").add(data) → receipt
- One object per log. The runtime runs one instance of a Durable Object at a time, so the object
is its database’s only writer:
fromDurableObjectStorageknows it, and its locks are local, with none of the lease’s writes. Run more logs by addressing more objects. - Opened once per instance, under
blockConcurrencyWhile; the appender’s timers then batch and publish while the instance lives, and the next instance resumes from storage. Pending timers keep the instance in memory (and billed for duration) while the log is open. - Durability.
POST /addanswers once a published checkpoint covers the entry, and the runtime’s output gate holds the response until the writes are persisted.
Other deployment targets. On D1, storage: { sqlite: fromD1(env.DB) } uses lease locking, so
any number of Worker instances can share the database; but a stateless Worker keeps no timers
between requests, so each request would open the log afresh, which a Durable Object avoids. On a
server or container, use ../log-server with node:sqlite, bun:sqlite, libSQL or
rqlite. Choosing storage compares them.
Run it
Build the library once at the repository root (bun run build), then here:
node scripts/keygen.ts localhost/my-log # prints the key pair, and the .dev.vars line
echo 'LOG_SKEY=PRIVATE+KEY+localhost/my-log+…' > .dev.vars
npx wrangler dev
$ curl -X POST --data-binary 'hello, edge' http://localhost:8787/add; echo
0
$ curl http://localhost:8787/checkpoint
localhost/my-log
1
JULsgW01d/mNqUZJrgOXttsvzDNwVjanoq6450j2EsI=
— localhost/my-log ZHGmMt1PNGVfOoCvUU6RV/oqCOi+59uCMoJmx7RKt7s2eaMtzqA1I/FQhnOiAcQ228/pyzAJVhhirGMnt/Vdvc/mtAE=
Deploy with npx wrangler secret put LOG_SKEY (paste the private key) and npx wrangler deploy.
The Worker needs no nodejs_compat flag: webtessera uses only web-standard APIs. Verify the
deployed log with ../log-server/scripts/verify.ts, or with
Tessera’s Go client (../log-server/go).
Outside this repository
Inside the repository this example uses the library it sits in: it declares webtessera as an
optional peer dependency, which Bun’s workspace never installs, and bun install links the
repository root in its place. Copied out on its own, it needs a real dependency instead. Make the
change once, in the copy:
npm pkg delete peerDependencies peerDependenciesMeta && npm install --legacy-peer-deps webtessera@^0.1.0
--legacy-peer-deps works around npm 10, the npm bundled with Node.js 22, which fails to install
Vitest 4.1 with Cannot read properties of null (reading 'edgesOut'); npm 11, Bun and pnpm do not
need it. To try a build that is not released yet, run bun pm pack at the repository root and give
npm install the tarball’s path in place of webtessera@^0.1.0. Everything above then works as
written, bun run build aside.
This example also uses the log server example’s routes, as the workspace package
webtessera-example-log-server. Copy ../log-server next to it, make the same
change there, and point this example at it:
npm pkg set dependencies.webtessera-example-log-server=file:../log-server
Trust model
As for ../log-server: LOG_SKEY is the one secret, held as a Workers
secret and imported as a non-extractable WebCrypto key; clients verify every entry against a signed
checkpoint and every checkpoint against the last; split views need monitors or witnesses to catch.
Here the platform also holds the data, so Cloudflare is in the same position as any host: it can
withhold the log, not rewrite it unseen.
Test
npm run ci # tsc --noEmit && vitest run, inside workerd
src/worker_test.ts runs in workerd with
@cloudflare/vitest-pool-workers
and drives the Worker over HTTP: POST /add answering once a signed checkpoint covers the entry,
tiles and bundles from which webtessera’s client verifies inclusion (including the largest entry
tlog-tiles allows), the cache headers, the errors, and appends over RPC with receipts that verify.
vitest.config.ts enables nodejs_compat for the test pool only.
Files to read first
src/worker.ts: the Durable Object and the Worker.../log-server/src/log_server.ts: the routes, shared with Node, Bun and Deno.
This page is generated from examples/edge/README.md
. Edit it on GitHub.