Back to N8n Mcp

The CDN / URL Requirement for Chat Surfaces

data/skills/n8n-binary-and-data/CDN_REQUIREMENT.md

2.63.16.1 KB
Original Source

The CDN / URL Requirement for Chat Surfaces

When a workflow generates an image and the user wants it shown inside a chat message — Slack, Discord, Teams, Telegram, embedded webhook chat — the image in $binary is not enough. Chat clients render messages that reference images by URL (or push bytes through the platform's own file-upload API). None of them read the $binary slot. The bytes have to live somewhere a URL can fetch them over HTTPS, and n8n does not bundle a CDN — the user provides the storage.

Contents


Why $binary doesn't display

A chat message is HTML or a JSON block. An embedded image is a reference to a URL:

html

Some surfaces accept bytes directly through a platform file API instead of a URL — Slack's two-step files.getUploadURLExternal + files.completeUploadExternal, Discord attachments, Telegram sendPhoto. Either way, the bytes have to be reachable: either at a URL the client fetches, or handed to the platform's upload endpoint. The raw $binary slot inside an n8n execution is neither — it's internal to the workflow run.


What the user needs

A place that serves the image over a fetchable URL. Ask what they already have, but lead with a recommendation:

  1. A real object store / CDN (recommended). Cloudflare R2, AWS S3 (+ CloudFront), Google Cloud Storage, Azure Blob, Backblaze B2, Vercel Blob, Supabase Storage, Bunny CDN. Direct URL embedding works once the object is public, edge caching keeps latency low, and signed-URL flows are first-class. Cloudflare R2 is the lowest-friction starting point if they have nothing — a few minutes to set up, generous free tier, no egress fees.
  2. Drive-style services (fallback). Dropbox, Google Drive, OneDrive, Box can produce shareable links, but the URL shape and whether it renders as an `` varies, and some need the share link converted to a direct-download URL first. Confirm the service can serve an inline-renderable URL before committing to it.
  3. Self-hosted. The user serves from their own domain. Fine if it already exists; don't propose standing one up just for this.

The right choice depends on the user's existing infrastructure, cost tolerance, and how sensitive the content is.


What the workflow does

The shape is always generate → upload → reply-with-URL:

[Generate image] → [Upload to storage] → [Set: imageUrl = response URL] → [Send chat reply referencing imageUrl]

Concretely, uploading to an S3-compatible store (R2 here) via the HTTP Request node:

[AI node: generate image]            ← set options.binaryPropertyOutput so bytes land in $binary
   ↓ binary on the item
[HTTP Request: PUT to R2]
   url: https://<account>.r2.cloudflarestorage.com/<bucket>/<key>
   authentication: AWS-style signed (or the S3 node with the R2 endpoint)
   contentType: binaryData
   binaryPropertyName: data
   ↓
[Set: { imageUrl: "https://pub-<id>.r2.dev/<key>" }]
   ↓
[Send to chat surface: imageUrl embedded — markdown, Block Kit image block, adaptive card, etc.]

Upload mechanics vary by provider; most expose S3-compatible APIs usable through n8n's S3 node or HTTP Request with AWS auth. Confirm the upload node's field names (contentType, binaryPropertyName) with get_node, and error-branch the upload so a failed write surfaces instead of producing a reply that references a URL that was never written — see n8n-error-handling. The exact reply shape per platform is surface-specific (see AGENT_TOOL_BINARY.md).


How to tell the user

Don't quietly ship a workflow that generates images "but they don't display." Surface the requirement before building:

"I can generate the image, but the chat surface can't display raw binary — it embeds images by URL. So I'll need to upload the image somewhere that serves a public URL first. What do you use for image/file storage today (R2, S3, GCS, Dropbox, Google Drive, …)? If you don't have anything set up, Cloudflare R2 is the lowest-friction starting point."

There is no fallback that hides this — n8n won't host the file. If the user has no storage, pause until they pick a service and provision a bucket and credentials, then resume. (Posting the URL as a plain link rather than an inline image is a lighter option if inline rendering isn't critical — but that link still has to come from somewhere.)


Signing and expiration

URL typeTrade-offUse for
PublicAnyone with the URL can fetch it; simplestNon-sensitive content (already-public assets)
Signed, with expiryPer-request URL that expires (e.g. 1 hour)Sensitive or user-specific content

For internal chat with scoped channels, public is usually fine — the URL only lives inside messages a known set of users sees. For compliance-sensitive content, default to signed URLs with a short expiry. A permanently public, unguessable-but-non-expiring URL is a slow leak for anything private.


File naming

SchemeExampleNote
UUID / randomimg/abc-123-def-456.pngUnguessable; good default
Content hashimg/sha256-abc123….pngFree deduplication
User-prefixedusers/<userId>/<name>.pngEasy per-user cleanup

Avoid user-controlled filenames (path traversal, collisions) and sequential IDs (predictable, scrapeable).


Cleanup

Without it, storage costs grow:

  • Lifecycle rules — object stores (S3, R2, GCS, Azure Blob) auto-delete objects after N days. 7–30 days is usually plenty for chat use cases.
  • Scheduled cleanup workflow — for drive-style backends that have no TTL, run a workflow that lists and deletes old files.

Ask the user's retention preference rather than picking a window for them — chat artifacts are often disposable, but some surfaces (audit, support transcripts) need them kept.