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:
| Variable | Local default |
|---|---|
BLOB_ENDPOINT | http://127.0.0.1:3900 |
BLOB_BUCKET | kalt |
BLOB_REGION | garage |
BLOB_ACCESS_KEY | GKkaltlocal0123456789abcdef |
BLOB_SECRET_KEY | the 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.