npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@xenterprises/fastify-ximagepipeline

v1.2.1

Published

Fastify plugin for image uploads with EXIF stripping, moderation, variant generation, and R2 storage with job queue

Readme

@xenterprises/fastify-ximagepipeline

Fastify plugin for image uploads with EXIF stripping, variant generation, blurhash placeholders, and R2/S3 storage — powered by a background job queue.

Install

npm install @xenterprises/fastify-ximagepipeline

Peer dependencies: fastify ^5.0.0, @fastify/multipart (register 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 "[email protected]" \
     -F "sourceType=avatar" \
     -F "sourceId=user-123" \
     http://localhost:3000/image-pipeline/upload

Plugin Options

| Option | Type | Default | Required | Description | |--------|------|---------|----------|-------------| | r2 | Object | — | Yes | R2/S3 connection config (see below) | | db | Object | — | Yes | Prisma client with mediaQueue and media models | | moderation | Object | null | No | Content moderation config with handler function | | variants | Object | 6 sizes | No | Variant dimension specs (xs/sm/md/lg/xl/2xl) | | sourceTypes | Object | 5 types | No | Per-source-type processing config | | worker | Object | enabled | No | Background worker settings | | stagingPath | string | "staging" | No | R2 prefix for staging uploads | | mediaPath | string | "media" | No | R2 prefix for processed media | | originalsPath | string | "originals" | No | R2 prefix for original files | | maxFileSize | number | 52428800 | No | Max upload size in bytes (50MB) | | allowedMimeTypes | string[] | jpeg/png/webp/gif | No | Accepted MIME types |

r2 Config

| Field | Type | Required | Description | |-------|------|----------|-------------| | endpoint | string | Yes | R2 or S3-compatible endpoint URL | | accessKeyId | string | Yes | Access key | | secretAccessKey | string | Yes | Secret key | | bucket | string | Yes | Bucket name | | region | string | No | Region (default "auto" for R2) |

worker Config

| Field | Type | Default | Description | |-------|------|---------|-------------| | enabled | boolean | true | Enable/disable background processing | | pollInterval | number | 5000 | Poll interval in ms | | maxAttempts | number | 3 | Max retry attempts per job | | lockTimeout | number | 300000 | Lock timeout in ms (5 min) | | failOnError | boolean | true | Throw if worker fails to start |

moderation Config

| Field | Type | Required | Description | |-------|------|----------|-------------| | handler | function | No | async (buffer, config) => { passed, flags, confidence } |

If no handler is provided, all images are approved. Provide a handler to call AWS Rekognition, Google Vision, or any moderation API.

Default Variants

| Name | Width | Height | Fit | Use Case | |------|-------|--------|-----|----------| | xs | 80 | 80 | cover | Tiny thumbnails, avatars | | sm | 200 | 200 | cover | Thumbnails, lists | | md | 600 | auto | inside | Content images, cards | | lg | 1200 | auto | inside | Detail views | | xl | 1920 | auto | inside | Full-width banners | | 2xl | 2560 | auto | inside | Retina/4K displays |

Default Source Types

| Type | Variants | Quality | Store Original | |------|----------|---------|----------------| | avatar | xs, sm | 85 | Yes | | member_photo | xs, sm, md | 85 | Yes | | gallery | md, lg, xl | 85 | No | | hero | lg, xl, 2xl | 80 | No | | content | md, lg | 85 | Yes |

API Endpoints

POST /image-pipeline/upload

Upload an image for processing. Requires multipart form data.

Fields:

  • file (binary, required) — image file
  • sourceType (string, required) — one of the configured source types
  • sourceId (string, required) — identifier for the source entity

Response (202 Accepted):

{
  "jobId": "clx1234...",
  "message": "File uploaded. Processing started.",
  "statusUrl": "/image-pipeline/status/clx1234..."
}

GET /image-pipeline/status/:jobId

Check job processing status.

| Status | HTTP Code | Extra Fields | |--------|-----------|--------------| | PENDING | 202 | — | | PROCESSING | 202 | — | | COMPLETE | 200 | media object with urls, blurhash, dimensions | | REJECTED | 400 | reason, moderationDetails | | FAILED | 500 | error, attempts |

Complete response example:

{
  "jobId": "clx1234...",
  "status": "COMPLETE",
  "media": {
    "id": "media-abc...",
    "urls": { "xs": "https://cdn.../xs.webp", "sm": "https://cdn.../sm.webp" },
    "originalUrl": "https://cdn.../original.jpg",
    "width": 1920,
    "height": 1080,
    "aspectRatio": "16:9",
    "blurhash": "LEHV6nWB2yk8pyo0adR*.7kCMdnj",
    "focalPoint": { "x": 0.5, "y": 0.5 }
  }
}

Decorated Properties

The plugin decorates fastify.xImagePipeline with:

| Method | Signature | Description | |--------|-----------|-------------| | getStatus | (jobId: string) => Promise<Object\|null> | Get job with media relation | | deleteMedia | (mediaId: string) => Promise<{ deleted, r2Deleted }> | Delete media + R2 objects | | listMedia | (sourceType, sourceId) => Promise<Object[]> | List media by source | | getVariantPresets | () => Object | Get source type → variant name mapping | | getSourceTypes | () => Object | Get source type configurations | | getVariants | () => Object | Get variant dimension specs |

Exported Utilities

@xenterprises/fastify-ximagepipeline/image

| Function | Description | |----------|-------------| | 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 | | generateBlurhash(buffer) | Create 4x3 component blurhash | | calculateFitDimensions(srcW, srcH, maxW, maxH) | Aspect-ratio-preserving resize calc | | getAspectRatio(width, height) | Return ratio string like "16:9" | | validateImage(buffer, options?) | Validate dimensions and format | | processImage(buffer, sourceType, config) | Full pipeline: strip, metadata, variants, blurhash |

@xenterprises/fastify-ximagepipeline/s3

| Function | Description | |----------|-------------| | initializeS3Client(config) | Create S3Client for R2/AWS | | uploadToS3(client, bucket, key, buffer, options?) | Upload with metadata + 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 1h) | | getPublicUrl(r2Config, key) | Generate public URL | | batchDeleteFromS3(client, bucket, prefix) | Delete up to 1000 objects by prefix |

