bknd-storage-config

📁 cameronapak/bknd-skills 📅 Jan 21, 2026
0
总安装量
6
周安装量
安装命令
npx skills add https://github.com/cameronapak/bknd-skills --skill bknd-storage-config

Agent 安装分布

gemini-cli 4
codex 4
cursor 3
antigravity 3
junie 3
windsurf 3

Skill 文档

Storage Configuration

Configure storage backends for Bknd’s media module.

Prerequisites

  • bknd package installed
  • For S3: bucket created with appropriate permissions
  • For Cloudinary: account with API credentials
  • For R2: Cloudflare Workers environment with R2 binding

When to Use UI Mode

  • Admin panel doesn’t support storage configuration
  • Storage must be configured in code

When to Use Code Mode

  • All storage configuration (required)
  • Setting up different adapters per environment
  • Production storage with credentials

Storage Adapter Overview

Adapter Type Use Case
s3 S3-compatible AWS S3, Cloudflare R2 (external), DigitalOcean Spaces, MinIO
cloudinary Media-optimized Image/video transformations, CDN delivery
local Filesystem Development only (Node.js/Bun runtime)
r2 Cloudflare R2 Cloudflare Workers with R2 binding

Step-by-Step: S3 Adapter

Step 1: Create S3 Bucket

Create bucket in AWS console or via CLI:

aws s3 mb s3://my-app-uploads --region us-east-1

Step 2: Configure CORS (if browser uploads)

{
  "CORSRules": [{
    "AllowedOrigins": ["https://yourapp.com"],
    "AllowedMethods": ["GET", "PUT", "POST", "DELETE"],
    "AllowedHeaders": ["*"],
    "ExposeHeaders": ["ETag"]
  }]
}

Step 3: Get Access Credentials

Create IAM user with S3 access and get:

  • Access Key ID
  • Secret Access Key

Step 4: Configure Bknd

import { defineConfig } from "bknd";

export default defineConfig({
  media: {
    enabled: true,
    adapter: {
      type: "s3",
      config: {
        access_key: process.env.S3_ACCESS_KEY,
        secret_access_key: process.env.S3_SECRET_KEY,
        url: "https://my-bucket.s3.us-east-1.amazonaws.com",
      },
    },
  },
});

Step 5: Add Environment Variables

# .env
S3_ACCESS_KEY=AKIA...
S3_SECRET_KEY=wJalr...

S3 URL Formats

Different S3-compatible services use different URL formats:

// AWS S3
url: "https://{bucket}.s3.{region}.amazonaws.com"
// Example: "https://my-bucket.s3.us-east-1.amazonaws.com"

// Cloudflare R2 (external access via S3 API)
url: "https://{account_id}.r2.cloudflarestorage.com/{bucket}"
// Example: "https://abc123.r2.cloudflarestorage.com/my-bucket"

// DigitalOcean Spaces
url: "https://{bucket}.{region}.digitaloceanspaces.com"
// Example: "https://my-bucket.nyc3.digitaloceanspaces.com"

// MinIO (self-hosted)
url: "http://localhost:9000/{bucket}"

Step-by-Step: Cloudinary Adapter

Step 1: Get Cloudinary Credentials

From Cloudinary dashboard, copy:

  • Cloud name
  • API Key
  • API Secret

Step 2: Configure Bknd

import { defineConfig } from "bknd";

export default defineConfig({
  media: {
    enabled: true,
    adapter: {
      type: "cloudinary",
      config: {
        cloud_name: process.env.CLOUDINARY_CLOUD_NAME,
        api_key: process.env.CLOUDINARY_API_KEY,
        api_secret: process.env.CLOUDINARY_API_SECRET,
      },
    },
  },
});

Step 3: Add Environment Variables

# .env
CLOUDINARY_CLOUD_NAME=my-cloud
CLOUDINARY_API_KEY=123456789
CLOUDINARY_API_SECRET=abcdef...

Optional: Upload Preset

For unsigned uploads or custom transformations:

adapter: {
  type: "cloudinary",
  config: {
    cloud_name: process.env.CLOUDINARY_CLOUD_NAME,
    api_key: process.env.CLOUDINARY_API_KEY,
    api_secret: process.env.CLOUDINARY_API_SECRET,
    upload_preset: "my-preset",  // Optional
  },
},

