X Enterprises

fastify-ximagepipeline

Async image upload pipeline for Fastify — EXIF stripping, content moderation, WebP variant generation, blurhash placeholders, and R2/S3 storage with a background job queue.

fastify-ximagepipeline

Fastify plugin for async image upload processing — EXIF stripping, optional content moderation, WebP variant generation, blurhash placeholders, and Cloudflare R2/S3 storage. Uploads land in a staging prefix; a background worker polls a Prisma job queue and processes each image asynchronously.

Installation

npm install @xenterprises/fastify-ximagepipeline
# Peer deps
npm install @fastify/multipart fastify

Register @fastify/multipart before this plugin.

Quick Start

import Fastify from "fastify";
import multipart from "@fastify/multipart";
import xImagePipeline from "@xenterprises/fastify-ximagepipeline";

const fastify = Fastify({ logger: true });
await fastify.register(multipart);
await fastify.register(xImagePipeline, {
  r2: {
    endpoint: process.env.R2_ENDPOINT,
    accessKeyId: process.env.R2_ACCESS_KEY_ID,
    secretAccessKey: process.env.R2_SECRET_ACCESS_KEY,
    bucket: process.env.R2_BUCKET,
  },
  db: prisma, // Prisma client with MediaQueue + Media models
});

await fastify.listen({ port: 3000 });
# Upload an image
curl -F "file=@photo.jpg" -F "sourceType=avatar" -F "sourceId=user-123" \
  http://localhost:3000/image-pipeline/upload

Options

Plugin Options

NameTypeDefaultRequiredDescription
r2objectYesR2/S3 connection config
dbPrismaClientYesPrisma client with mediaQueue and media models
moderationobjectnullNoContent moderation config
variantsobject6 sizesNoVariant dimension specs (xs/sm/md/lg/xl/2xl)
sourceTypesobject5 typesNoPer-source-type processing config
workerobjectenabledNoBackground worker settings
stagingPathstring"staging"NoR2 prefix for staging uploads
mediaPathstring"media"NoR2 prefix for processed media
originalsPathstring"originals"NoR2 prefix for original files
maxFileSizenumber52428800NoMax upload size in bytes (50 MB)
allowedMimeTypesstring[]jpeg/png/webp/gifNoAccepted MIME types

r2 Config

FieldTypeRequiredDescription
endpointstringYesR2 or S3-compatible endpoint URL
accessKeyIdstringYesAccess key
secretAccessKeystringYesSecret key
bucketstringYesBucket name
regionstringNoRegion (default "auto" for R2)

worker Config

FieldTypeDefaultDescription
enabledbooleantrueEnable/disable background processing
pollIntervalnumber5000Poll interval in ms
maxAttemptsnumber3Max retry attempts per job
lockTimeoutnumber300000Stale lock timeout in ms (5 min)
failOnErrorbooleantrueThrow if worker fails to start

moderation Config

FieldTypeDescription
handlerasync (buffer, config) => { passed, flags, confidence }Custom moderation function — call AWS Rekognition, Google Vision, etc. If omitted, all images pass.

Default Variants

NameWidthHeightFitUse Case
xs8080coverTiny thumbnails, avatars
sm200200coverThumbnails, lists
md600autoinsideContent images, cards
lg1200autoinsideDetail views
xl1920autoinsideFull-width banners
2xl2560autoinsideRetina/4K displays

Default Source Types

TypeVariantsQualityStore Original
avatarxs, sm85Yes
member_photoxs, sm, md85Yes
gallerymd, lg, xl85No
herolg, xl, 2xl80No
contentmd, lg85Yes

Routes & Methods

Routes registered automatically at /image-pipeline/:

Decorator methods on fastify.xImagePipeline:

Exported Utilities

@xenterprises/fastify-ximagepipeline/image

import { stripExif, generateVariants, generateBlurhash } from "@xenterprises/fastify-ximagepipeline/image";
FunctionDescription
stripExif(buffer)Remove EXIF metadata, preserve orientation
getImageMetadata(buffer)Extract width, height, format, colorspace, hasAlpha, density
compressToJpeg(buffer, quality?)Compress to JPEG (mozjpeg, default quality 85)
generateVariants(buffer, specs, sourceType, quality?)Generate WebP variants per spec
generateBlurhash(buffer)Create 4×3 component blurhash string
calculateFitDimensions(srcW, srcH, maxW, maxH)Aspect-ratio-preserving resize dimensions
getAspectRatio(width, height)Ratio string e.g. "16:9"
validateImage(buffer, options?)Validate dimensions and format
processImage(buffer, sourceType, config)Full pipeline: strip → metadata → variants → blurhash

