Attachments
Cards support file attachments. Upload from the card detail panel, or attach a URL. Attachments can optionally be scoped to a specific comment.
Storage drivers
Attachment bytes go to a pluggable StorageDriver, chosen with ATTACHMENT_STORAGE (see Configuration):
local(default, zero-config): the api proxies upload/download bytes and stores them on disk underATTACHMENT_LOCAL_PATH. No external dependency — ideal for self-hosting a single host. Compose persists the bytes in a volume.s3(production): any S3-compatible backend (AWS S3, MinIO, R2, …). The api issues presigned PUT/GET URLs, so bytes never transit the api; downloads302-redirect to a short-TTL signed URL.
The driver is transparent to clients — the same endpoints and the same MCP card_write { method: "attach_url" } work regardless.
Limits
Server-side limits are enforced on every surface (REST, MCP, URL ingestion), regardless of driver:
- per-attachment size —
ATTACHMENT_MAX_BYTES(default 25 MiB) - per-card count —
ATTACHMENT_MAX_PER_CARD(default 20) - content-type allowlist —
ATTACHMENT_CONTENT_TYPES(default: images, PDF, text/CSV, common office docs, zip) - aggregate quotas — per board and global byte caps
Upload lifecycle
A direct file upload is a two-step, confirmed flow: the client creates a pending attachment (reserving its declared size against the quota), uploads the bytes, then confirms it. Unconfirmed uploads are swept and deleted after ATTACHMENT_PENDING_TTL_SECONDS. A confirmed upload appends a card.attachment_added activity (payload { cardId, attachmentId, filename, contentType, sizeBytes }); deletion appends card.attachment_removed. Comment-scoped attachments carry the commentId, and deleting the comment removes its attachments.
Attaching a URL (agents)
An agent can attach an external URL directly:
// MCP
card_write { "method": "attach_url", "params": { "cardId": "…", "url": "https://…" } }URL attachments go through the same SSRF guard as webhooks and require https.