Environment Variables

| Variable | Required | Description | |----------|----------|-------------| | R2_ENDPOINT | Yes | R2/S3-compatible endpoint URL | | R2_ACCESS_KEY_ID | Yes | Storage access key | | R2_SECRET_ACCESS_KEY | Yes | Storage secret key | | R2_BUCKET | Yes | Storage bucket name | | DATABASE_URL | Yes | Prisma database connection string |

Error Reference

All errors are prefixed with [xImagePipeline].

| Error | When | |-------|------| | R2 configuration is required | Missing r2 option | | Database instance (Prisma client) is required | Missing db option | | R2 configuration must include: endpoint, accessKeyId, secretAccessKey, bucket | Incomplete R2 config | | No file provided | Upload request has no file | | File type {mime} not allowed | Upload MIME type not in allowedMimeTypes | | sourceType and sourceId are required | Missing form fields on upload | | Unknown sourceType: {type} | sourceType not in variant presets | | File too large. Maximum size: {n}MB | File exceeds maxFileSize | | Failed to upload file to storage | R2 upload error | | Failed to create processing job | Database error during job creation | | Media not found: {id} | deleteMedia called with invalid ID |

Database Schema

The plugin requires two Prisma models. See SCHEMA.prisma in the package for the full schema.

MediaQueue — job queue for async processing (PENDING → PROCESSING → COMPLETE/REJECTED/FAILED)

Media — processed media records with variant URLs, dimensions, blurhash, and metadata

How It Works

  1. Upload: Client sends multipart POST with image + sourceType + sourceId. The file is validated (type, size) and uploaded to R2 staging. A MediaQueue job is created with PENDING status.

  2. Processing: A background worker polls for PENDING jobs using pessimistic locking (prevents duplicate processing). For each job:

    • Downloads from staging
    • Strips EXIF metadata (preserves orientation)
    • Extracts dimensions and format
    • Runs content moderation (if configured)
    • Generates WebP variants per sourceType config
    • Generates blurhash placeholder
    • Uploads variants (and optional original) to R2
    • Creates Media record with URLs and metadata
    • Marks job COMPLETE and cleans up staging
  3. Retrieval: Client polls the status endpoint. On COMPLETE, the response includes all variant URLs, blurhash, and dimensions for immediate frontend use.

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

License

UNLICENSED