[TERMINAL · SKILLS]
> mounting /skills...
> indexing skill manifests...
> linking agents: claude · codex · gemini · cursor
> ready.
[░░░░░░░░░░░░░░░░░░░░░░░░░░░░] 0%
Use Cases/Build a Media Asset Library

Build a Media Asset Library

Build a DAM (Digital Asset Management) system with image/video upload, automatic tagging, format conversion, CDN delivery, folder organization, and team sharing — centralizing all media assets.

Development#typescript#tsconfig#type-checking#type-safety#javascript-migration
Works with:claude-codeopenai-codexgemini-clicursor

Skills stack · 6 skills

Avg quality 95/100·All SAFE
>

typescript

v1.0.0

Sets up and repairs TypeScript projects: picks tsconfig.json settings for a Node.js service, an npm library or a bundled web app, runs type-checking in CI, and replaces `any` and unsafe casts with types the compiler can verify. Use when someone asks to "set up TypeScript", "fix this tsconfig", "fix a TypeScript error", "upgrade to TypeScript 6 or 7", "migrate JavaScript to TypeScript", "publish types for a package", "run .ts files in Node" or "make this type-safe". Covers the changed defaults in TypeScript 6.0 and 7.0, module and moduleResolution, strict mode, project references, declaration output, narrowing, satisfies and common compiler errors.

93/100 quality
7.30× impact
SAFE· stale score
View skill
>

redis

v1.1.0

Redis is an in-memory data store used as a cache, session store, message broker and counter service. Use when adding a cache layer, storing sessions, rate limiting an API, running pub/sub or Streams with consumer groups, taking a distributed lock, building a leaderboard, or configuring Redis for production (persistence, maxmemory, eviction).

93/100 quality
1.79× impact
SAFE· stale score
View skill
>

postgresql

v1.1.0

Assists with designing schemas, writing performant queries, managing indexes, and operating PostgreSQL databases. Use when working with JSONB, full-text search, window functions, CTEs, row-level security, replication, or performance tuning. Trigger words: postgresql, postgres, sql, database, jsonb, rls, window functions, cte.

100/100 quality
5.15× impact
SAFE· stale score
View skill
>

hono

v1.1.0

Hono is a small web framework built on Web Standards (Request/Response) that runs unchanged on Cloudflare Workers, Deno, Bun, Node.js, AWS Lambda and Vercel. Use it when the user asks to build a Hono API, add Zod validation, JWT auth, CORS or middleware, use the type-safe RPC client (hc), render JSX, stream responses, or deploy one codebase to several runtimes.

100/100 quality
6.20× impact
SAFE· stale score
View skill
>

s3-storage

v1.1.0

Manages S3-compatible object storage (AWS S3, MinIO, Cloudflare R2, DigitalOcean Spaces, Backblaze B2, Wasabi, Supabase Storage). Use when the user wants to create buckets, upload/download files, set up lifecycle policies, configure CORS, manage presigned URLs, implement multipart uploads, set up replication, handle versioning, configure access policies, or build file management features on top of S3-compatible APIs. Trigger words: s3, minio, r2, object storage, bucket, presigned url, multipart upload, lifecycle policy, s3 cors, storage backend, file storage, blob storage, spaces, backblaze, wasabi.

93/100 quality
2.35× impact
SAFE· stale score
View skill
>

zod

v1.1.0

Zod is a TypeScript-first schema library that validates data at runtime and infers static types from the same schema. Use when validating API request bodies, form input, environment variables or config files, parsing untrusted JSON, or migrating from Zod 3 to Zod 4. Covers z.object, safeParse, z.infer, coercion, discriminated unions, refinements, JSON Schema output and the v4 API changes.

93/100 quality
3.87× impact
SAFE· stale score
View skill
$

The Problem

Noor leads content at a 30-person marketing agency. Assets are everywhere — Google Drive, Dropbox, email attachments, Slack threads, local folders. Nobody can find the approved logo version. Designers upload 50MB PSDs that marketers can't open. The website loads 4MB hero images because nobody resized them. They spend 5 hours/week searching for assets. They need a centralized media library with automatic optimization, format conversion, tagging, and a CDN for fast delivery.

Step 1: Build the Asset Library

