canonical term: blob | table: hol_blobsThis document explains the Blob concept — the storage ledger that connects Cloudflare R2 objects to House of Legends shows, offers, menus, and operator-managed assets.
Doc Status: Current implementation guide | Verified against Hono storage services, Drizzle schema, R2 helpers, generated client calls, seeder uploads, and deployed menu/public asset routes.
House of Legends stores images, PDFs, and uploaded operational assets in Cloudflare R2. R2 is where the file bytes live; hol_blobs is where the application records what the file is, who created it, how it should be used, and whether it is still active.Without the blob ledger, an R2 key is just a string. With the ledger, operators can list assets, attach them to resources, activate a menu PDF, audit uploads, and eventually clean up unused files safely.
A Blob is the application record for one uploaded R2 object. It does not contain the file bytes. It contains the metadata that makes an R2 object understandable to the product.Every blob records:
How the app names it — caller-provided slug
Where the file lives — R2 bucket and object key
How the app serves it — public API URL in publicUrl
What it is — original filename, MIME type, size, purpose
Where it came from — operator upload or seeder upload
Lifecycle state — active, archived, or a reserved pending state
Audit context — creating role, subject, timestamps, and metadata
R2 is not the source of truth for product meaning. The database is. Code should treat the R2 key as an address and the blob row as the application-owned record.
Do not store raw R2 S3 API URLs in product tables. Store a blob ID or object key, then resolve the browser-readable URL through the backend.
The caller can dictate the slug; it cannot dictate the id. The backend owns the ULID because IDs must stay globally unique and database-safe. The backend also owns the R2 key because object addressing is infrastructure, not product copy.Use slug whenever humans, seed manifests, dashboard workflows, or resource relationships need a stable name:
Do not parse originalFilename to recover identity. Filenames are user input and operating-system metadata. They can change, contain spaces, include duplicate names, or lose product meaning during export. Store the filename for display and audit only.
If a seed or dashboard upload needs a meaningful name, pass slug explicitly. Do not encode the filename into the R2 key and do not derive the blob’s identity by splitting filename strings.
purpose is a flat category for browsing and operational grouping.
Purpose
Typical use
menu
Menu PDFs uploaded from the dashboard or seeder
addons
Add-on images
offers
Offer and menu-item images
shows
Show thumbnails and galleries
show-events
Future event-specific media
uploads
General operator uploads
seed
Seeder-managed assets that do not fit a narrower purpose
purpose should not replace business ownership. If a blob belongs to a show, offer, or another entity, use a direct blob field or an asset link to express that relationship.
Reserved schema value; current dashboard and seeder uploads should not create this state
ACTIVE
The R2 object exists and the blob can be used
ARCHIVED
The blob should no longer be used for new links
The direct Worker flow writes bytes to R2 before inserting the active blob row, so the database should not point at an object that was never uploaded. A normal dashboard or seeder upload should land as ACTIVE.
Dashboard surfaces send multipart form data to Hono:
POST /api/storage/uploadcontentType: multipart/form-datafields: file: menu.pdf purpose: menu slug: menu-dinner-show bookingPath: DINNER_SHOW
Hono validates the request, generates a unique object key, writes the bytes with env.R2_BLOBS.put(...), and records the blob as ACTIVE. The filename remains available as originalFilename, but the dashboard should use slug for stable identity.
The seeder sends base64 assets through the superuser seed endpoint with an explicit slug. Hono writes the object with env.R2_BLOBS.put(...), records the blob with source=SEED, and can activate menu blobs during seed.The seeder does not write R2 metadata or database rows directly. Seed manifests should provide the blob slug; the upload filename should only identify which local file to read.
hol_asset_links is the contextual relationship between a blob and a product resource.A single blob row answers “what file is this?” An asset link answers “how is this file being used here?”
Field
Purpose
blobId
The uploaded file record
resourceType
Loose resource namespace, such as show or offer
resourceId
Resource identifier
role
gallery, thumbnail, avatar, image, attachment, video, or archived
displayOrder
Ordering for galleries
isPrimary
Primary image marker
caption / altText
Per-use presentation metadata
Asset links are intentionally loose references. Adding a new resource type should not require a storage migration.
Use hol_asset_links when the same storage object needs per-resource presentation metadata, ordering, or audit history. Use a direct blob ID only for simple one-to-one fields such as a show
thumbnail.
The public asset route reads the object from the private R2_BLOBS binding and returns the object’s HTTP metadata, ETag, and a long-lived cache header.This keeps R2 credentials and the raw S3-compatible endpoint out of the frontend.
The long-lived immutable cache is only safe for generated blob keys that are never overwritten. slug can be stable while key stays immutable. Stable product routes, such as active menu PDFs,
must revalidate.
Menu PDFs use the blob ledger and stream from the active R2_BLOBS object.When an operator activates a menu blob:
Hono verifies the selected blob still exists in R2_BLOBS.
Hono marks other menu blobs for the same bookingPath inactive.
Hono marks the selected menu blob active.
Guest menu routes stream the active PDF from R2_BLOBS by booking path.
The guest route is:
GET /api/storage/menu/{bookingPath}/menu.pdf
If no active blob exists for that booking path, the route returns 404. This is intentional: menu files must be managed through the dashboard blob ledger.