Uploads
Uploads are files keyed by an upl_... ID and attached to messages through a join table. The default backend is local disk; Cloudflare R2 can store bytes for ephemeral container deployments. The server streams uploads back to authenticated workspace members and serves common safe preview types inline.
#Endpoints
POST /api/uploads?workspace_id=... # multipart: file; form workspace_id also supported
GET /api/uploads/{upload_id} # streams the file
POST /api/messages/{message_id}/attachments # { upload_id }
- Upload size cap:
64 MiBper request. - The file is written to the configured upload backend under a random
Content-Typefalls back toapplication/octet-streamwhen the client- Guests cannot upload or attach files while they are still in the waiting
upload-* key. The original filename is recorded in the uploads row but not used as the storage key.
doesn't send one.
room. Timed-out and blocked users are also upload-restricted.
#Attaching to a message
POST /api/messages/{message_id}/attachments records a row in message_attachments. The store hydrates attachments on ListMessages/GetThread, so subsequent reads include the attachment list without an extra round-trip.
The handler checks that the requester can read the upload, can access the message workspace, and is the message author before linking. Bot tokens need both uploads:write and messages:write.
The web client renders common previewable types in compact attachment cards:
image/*inline, preserving recorded dimensions when available.video/*as inline native players with controls.audio/*as inline native audio controls.application/pdfas a first-page thumbnail card with the filename, size,text/plainas a lightweight text-file card.
and authenticated download link.
Other content types appear as authenticated download cards that link to /api/uploads/{upload_id}. The server sends Content-Disposition: inline only for the safe preview set (image, video, audio, text/plain, and application/pdf) and keeps X-Content-Type-Options: nosniff plus a sandbox content-security policy on upload responses.
#Storage layout
Local disk:
<data>/
clickclack.db
uploads/
upload-XXXXXXXX
...
logs/
Configure <data> with --data or CLICKCLACK_DATA. The server creates uploads/ on demand when the first request arrives.
R2:
CLICKCLACK_UPLOADS=r2://clickclack-uploads/prod
CLICKCLACK_R2_ACCOUNT_ID=...
CLICKCLACK_R2_ACCESS_KEY_ID=...
CLICKCLACK_R2_SECRET_ACCESS_KEY=...
R2 keys are stored in the database as r2://bucket/prefix/upload-.... Download requests are still authenticated by ClickClack before the object is fetched from R2.
#What is intentionally missing
- Server-side image thumbnailing/transcoding.
- Virus scanning.
- Per-workspace quotas.