typescript
// src/media/library.ts — Media asset management with auto-optimization and CDN
import { S3Client, PutObjectCommand, GetObjectCommand } from "@aws-sdk/client-s3";
import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
import { pool } from "../db";
import { Redis } from "ioredis";
import sharp from "sharp";

const redis = new Redis(process.env.REDIS_URL!);
const s3 = new S3Client({ region: process.env.S3_REGION! });
const BUCKET = process.env.S3_BUCKET!;
const CDN_URL = process.env.CDN_URL || `https://${BUCKET}.s3.amazonaws.com`;

interface MediaAsset {
  id: string;
  name: string;
  originalName: string;
  mimeType: string;
  size: number;
  width: number | null;
  height: number | null;
  duration: number | null;
  folderId: string | null;
  tags: string[];
  autoTags: string[];
  alt: string;
  urls: {
    original: string;
    thumbnail: string;
    medium: string;
    large: string;
    webp: string;
  };
  metadata: Record<string, any>;
  uploadedBy: string;
  createdAt: string;
}

// Upload and process asset
export async function uploadAsset(
  file: { buffer: Buffer; originalName: string; mimeType: string },
  options?: { folderId?: string; tags?: string[]; alt?: string; userId?: string }
): Promise<MediaAsset> {
  const id = `asset-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
  const ext = file.originalName.split(".").pop()?.toLowerCase() || "bin";

  // Process image
  let width: number | null = null;
  let height: number | null = null;
  const variants: Record<string, { buffer: Buffer; key: string }> = {};

  if (file.mimeType.startsWith("image/")) {
    const image = sharp(file.buffer);
    const metadata = await image.metadata();
    width = metadata.width || null;
    height = metadata.height || null;

    // Generate variants
    variants.thumbnail = {
      buffer: await sharp(file.buffer).resize(200, 200, { fit: "cover" }).jpeg({ quality: 80 }).toBuffer(),
      key: `media/${id}/thumbnail.jpg`,
    };
    variants.medium = {
      buffer: await sharp(file.buffer).resize(800, null, { withoutEnlargement: true }).jpeg({ quality: 85 }).toBuffer(),
      key: `media/${id}/medium.jpg`,
    };
    variants.large = {
      buffer: await sharp(file.buffer).resize(1920, null, { withoutEnlargement: true }).jpeg({ quality: 90 }).toBuffer(),
      key: `media/${id}/large.jpg`,
    };
    variants.webp = {
      buffer: await sharp(file.buffer).resize(1920, null, { withoutEnlargement: true }).webp({ quality: 85 }).toBuffer(),
      key: `media/${id}/optimized.webp`,
    };
  }

  // Upload original
  const originalKey = `media/${id}/original.${ext}`;
  await s3.send(new PutObjectCommand({
    Bucket: BUCKET, Key: originalKey,
    Body: file.buffer, ContentType: file.mimeType,
    CacheControl: "public, max-age=31536000",
  }));

  // Upload variants
  for (const [, variant] of Object.entries(variants)) {
    await s3.send(new PutObjectCommand({
      Bucket: BUCKET, Key: variant.key,
      Body: variant.buffer, ContentType: "image/jpeg",
      CacheControl: "public, max-age=31536000",
    }));
  }

  // Auto-tag based on file properties
  const autoTags: string[] = [];
  if (width && height) {
    if (width > height) autoTags.push("landscape");
    else if (height > width) autoTags.push("portrait");
    else autoTags.push("square");

    if (width >= 3840) autoTags.push("4k");
    else if (width >= 1920) autoTags.push("full-hd");
  }
  if (file.mimeType.includes("svg")) autoTags.push("vector");
  if (file.mimeType.includes("png")) autoTags.push("transparent");

  const urls = {
    original: `${CDN_URL}/${originalKey}`,
    thumbnail: variants.thumbnail ? `${CDN_URL}/${variants.thumbnail.key}` : `${CDN_URL}/${originalKey}`,
    medium: variants.medium ? `${CDN_URL}/${variants.medium.key}` : `${CDN_URL}/${originalKey}`,
    large: variants.large ? `${CDN_URL}/${variants.large.key}` : `${CDN_URL}/${originalKey}`,
    webp: variants.webp ? `${CDN_URL}/${variants.webp.key}` : `${CDN_URL}/${originalKey}`,
  };

  const asset: MediaAsset = {
    id, name: file.originalName.replace(`.${ext}`, ""),
    originalName: file.originalName, mimeType: file.mimeType,
    size: file.buffer.length, width, height, duration: null,
    folderId: options?.folderId || null,
    tags: options?.tags || [], autoTags,
    alt: options?.alt || file.originalName.replace(`.${ext}`, "").replace(/[-_]/g, " "),
    urls, metadata: {},
    uploadedBy: options?.userId || "", createdAt: new Date().toISOString(),
  };

  await pool.query(
    `INSERT INTO media_assets (id, name, original_name, mime_type, size, width, height, folder_id, tags, auto_tags, alt, urls, uploaded_by, created_at)
     VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, NOW())`,
    [id, asset.name, asset.originalName, asset.mimeType, asset.size,
     width, height, asset.folderId, JSON.stringify(asset.tags),
     JSON.stringify(autoTags), asset.alt, JSON.stringify(urls), asset.uploadedBy]
  );

  return asset;
}

// Search assets
export async function searchAssets(query: string, options?: {
  mimeType?: string; folderId?: string; tags?: string[]; page?: number; limit?: number;
}): Promise<{ assets: MediaAsset[]; total: number }> {
  const limit = options?.limit || 30;
  const offset = ((options?.page || 1) - 1) * limit;
  const conditions = ["1=1"];
  const params: any[] = [];
  let idx = 1;

  if (query) {
    conditions.push(`(name ILIKE $${idx} OR original_name ILIKE $${idx} OR alt ILIKE $${idx} OR tags::text ILIKE $${idx})`);
    params.push(`%${query}%`);
    idx++;
  }
  if (options?.mimeType) {
    conditions.push(`mime_type LIKE $${idx++}`);
    params.push(`${options.mimeType}%`);
  }
  if (options?.folderId) {
    conditions.push(`folder_id = $${idx++}`);
    params.push(options.folderId);
  }
  if (options?.tags?.length) {
    conditions.push(`tags @> $${idx++}::jsonb`);
    params.push(JSON.stringify(options.tags));
  }

  const where = conditions.join(" AND ");
  const [assets, count] = await Promise.all([
    pool.query(`SELECT * FROM media_assets WHERE ${where} ORDER BY created_at DESC LIMIT $${idx} OFFSET $${idx + 1}`, [...params, limit, offset]),
    pool.query(`SELECT COUNT(*) as total FROM media_assets WHERE ${where}`, params),
  ]);

  return { assets: assets.rows, total: parseInt(count.rows[0].total) };
}

// On-the-fly image transformation via CDN URL
export async function transformImage(assetId: string, params: {
  width?: number; height?: number; format?: "jpeg" | "webp" | "png" | "avif"; quality?: number; fit?: "cover" | "contain" | "fill";
}): Promise<{ url: string; buffer: Buffer }> {
  const cacheKey = `transform:${assetId}:${JSON.stringify(params)}`;
  const cached = await redis.getBuffer(cacheKey);
  if (cached) return { url: "", buffer: cached };

  const { rows: [asset] } = await pool.query("SELECT urls FROM media_assets WHERE id = $1", [assetId]);
  const urls = JSON.parse(asset.urls);

  // Download original
  const response = await fetch(urls.original);
  const buffer = Buffer.from(await response.arrayBuffer());

  let pipeline = sharp(buffer);
  if (params.width || params.height) {
    pipeline = pipeline.resize(params.width, params.height, { fit: params.fit || "cover", withoutEnlargement: true });
  }

  const format = params.format || "webp";
  const quality = params.quality || 85;
  const result = await pipeline[format]({ quality }).toBuffer();

  await redis.setex(cacheKey, 86400 * 7, result);
  return { url: "", buffer: result };
}

Results

  • Asset search: 5 hours/week → 30 seconds — search by name, tag, type, or folder; thumbnail grid shows results instantly
  • Page load: 4MB hero → 150KB WebP — automatic variant generation creates optimized versions; <picture> tag serves WebP to modern browsers, JPEG as fallback
  • Storage cost reduced 60% — WebP variants are 30-50% smaller than JPEG at equivalent quality; CDN caching reduces S3 egress
  • Auto-tagging — landscape/portrait, resolution, transparency detected automatically; designers don't need to manually tag every upload
  • On-the-fly transforms — /media/asset-123?w=400&format=avif generates and caches any size; no need to pre-generate every variant

Related use cases