@xenterprises/fastify-ximagepipeline/s3

import { uploadToS3, deleteFromS3 } from "@xenterprises/fastify-ximagepipeline/s3";
FunctionDescription
initializeS3Client(config)Create S3Client for R2/AWS
uploadToS3(client, bucket, key, buffer, options?)Upload with metadata and cache headers
downloadFromS3(client, bucket, key)Download buffer
deleteFromS3(client, bucket, key)Delete single object
listFromS3(client, bucket, prefix)List objects by prefix
getSignedUrlForS3(client, bucket, key, expiresIn?)Generate signed URL (default 1 h)
getPublicUrl(r2Config, key)Generate public URL
batchDeleteFromS3(client, bucket, prefix)Delete up to 1000 objects by prefix

Error Reference

All errors use the [xImagePipeline] prefix.

ErrorCause
R2 configuration is requiredMissing r2 option at registration
Database instance (Prisma client) is requiredMissing db option at registration
R2 configuration must include: endpoint, accessKeyId, secretAccessKey, bucketIncomplete R2 config
No file providedUpload request has no file
File type {mime} not allowedMIME type not in allowedMimeTypes
sourceType and sourceId are requiredMissing form fields on upload
Unknown sourceType: {type}sourceType not in variant presets
File too large. Maximum size: {n}MBFile exceeds maxFileSize
Failed to upload file to storageR2/S3 upload error
Failed to create processing jobDatabase error during job creation
Media not found: {id}deleteMedia called with invalid media ID

Environment Variables

VariableRequiredDescription
R2_ENDPOINTYesR2 or S3-compatible endpoint URL
R2_ACCESS_KEY_IDYesStorage access key
R2_SECRET_ACCESS_KEYYesStorage secret key
R2_BUCKETYesStorage bucket name
DATABASE_URLYesPrisma database connection string

Database Schema

Requires two Prisma models (see SCHEMA.prisma in the package):

  • MediaQueue — job queue: PENDING → PROCESSING → COMPLETE / REJECTED / FAILED
  • Media — processed records with variant URLs, dimensions, blurhash, and source metadata

How It Works

Upload: Client sends multipart POST. The file is validated (type, size, sourceType) and uploaded to the R2 staging prefix. A MediaQueue row is created with PENDING status; the route returns a jobId immediately.

Processing: A background worker polls MediaQueue for PENDING jobs using pessimistic locking. For each job it downloads from staging, strips EXIF, runs content moderation (if a handler is configured), generates WebP variants per sourceType config, produces a blurhash placeholder, uploads variants and optional originals to R2, creates a Media record, marks the job COMPLETE, and deletes the staging file.

Retrieval: The client polls GET /image-pipeline/status/:jobId. On COMPLETE the response includes all variant URLs, blurhash, and dimensions.

Retry: Failed jobs are retried up to maxAttempts. Stale locks (worker crash) are automatically recovered after lockTimeout.

AI Context

package: "@xenterprises/fastify-ximagepipeline"
type: fastify-plugin
use-when: Async image upload pipeline — EXIF stripping, WebP variant generation, blurhash, content moderation, R2/S3 storage with background job queue
decorator: fastify.xImagePipeline (getStatus, deleteMedia, listMedia, getVariantPresets, getSourceTypes, getVariants)
routes: POST /image-pipeline/upload, GET /image-pipeline/status/:jobId
env: R2_ENDPOINT, R2_ACCESS_KEY_ID, R2_SECRET_ACCESS_KEY, R2_BUCKET, DATABASE_URL
requires: Prisma client with MediaQueue and Media models; @fastify/multipart registered before this plugin
utility-exports: "@xenterprises/fastify-ximagepipeline/image" (image processing), "@xenterprises/fastify-ximagepipeline/s3" (S3 helpers)
Copyright © 2026