or object storage with strong consistency. The schema defines the expected structure.
2. Idempotent Transformation: Pipeline steps must be idempotent. Given the same source and configuration, the output must be byte-identical. This enables caching and rollback.
3. Metadata Coupling: Metadata is embedded or side-carred with strong linking. Transformations preserve and enrich metadata; they never discard it.
4. Matrix Generation: Variants are generated based on a configuration matrix, not individual manual exports. The matrix defines dimensions (e.g., widths: [320, 640, 1280], formats: ['webp', 'avif']).
Step-by-Step Implementation
1. Define Asset Schema
Create a TypeScript interface to enforce structure. This schema acts as the contract between creative input and engineering consumption.
export interface AssetSchema {
id: string;
source: {
path: string;
hash: string; // Content hash for deduplication
mimeType: string;
};
metadata: {
title: string;
altText: string;
locale: string;
tags: string[];
custom: Record<string, unknown>;
};
matrix: {
dimensions: Dimension[];
formats: Format[];
optimizations: Optimization[];
};
}
export interface Dimension {
width: number;
height?: number;
aspectRatio?: number;
cropStrategy?: 'contain' | 'cover' | 'fill';
}
export interface Format {
type: 'image/jpeg' | 'image/webp' | 'image/avif' | 'model/gltf-binary';
quality?: number;
}
export interface Optimization {
type: 'resize' | 'compress' | 'color-profile' | 'drm-encrypt';
params: Record<string, unknown>;
}
2. Pipeline Orchestrator
Implement the pipeline in TypeScript. This orchestrator reads the source, applies the matrix transformations, validates outputs, and emits artifacts.
import { createHash } from 'crypto';
import sharp from 'sharp';
import { AssetSchema, Format, Dimension } from './schema';
interface PipelineResult {
assetId: string;
variants: Variant[];
metadata: Record<string, unknown>;
status: 'success' | 'failed';
}
interface Variant {
path: string;
format: string;
dimensions: { width: number; height: number };
sizeBytes: number;
hash: string;
}
export class AssetMatrixPipeline {
private cache: Map<string, Buffer>;
constructor() {
this.cache = new Map();
}
async process(schema: AssetSchema): Promise<PipelineResult> {
const sourceBuffer = await this.readSource(schema.source.path);
const sourceHash = createHash('sha256').update(sourceBuffer).digest('hex');
if (sourceHash !== schema.source.hash) {
throw new Error(`Source hash mismatch for ${schema.id}. Asset tampered or outdated.`);
}
const variants: Variant[] = [];
// Generate matrix: Cross-product of dimensions and formats
for (const dim of schema.matrix.dimensions) {
for (const fmt of schema.matrix.formats) {
const variantKey = `${schema.id}_${dim.width}x${dim.height || 'auto'}_${fmt.type}`;
// Idempotency check via cache or artifact store
if (!this.cache.has(variantKey)) {
const buffer = await this.transform(sourceBuffer, dim, fmt, schema.matrix.optimizations);
this.cache.set(variantKey, buffer);
}
const buffer = this.cache.get(variantKey)!;
const variantHash = createHash('sha256').update(buffer).digest('hex');
variants.push({
path: `variants/${variantKey}.${this.getExtension(fmt.type)}`,
format: fmt.type,
dimensions: { width: dim.width, height: dim.height || 0 },
sizeBytes: buffer.length,
hash: variantHash,
});
}
}
return {
assetId: schema.id,
variants,
metadata: schema.metadata,
status: 'success',
};
}
private async transform(
source: Buffer,
dim: Dimension,
fmt: Format,
optimizations: AssetSchema['matrix']['optimizations']
): Promise<Buffer> {
let pipeline = sharp(source);
// Apply dimensions
if (dim.cropStrategy === 'cover') {
pipeline = pipeline.resize(dim.width, dim.height, { fit: 'cover' });
} else {
pipeline = pipeline.resize(dim.width, dim.height, { fit: 'contain' });
}
// Apply format conversion
switch (fmt.type) {
case 'image/webp':
pipeline = pipeline.webp({ quality: fmt.quality || 80 });
break;
case 'image/avif':
pipeline = pipeline.avif({ quality: fmt.quality || 70 });
break;
case 'image/jpeg':
pipeline = pipeline.jpeg({ quality: fmt.quality || 85 });
break;
}
// Apply additional optimizations
for (const opt of optimizations) {
if (opt.type === 'color-profile') {
pipeline = pipeline.withMetadata({
icc: opt.params.profile as string,
});
}
}
return pipeline.toBuffer();
}
private readSource(path: string): Promise<Buffer> {
// Implementation for reading from FS, S3, or Git LFS
return Promise.resolve(Buffer.from(''));
}
private getExtension(mimeType: string): string {
const map: Record<string, string> = {
'image/jpeg': 'jpg',
'image/webp': 'webp',
'image/avif': 'avif',
'model/gltf-binary': 'glb',
};
return map[mimeType] || 'bin';
}
}
3. Validation and CI Integration
The pipeline must be integrated into CI/CD. A validation step runs post-processing to ensure all matrix variants were generated and metadata is intact.
export async function validatePipeline(result: PipelineResult): Promise<boolean> {
const requiredFormats = ['image/webp', 'image/avif'];
const requiredWidths = [320, 640, 1280];
const hasAllFormats = requiredFormats.every(fmt =>
result.variants.some(v => v.format === fmt)
);
const hasAllWidths = requiredWidths.every(w =>
result.variants.some(v => v.dimensions.width === w)
);
if (!hasAllFormats || !hasAllWidths) {
console.error(`Matrix gap detected for ${result.assetId}.`);
return false;
}
// Validate metadata completeness
if (!result.metadata.altText) {
console.error(`Accessibility metadata missing for ${result.assetId}.`);
return false;
}
return true;
}
Pitfall Guide
1. Storing Binaries in Git Without LFS
Mistake: Committing large asset files directly to Git repositories.
Impact: Repository bloat, slow clone times, history corruption, and CI/CD failures.
Best Practice: Always use Git LFS or an external object store (S3/GCS) with a manifest file in Git. The manifest tracks hashes and metadata, keeping the repo lightweight.
Mistake: Treating metadata as optional or unstructured JSON blobs.
Impact: Runtime errors, broken localization, accessibility violations, and inability to query assets programmatically.
Best Practice: Enforce strict schemas using JSON Schema or TypeScript interfaces. Validate metadata at ingestion time. Missing required fields should block the build.
3. Hardcoded Variant Configurations
Mistake: Embedding dimensions and formats in code or scripts rather than configuration.
Impact: Inflexibility. Adding a new format requires code changes and redeployment.
Best Practice: Externalize the matrix configuration. Use a declarative config file (YAML/JSON) that defines the matrix dimensions. The pipeline reads this config and generates variants dynamically.
4. Lack of Idempotency and Caching
Mistake: Re-processing assets on every build regardless of changes.
Impact: Excessive build times, high compute costs, and environmental waste.
Best Practice: Implement content-hash-based caching. If the source hash matches a cached result, skip transformation. Use artifact caching in CI to persist processed variants across runs.
5. Decoupling Asset Versioning from Code
Mistake: Updating assets independently of the application code that consumes them.
Impact: Version drift. The app may request an asset variant that doesn't exist, or display outdated content.
Best Practice: Tie asset deployments to release tags. Use atomic deployments where assets and code are updated together. Implement fallback mechanisms for missing variants, but treat them as errors in staging.
6. Over-Optimizing at Build Time
Mistake: Generating every possible variant at build time, including low-probability edge cases.
Impact: Massive storage usage and long build times.
Best Practice: Use a hybrid approach. Generate high-confidence variants at build time. Use on-the-fly transformation for rare variants or user-specific requests, leveraging edge functions or CDN image processing.
7. Neglecting DRM and Security
Mistake: Processing assets without considering intellectual property protection.
Impact: Asset theft, unauthorized reuse, and compliance violations.
Best Practice: Integrate DRM encryption or watermarking into the pipeline for sensitive assets. Ensure access controls are enforced at the storage layer and via signed URLs.
Production Bundle
Action Checklist
Decision Matrix
| Scenario | Recommended Approach | Why | Cost Impact |
|---|
| Static Site / Low Variants | Build-time generation | Simplicity; all variants ready at deploy. | Low storage, Low compute. |
| Dynamic App / High Variants | Hybrid (Build + Edge) | Balance speed and flexibility; edge handles rare cases. | Moderate storage, Higher edge compute. |
| Strict Compliance / DRM | Pre-encrypted Pipeline | Security cannot be delegated to edge; must be baked in. | Higher compute, Lower risk. |
| Rapid Prototyping | Manual with Schema Validation | Speed of iteration; schema prevents drift. | Low infra cost, Higher manual effort. |
| Enterprise Scale | Full Matrix Automation | Necessity for consistency across teams and platforms. | High initial investment, Low marginal cost. |
Configuration Template
asset-pipeline.config.json
{
"schemaVersion": "1.0.0",
"source": {
"provider": "git-lfs",
"manifestPath": "assets/manifest.json"
},
"matrix": {
"dimensions": [
{ "width": 320, "height": 240, "cropStrategy": "cover" },
{ "width": 640, "height": 480, "cropStrategy": "cover" },
{ "width": 1280, "height": 960, "cropStrategy": "contain" }
],
"formats": [
{ "type": "image/webp", "quality": 80 },
{ "type": "image/avif", "quality": 70 },
{ "type": "image/jpeg", "quality": 85 }
],
"optimizations": [
{ "type": "color-profile", "params": { "profile": "srgb" } },
{ "type": "compress", "params": { "level": 9 } }
]
},
"metadata": {
"requiredFields": ["title", "altText", "locale"],
"allowedLocales": ["en-US", "es-ES", "fr-FR"]
},
"output": {
"destination": "s3://my-bucket/assets/",
"cacheControl": "public, max-age=31536000, immutable"
}
}
Quick Start Guide
-
Initialize Project:
npm init -y
npm install sharp @types/sharp typescript ts-node
npx tsc --init
-
Create Config:
Copy the asset-pipeline.config.json template to your project root. Adjust dimensions and formats to match your requirements.
-
Implement Pipeline:
Use the TypeScript code provided in the Core Solution to create pipeline.ts. Ensure you implement the readSource method to point to your asset directory.
-
Run Transformation:
ts-node pipeline.ts --config asset-pipeline.config.json --input ./src/assets/logo.png
-
Verify Output:
Check the output directory for generated variants. Validate that metadata is preserved and all matrix combinations exist. Run the validation function to confirm compliance.