.agents/skills/bknd-serve-files/SKILL.md
Use when serving uploaded files to users. Covers API-proxied file serving, direct storage URLs (S3/R2/Cloudinary), CDN configuration, public file URLs, caching headers, image optimization with Cloudinary, and serving files in frontend applications.
npx skillsauth add cameronapak/cultivate-fellowship bknd-serve-filesInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Serve uploaded files from Bknd storage to users via API proxy or direct storage URLs.
bknd-file-upload skillBknd supports two approaches to serve files:
| Method | Use Case | Performance | Control | |--------|----------|-------------|---------| | API Proxy | Simple setup, private files | Moderate | Full (auth, permissions) | | Direct URL | High traffic, public files | Best (CDN) | Limited (bucket ACLs) |
Files served through Bknd API at /api/media/file/{filename}.
import { Api } from "bknd";
const api = new Api({ host: "http://localhost:7654" });
// Build file URL
const fileUrl = `${api.host}/api/media/file/image.png`;
// "http://localhost:7654/api/media/file/image.png"
function Image({ filename }) {
const { api } = useApp();
const src = `${api.host}/api/media/file/${filename}`;
return <img src={src} alt="" />;
}
// Get as File object
const file = await api.media.download("image.png");
// Get as stream (for large files)
const stream = await api.media.getFileStream("image.png");
# Test file access
curl -I http://localhost:7654/api/media/file/image.png
# Response includes:
# Content-Type: image/png
# Content-Length: 12345
# ETag: "abc123..."
Serve files directly from S3/R2/Cloudinary for better performance.
// S3 URL pattern
const s3Url = `https://${bucket}.s3.${region}.amazonaws.com/${filename}`;
// "https://mybucket.s3.us-east-1.amazonaws.com/image.png"
// R2 URL pattern (public bucket)
const r2Url = `https://${customDomain}/${filename}`;
// "https://media.myapp.com/image.png"
Cloudinary provides automatic CDN and transformations:
// Basic URL
const cloudinaryUrl = `https://res.cloudinary.com/${cloudName}/image/upload/${filename}`;
// With transformations
const optimizedUrl = `https://res.cloudinary.com/${cloudName}/image/upload/w_800,q_auto,f_auto/${filename}`;
// Helper to get direct URL based on adapter type
function getFileUrl(filename: string, config: MediaConfig): string {
const { adapter } = config;
switch (adapter.type) {
case "s3":
// S3/R2 URL from configured endpoint
return `${adapter.config.url}/${filename}`;
case "cloudinary":
return `https://res.cloudinary.com/${adapter.config.cloud_name}/image/upload/${filename}`;
case "local":
// Always use API proxy for local
return `/api/media/file/${filename}`;
default:
return `/api/media/file/${filename}`;
}
}
Create R2 bucket in Cloudflare dashboard
Enable public access on bucket
Configure custom domain (Cloudflare DNS):
media.yourapp.com -> <bucket>.<account>.r2.devUse in Bknd config:
export default defineConfig({
media: {
enabled: true,
adapter: {
type: "s3",
config: {
access_key: process.env.R2_ACCESS_KEY,
secret_access_key: process.env.R2_SECRET_KEY,
url: `https://${process.env.R2_ACCOUNT_ID}.r2.cloudflarestorage.com/${process.env.R2_BUCKET}`,
},
},
},
});
const publicUrl = `https://media.yourapp.com/${filename}`;
Create S3 bucket with public read (or CloudFront OAI)
Create CloudFront distribution:
Use CloudFront URL:
const cdnUrl = `https://d123abc.cloudfront.net/${filename}`;
// Or with custom domain
const cdnUrl = `https://cdn.yourapp.com/${filename}`;
Cloudinary includes global CDN automatically:
export default defineConfig({
media: {
enabled: true,
adapter: {
type: "cloudinary",
config: {
cloud_name: "your-cloud-name",
api_key: process.env.CLOUDINARY_API_KEY,
api_secret: process.env.CLOUDINARY_API_SECRET,
},
},
},
});
Files served from res.cloudinary.com with global CDN.
// Build optimized image URL
function getOptimizedImage(filename: string, options: {
width?: number;
height?: number;
quality?: "auto" | number;
format?: "auto" | "webp" | "avif" | "jpg" | "png";
crop?: "fill" | "fit" | "scale" | "thumb";
} = {}) {
const cloudName = process.env.CLOUDINARY_CLOUD_NAME;
const transforms: string[] = [];
if (options.width) transforms.push(`w_${options.width}`);
if (options.height) transforms.push(`h_${options.height}`);
if (options.quality) transforms.push(`q_${options.quality}`);
if (options.format) transforms.push(`f_${options.format}`);
if (options.crop) transforms.push(`c_${options.crop}`);
const transformStr = transforms.length > 0 ? transforms.join(",") + "/" : "";
return `https://res.cloudinary.com/${cloudName}/image/upload/${transformStr}${filename}`;
}
// Usage
const thumb = getOptimizedImage("avatar.png", {
width: 100,
height: 100,
crop: "fill",
quality: "auto",
format: "auto",
});
// "https://res.cloudinary.com/mycloud/image/upload/w_100,h_100,c_fill,q_auto,f_auto/avatar.png"
// Responsive images
const srcSet = [400, 800, 1200].map(w =>
`${getOptimizedImage(filename, { width: w, format: "auto" })} ${w}w`
).join(", ");
// Thumbnail generation
const thumb = getOptimizedImage(filename, {
width: 150,
height: 150,
crop: "thumb",
});
// Automatic format (WebP/AVIF when supported)
const optimized = getOptimizedImage(filename, {
quality: "auto",
format: "auto",
});
function StoredImage({ filename, alt, ...props }) {
const { api } = useApp();
const [error, setError] = useState(false);
// API proxy URL as fallback
const apiUrl = `${api.host}/api/media/file/${filename}`;
// Direct CDN URL (configure based on your adapter)
const cdnUrl = `https://media.yourapp.com/${filename}`;
return (
<img
src={error ? apiUrl : cdnUrl}
alt={alt}
onError={() => setError(true)}
{...props}
/>
);
}
function ResponsiveImage({ filename, alt, sizes = "100vw" }) {
const cloudName = process.env.NEXT_PUBLIC_CLOUDINARY_CLOUD_NAME;
const base = `https://res.cloudinary.com/${cloudName}/image/upload`;
const srcSet = [400, 800, 1200, 1600].map(w =>
`${base}/w_${w},q_auto,f_auto/${filename} ${w}w`
).join(", ");
return (
<img
src={`${base}/w_800,q_auto,f_auto/${filename}`}
srcSet={srcSet}
sizes={sizes}
alt={alt}
loading="lazy"
/>
);
}
function DownloadButton({ filename, label }) {
const { api } = useApp();
const [downloading, setDownloading] = useState(false);
const handleDownload = async () => {
setDownloading(true);
try {
const file = await api.media.download(filename);
// Create download link
const url = URL.createObjectURL(file);
const link = document.createElement("a");
link.href = url;
link.download = filename;
link.click();
URL.revokeObjectURL(url);
} catch (err) {
console.error("Download failed:", err);
} finally {
setDownloading(false);
}
};
return (
<button onClick={handleDownload} disabled={downloading}>
{downloading ? "Downloading..." : label || "Download"}
</button>
);
}
Set cache headers when uploading:
// Custom adapter with cache headers (advanced)
// S3 adapter doesn't expose this directly; configure via bucket policy
// or CloudFront cache behaviors
In Cloudflare dashboard:
Bknd's API proxy supports standard HTTP caching:
# Client can use conditional requests
curl -H "If-None-Match: \"abc123\"" \
http://localhost:7654/api/media/file/image.png
# Returns 304 Not Modified if unchanged
Configure default role with media.read permission:
export default defineConfig({
auth: {
guard: {
roles: {
anonymous: {
is_default: true,
permissions: {
"media.read": true, // Public read access
},
},
},
},
},
});
Remove media.read from anonymous:
export default defineConfig({
auth: {
guard: {
roles: {
user: {
permissions: {
"media.read": true,
"media.create": true,
},
},
// No anonymous role, or no media.read permission
},
},
},
});
Access requires auth:
# Fails without auth
curl http://localhost:7654/api/media/file/private.pdf
# 401 Unauthorized
# Works with auth
curl -H "Authorization: Bearer $TOKEN" \
http://localhost:7654/api/media/file/private.pdf
For S3/R2, generate presigned URLs:
// Custom endpoint for signed URLs (advanced)
// Requires S3 SDK directly, not through Bknd adapter
import { S3Client, GetObjectCommand } from "@aws-sdk/client-s3";
import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
async function getSignedDownloadUrl(filename: string): Promise<string> {
const client = new S3Client({ /* config */ });
const command = new GetObjectCommand({
Bucket: process.env.S3_BUCKET,
Key: filename,
});
return getSignedUrl(client, command, { expiresIn: 3600 }); // 1 hour
}
| Method | Endpoint | Description |
|--------|----------|-------------|
| GET | /api/media/file/:filename | Download/view file |
| GET | /api/media/files | List all files |
| Header | Description |
|--------|-------------|
| Authorization | Bearer token (if auth required) |
| If-None-Match | ETag for conditional request |
| Range | Byte range for partial download |
| Header | Description |
|--------|-------------|
| Content-Type | File MIME type |
| Content-Length | File size in bytes |
| ETag | File hash for caching |
| Accept-Ranges | Indicates range support |
Problem: File URL returns 404.
Causes:
Fix: Verify file exists:
const { data: files } = await api.media.listFiles();
const exists = files.some(f => f.key === filename);
Problem: Browser blocks direct S3 access.
Fix: Configure CORS on S3 bucket:
{
"CORSRules": [{
"AllowedOrigins": ["https://yourapp.com"],
"AllowedMethods": ["GET"],
"AllowedHeaders": ["*"],
"MaxAgeSeconds": 3600
}]
}
Problem: Files load slowly via API proxy.
Fix: Use direct storage URLs with CDN:
// Instead of API proxy
const slow = "/api/media/file/image.png";
// Use direct CDN URL
const fast = "https://cdn.yourapp.com/image.png";
Problem: HTTPS page loading HTTP file URLs.
Fix: Ensure storage URL uses HTTPS:
// WRONG
url: "http://bucket.s3.amazonaws.com",
// CORRECT
url: "https://bucket.s3.amazonaws.com",
Problem: Download times out or memory error.
Fix: Use streaming for large files:
// Stream instead of loading into memory
const stream = await api.media.getFileStream("large-file.zip");
// Or direct download link
const downloadUrl = `${api.host}/api/media/file/large-file.zip`;
window.location.href = downloadUrl;
Problem: Cloudinary returns 404 for uploaded file.
Cause: Cloudinary uses eventual consistency; file not yet indexed.
Fix: Wait briefly or use upload response URL directly:
const { data } = await api.media.upload(file);
// Use data.name immediately rather than re-fetching
Test file serving setup:
async function testFileServing() {
const filename = "test-image.png";
// 1. Verify file exists
const { data: files } = await api.media.listFiles();
const file = files.find(f => f.key === filename);
console.log("File exists:", !!file);
// 2. Test API proxy
const apiUrl = `${api.host}/api/media/file/${filename}`;
const apiRes = await fetch(apiUrl);
console.log("API proxy status:", apiRes.status);
console.log("Content-Type:", apiRes.headers.get("content-type"));
// 3. Test conditional request
const etag = apiRes.headers.get("etag");
if (etag) {
const conditionalRes = await fetch(apiUrl, {
headers: { "If-None-Match": etag },
});
console.log("Conditional request:", conditionalRes.status === 304 ? "304 (cached)" : conditionalRes.status);
}
// 4. Test SDK download
const downloadedFile = await api.media.download(filename);
console.log("SDK download:", downloadedFile.name, downloadedFile.size);
}
DO:
DON'T:
development
Use btca (Better Context App) to efficiently query and learn from the bknd backend framework. Use when working with bknd for (1) Understanding data module and schema definitions, (2) Implementing authentication and authorization, (3) Setting up media file handling, (4) Configuring adapters (Node, Cloudflare, etc.), (5) Learning from bknd source code and examples, (6) Debugging bknd-specific issues
development
Use when configuring webhook integrations in Bknd. Covers receiving incoming webhooks via HTTP triggers, sending outgoing webhooks with FetchTask, event-triggered webhooks on data changes, signature verification, retry patterns, and async processing.
development
Use when encountering Bknd errors, getting error messages, something not working, or needing quick fixes. Covers error code reference, quick solutions, and common mistake patterns.
tools
Use when writing tests for Bknd applications, setting up test infrastructure, creating unit/integration tests, or testing API endpoints. Covers in-memory database setup, test helpers, mocking, and test patterns.