Files
Files are the unified content primitive — every piece of user content is a file row, with images and videos as automatic specializations.
Files are the unified content primitive in Aeontel. Every piece of user content — documents, images, videos, datasets, anything — is a file row. There is no separate "upload" or "asset" entity: a file starts in the uploading state, transitions to ready once bytes are in R2 and the MIME type has been sniffed, or to error on any terminal failure.
Storage model
Each workspace gets its own R2 bucket (aeontel-wsp-{"{id}"} ), created on workspace.created. R2 storage is flat and keyed by file id — every file object lives at files/{"{fileId}"}. Hierarchical organization lives entirely in the database via directories ; R2 never sees a directory path.
Multipart upload state (the R2 multipart ID, per-part ETags, total size) lives inline on the file row and is nulled out once status = "ready". There is no separate bookkeeping table.
Upload lifecycle
Uploads are a standard R2 multipart flow directly on the file entity:
POST /api/files— initiate. Server creates the file row inuploadingstate and returns signed PUT URLs for each part.- For each part, the client PUTs the raw bytes directly to the signed
URL and captures the
ETagfrom the response. POST /api/files/:id/parts— sign more parts if the initial batch wasn't enough.POST /api/files/:id/complete— finalize. Server completes the R2 multipart upload, sniffs the MIME type, and flips the row toready.POST /api/files/:id/abort— on any failure. Deletes the row and buffered R2 parts.
The CLI's aeontel file upload command drives this whole lifecycle from Node with a progress bar. In the browser, the platform Content page uses the same flow with parallel part uploads.
Image and video specializations
side tables onfile, keyed by file id. They are populated automatically when a file with an image/* or video/* MIME type reaches the ready state — an event handler emits image.provision_requested or video.provision_requested, a downstream handler provisions the asset in Cloudflare Images or Cloudflare Stream, and the side-table row is inserted. You never need to "promote" a file — upload with the right MIME type and the specialization appears.
Operations
GET /api/files— list files, filter byworkspace_id,directory_id, ormimeprefixGET /api/files/:id— retrieve file metadataPOST /api/files//parts//complete//abort— the multipart upload flowPATCH /api/files/:id— rename, move, or setexpiresAtGET /api/files/:id/download— short-lived signed R2 download URLDELETE /api/files/:id— delete the file (handler cleans up R2, CF Images, and CF Stream)POST /api/files/pdf-from-url— render a URL to PDF via Cloudflare Browser Rendering and store the result as a filePOST /api/files/screenshot-from-url— capture a URL as a screenshot and store the result as a filePOST /api/files/markdown-from-url— render a URL and extract its content as markdown, stored as atext/markdownfilePOST /api/files/html-from-url— capture the post-render HTML of a URL, stored as atext/htmlfilePOST /api/files/:id/to-markdown— convert an uploaded file (PDF, DOCX, PPTX, XLSX, image, HTML) to markdown via Cloudflare Workers AI, stored as a newtext/markdownfile
All operations are available via the CLI ( aeontel file list/get/upload/update/download/read/delete/pdf-from-url/screenshot-from-url/markdown-from-url/html-from-url/to-markdown ), the MCP server, and the @aeontel/react hooks.
TTL and browser-rendered files
Every file row supports an optional expiresAt timestamp. When set, a daily cleanup worker removes the row and its R2 object once past the deadline — the standard file.deleted event still fires so downstream CF cleanup cascades normally.
Files generated by pdf-from-url / screenshot-from-url default to temp: they land at the workspace root with a 1-day TTL. Passing a directoryId switches to permanent placement; passing an explicit ttlMs (or null for "never") overrides either default. To promote a temp file to permanent, PATCH /api/files/:id with expiresAt: null (and optionally a new directoryId).
Screenshots produced via screenshot-from-url are plain image files — they are not auto-provisioned as CF Images entities. Callers that want variants can materialize an image from a permanent copy.
Events
file.created— file row inserted inuploadingstatefile.ready— multipart upload finalized, MIME sniffedfile.error— terminal failurefile.updated— rename or movefile.deleted— delete; payload carriesr2Key,bucketName, and optionallycfImageId/cfStreamUidso the cleanup handler can cascade into CF Images and CF Stream.