Blob

lib/blob (kalt-blob) stores objects in Garage. Garage speaks the S3 API. The library signs each request. Object bytes are not rows.

pnpm dev starts two local nodes from platform/blob. garage-workforce listens on http://127.0.0.1:3900. People, agents, and their books use it. garage-workspace listens on http://127.0.0.1:3910. Workspace logos use it. The bucket on each node is kalt. scripts/dev.sh points the workspace process at 3910 and leaves BLOB_ENDPOINT for workforce. Keys come from object_key, logo_key, and book_key.

Call kalt_blob::install() once at process start, then put, get, and delete.

kalt_blob::install()?;
kalt_blob::put("workspace/person/id", &bytes, "image/png").await?;
let object = kalt_blob::get("workspace/person/id").await?;
kalt_blob::delete("workspace/person/id").await?;

Store::from_env reads:

VariableLocal default
BLOB_ENDPOINThttp://127.0.0.1:3900
BLOB_BUCKETkalt
BLOB_REGIONgarage
BLOB_ACCESS_KEYGKkaltlocal0123456789abcdef
BLOB_SECRET_KEYthe dev secret in .env.example

A missing scheme on BLOB_ENDPOINT is treated as http://. Keys are object paths. Empty keys, NUL bytes, and . or .. segments are rejected. put overwrites the same key. get returns None when Garage replies that the object is missing. delete is idempotent.

Workforce avatars use the workforce node. POST /api/workforce/avatars/{kind}/{id} writes {workspace}/{kind}/{id}. GET /api/workforce/media?key= reads it back for a signed-in member of that workspace.

Workspace logos use the workspace node. POST /api/workspace/{id}/logo writes {workspace_id}/logo. GET /api/workspace/media?key= reads it back for a signed-in member of that workspace.

On Render, the kalt-blob env group holds BLOB_ACCESS_KEY and BLOB_SECRET_KEY. Set both before the first Garage boot. Garage keeps the secret it imported and refuses a later change for the same key id. The workforce service receives BLOB_ENDPOINT from kalt-workforce-garage. The workspace service receives it from kalt-workspace-garage.