Step-by-Step: Local Adapter (Development)

Step 1: Create Upload Directory

mkdir -p ./uploads

Step 2: Register and Configure

import { defineConfig } from "bknd";
import { registerLocalMediaAdapter } from "bknd/adapter/node";

// Register the local adapter
const local = registerLocalMediaAdapter();

export default defineConfig({
  media: {
    enabled: true,
    adapter: local({ path: "./uploads" }),
  },
});

Files served at /api/media/file/{filename}.

Note on Runtime

Local adapter requires Node.js or Bun runtime (filesystem access). It won’t work in:

  • Cloudflare Workers
  • Vercel Edge Functions
  • Browser environments

Step-by-Step: Cloudflare R2 (Workers)

Step 1: Create R2 Bucket

wrangler r2 bucket create my-bucket

Step 2: Add R2 Binding to wrangler.toml

[[r2_buckets]]
binding = "MY_BUCKET"
bucket_name = "my-bucket"

Step 3: Configure Bknd

import { serve, type CloudflareBkndConfig } from "bknd/adapter/cloudflare";

const config: CloudflareBkndConfig = {
  app: (env) => ({
    connection: { url: env.DB },
    config: {
      media: {
        enabled: true,
        adapter: {
          type: "r2",
          config: {
            binding: "MY_BUCKET",
          },
        },
      },
    },
  }),
};

export default serve(config);

R2 adapter uses the Cloudflare Workers binding directly, no external credentials needed.

Media Module Options

Size Limit

export default defineConfig({
  media: {
    enabled: true,
    body_max_size: 10 * 1024 * 1024,  // 10MB max upload
    adapter: { ... },
  },
});

Default Size Behavior

If body_max_size not set, uploads have no size limit. Always set a reasonable limit in production.

Environment-Based Configuration

Different adapters for dev vs production:

import { defineConfig } from "bknd";
import { registerLocalMediaAdapter } from "bknd/adapter/node";

const local = registerLocalMediaAdapter();
const isDev = process.env.NODE_ENV !== "production";

export default defineConfig({
  media: {
    enabled: true,
    body_max_size: 25 * 1024 * 1024,  // 25MB
    adapter: isDev
      ? local({ path: "./uploads" })
      : {
          type: "s3",
          config: {
            access_key: process.env.S3_ACCESS_KEY,
            secret_access_key: process.env.S3_SECRET_KEY,
            url: process.env.S3_BUCKET_URL,
          },
        },
  },
});

Verify Storage Configuration

Check Media Module Enabled

import { Api } from "bknd";

const api = new Api({ host: "http://localhost:7654" });

// List files (empty if no uploads yet)
const { ok, data, error } = await api.media.listFiles();

if (ok) {
  console.log("Media module working, files:", data.length);
} else {
  console.error("Media error:", error);
}

Test Upload

async function testStorage() {
  const testFile = new File(["test content"], "test.txt", {
    type: "text/plain"
  });

  const { ok, data, error } = await api.media.upload(testFile);

  if (ok) {
    console.log("Upload succeeded:", data.name);

    // Clean up
    await api.media.deleteFile(data.name);
    console.log("Cleanup complete");
  } else {
    console.error("Upload failed:", error);
  }
}

Check via REST

# List files
curl http://localhost:7654/api/media/files

# Upload test file
echo "test" | curl -X POST \
  -H "Content-Type: text/plain" \
  --data-binary @- \
  http://localhost:7654/api/media/upload/test.txt

Complete Configuration Examples

AWS S3 Production

import { defineConfig } from "bknd";

export default defineConfig({
  connection: {
    url: process.env.DATABASE_URL,
  },
  config: {
    media: {
      enabled: true,
      body_max_size: 50 * 1024 * 1024,  // 50MB
      adapter: {
        type: "s3",
        config: {
          access_key: process.env.AWS_ACCESS_KEY_ID,
          secret_access_key: process.env.AWS_SECRET_ACCESS_KEY,
          url: `https://${process.env.S3_BUCKET}.s3.${process.env.AWS_REGION}.amazonaws.com`,
        },
      },
    },
  },
});

Cloudflare R2 + D1

import { serve, type CloudflareBkndConfig } from "bknd/adapter/cloudflare";

const config: CloudflareBkndConfig = {
  app: (env) => ({
    connection: { url: env.DB },  // D1 binding
    config: {
      media: {
        enabled: true,
        body_max_size: 25 * 1024 * 1024,
        adapter: {
          type: "r2",
          config: { binding: "UPLOADS" },
        },
      },
    },
  }),
};

export default serve(config);

Development with Hot Reload

import { defineConfig } from "bknd";
import { registerLocalMediaAdapter } from "bknd/adapter/node";

const local = registerLocalMediaAdapter();

export default defineConfig({
  connection: {
    url: "file:data.db",
  },
  config: {
    media: {
      enabled: true,
      adapter: local({ path: "./public/uploads" }),
    },
  },
});

Common Pitfalls

S3 403 Forbidden

Problem: Upload fails with 403 error.

Causes:

  1. Invalid credentials
  2. Bucket policy blocks access
  3. URL format incorrect

Fix:

// Check URL format - must NOT have trailing slash
url: "https://bucket.s3.region.amazonaws.com"  // CORRECT
url: "https://bucket.s3.region.amazonaws.com/" // WRONG

// Verify credentials
console.log("Key:", process.env.S3_ACCESS_KEY?.substring(0, 8) + "...");

Local Adapter 404

Problem: Files not found after upload.

Causes:

  1. Upload directory doesn’t exist
  2. Wrong path (absolute vs relative)

Fix:

# Create directory first
mkdir -p ./uploads

# Use relative path from project root
adapter: local({ path: "./uploads" })  // CORRECT
adapter: local({ path: "/uploads" })   // WRONG (absolute)

R2 Binding Not Found

Problem: “No R2Bucket found with key” error.

Fix: Ensure wrangler.toml has correct binding:

[[r2_buckets]]
binding = "MY_BUCKET"      # This name goes in config
bucket_name = "actual-bucket"

Cloudinary Upload Fails Silently

Problem: putObject returns undefined.

Causes:

  1. Invalid credentials
  2. API key doesn’t have upload permission

Fix:

  • Verify credentials in Cloudinary dashboard
  • Ensure API key has write access (not read-only)

Wrong Adapter for Runtime

Problem: Local adapter fails in serverless.

Fix: Use S3/R2/Cloudinary for serverless:

// Cloudflare Workers - use r2
// Vercel - use s3
// AWS Lambda - use s3
// Node.js server - local is OK for dev

Missing Environment Variables

Problem: Credentials undefined at runtime.

Fix:

// Add validation
if (!process.env.S3_ACCESS_KEY) {
  throw new Error("S3_ACCESS_KEY not set");
}

// Or provide defaults for dev
const config = {
  access_key: process.env.S3_ACCESS_KEY ?? "dev-key",
  // ...
};

CORS Not Configured

Problem: Browser uploads blocked.

Fix: Configure CORS on the bucket itself (AWS/R2 console), not in Bknd.

Security Checklist

  1. Never commit credentials – Use environment variables
  2. Restrict bucket access – Don’t make bucket public unless needed
  3. Set size limits – Always configure body_max_size
  4. Use HTTPS – Especially for credential-based uploads
  5. Rotate keys – Periodically rotate access keys
  6. Principle of least privilege – IAM user needs only S3 access to one bucket

DOs and DON’Ts

DO:

  • Use environment variables for credentials
  • Set body_max_size in production
  • Use S3/R2/Cloudinary for production
  • Configure CORS for browser uploads
  • Test storage before deploying

DON’T:

  • Commit credentials to git
  • Use local adapter in production
  • Leave upload size unlimited
  • Forget to create upload directory for local
  • Use trailing slash in S3 URL

Related Skills

  • bknd-file-upload – Upload files after storage configured
  • bknd-serve-files – Serve and deliver uploaded files
  • bknd-env-config – Manage environment variables
  • bknd-deploy-hosting – Deploy with production storage
  • bknd-assign-permissions – Set media permissions