@signskart/uploader
v2.0.15
Published
Upload manager SDK for Node.js, React, Vite, and Next.js
Keywords
Readme
🚀 @signskart/uploader
Production-grade Upload Manager SDK by Signskart.
A powerful, fully-typed file upload SDK with queue management, retry logic, cancellation support, concurrency control, and multi-provider architecture (S3 + Cloudinary + Firebase Storage).
✨ Features
- 🚀 Upload queue system
- 🔁 Automatic retry with exponential backoff
- 📊 Real-time progress tracking
- ❌ Cancel uploads (AbortController support)
- ⚡ Concurrency control
- ☁ Multi-provider support (Amazon S3, Cloudinary, Firebase Storage)
- 🧠 Fully typed (TypeScript)
- 🌍 Works with React, Vue, Next.js, Vite, vanilla JS
- 🔐 Secure S3 presigned upload support
- 🖥 Optional server helper included
📦 Installation
npm install @signskart/uploaderor
yarn add @signskart/uploader🏗 Architecture
For Amazon S3, uploads require a backend to generate presigned URLs.
Flow:
Frontend → Backend (presign) → S3 → Frontend uploads directly
Your AWS credentials NEVER reach the browser.
Cloudinary does NOT require backend if using unsigned preset.
🚀 Quick Start
1️⃣ Client (S3)
import { UploadManager, S3Uploader } from '@signskart/uploader';
const manager = new UploadManager(
new S3Uploader({
apiBaseUrl: import.meta.env.VITE_APP_API_BASE_URL,
publicUrl: import.meta.env.VITE_AWS_S3_BASE_URL,
}),
3
);
const task = manager.add({
file,
folder: 'uploads',
});
task.events.subscribe((state) => {
console.log(state.progress, state.status);
});2️⃣ Server Presign Endpoint
Use createS3PresignHandler in your backend and expose:
POST /api/s3/presign-upload
Expected request body:
{
"fileName": "logo.png",
"folder": "uploads",
"contentType": "image/png"
}Expected response body:
{
"signedUrl": "https://...",
"key": "uploads/1710000000000-logo.png",
"publicUrl": "https://cdn.example.com/uploads/1710000000000-logo.png"
}✅ Signskart Admin Pattern (Recommended)
Goal: keep upload implementation inside @signskart/uploader; in admin only pass env and selected file.
1️⃣ Admin wrapper (env only)
// apps/signskart-admin/src/lib/uploads/createUploadManager.ts
import { S3Uploader, UploadManager } from '@signskart/uploader';
export const createUploadManager = (concurrency = 3) => {
return new UploadManager(
new S3Uploader({
apiBaseUrl: import.meta.env.VITE_APP_API_BASE_URL,
publicUrl: import.meta.env.VITE_AWS_S3_BASE_URL,
}),
concurrency
);
};2️⃣ Admin component (select file + call manager)
const uploadManager = useMemo(() => createUploadManager(3), []);
const handleUpload = (file: File) => {
const task = uploadManager.add({
file,
folder: 'assets/store',
});
task.events.subscribe((state) => {
if (state.status === 'success') {
console.log('Uploaded URL:', state.response?.url);
}
});
};This keeps admin thin: env + selected file only.
2️⃣ Backend Setup (Next.js Example)
// app/api/s3/presign-upload/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { createS3PresignHandler } from '@signskart/uploader/server';
const generatePresignedUpload = createS3PresignHandler({
region: process.env.AWS_REGION!,
accessKeyId: process.env.AWS_KEY!,
secretAccessKey: process.env.AWS_SECRET!,
bucket: process.env.AWS_S3_BUCKET!,
publicUrl: process.env.NEXT_PUBLIC_S3_PUBLIC_URL!,
});
export async function POST(req: NextRequest) {
try {
const body = await req.json();
const result = await generatePresignedUpload(body);
return NextResponse.json(result);
} catch (error) {
return NextResponse.json(
{ error: 'Failed to generate presigned URL' },
{ status: 500 }
);
}
}3️⃣ Backend Setup (Express Example)
import express from 'express';
import { createS3PresignHandler } from '@signskart/uploader/server';
const app = express();
app.use(express.json());
const generatePresignedUpload = createS3PresignHandler({
region: process.env.AWS_REGION!,
accessKeyId: process.env.AWS_KEY!,
secretAccessKey: process.env.AWS_SECRET!,
bucket: process.env.AWS_S3_BUCKET!,
publicUrl: process.env.S3_PUBLIC_URL!,
});
app.post('/api/s3/presign-upload', async (req, res) => {
try {
const result = await generatePresignedUpload(req.body);
res.json(result);
} catch (error) {
res.status(500).json({ error: 'Failed to generate presigned URL' });
}
});
app.listen(3000);☁ Using Cloudinary (No Backend Required)
import { UploadManager, CloudinaryUploader } from '@signskart/uploader';
const uploader = new CloudinaryUploader({
cloudName: 'your-cloud-name',
uploadPreset: 'unsigned-preset'
});
const manager = new UploadManager(uploader);
const task = manager.add({
file,
folder: 'designs'
});🔥 Firebase Storage Full Examples (Next.js + Node.js)
Firebase mode is different from S3 mode:
- S3: frontend uploads via backend presign endpoint.
- Firebase: frontend can upload directly with Firebase client SDK.
1️⃣ Next.js Client Example (using uploader package)
Install:
npm install @signskart/uploader firebaseCreate env file:
# .env.local
NEXT_PUBLIC_FIREBASE_API_KEY=...
NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN=...
NEXT_PUBLIC_FIREBASE_PROJECT_ID=...
NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET=...
NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID=...
NEXT_PUBLIC_FIREBASE_APP_ID=...
NEXT_PUBLIC_FIREBASE_MEASUREMENT_ID=...Create uploader helper:
// lib/firebaseUploader.ts
import { FirebaseStorageUploader, UploadManager } from '@signskart/uploader';
export const createFirebaseUploadManager = (concurrency = 3) => {
return new UploadManager(
new FirebaseStorageUploader({
firebaseConfig: {
apiKey: process.env.NEXT_PUBLIC_FIREBASE_API_KEY,
authDomain: process.env.NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN,
projectId: process.env.NEXT_PUBLIC_FIREBASE_PROJECT_ID,
storageBucket: process.env.NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET,
messagingSenderId: process.env.NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID,
appId: process.env.NEXT_PUBLIC_FIREBASE_APP_ID,
measurementId: process.env.NEXT_PUBLIC_FIREBASE_MEASUREMENT_ID,
},
appName: 'signskart-nextjs-uploader',
defaultFolder: 'uploads/nextjs',
}),
concurrency
);
};Use in a client component:
// app/upload/page.tsx
'use client';
import { useMemo, useState } from 'react';
import { createFirebaseUploadManager } from '@/lib/firebaseUploader';
export default function UploadPage() {
const [progress, setProgress] = useState(0);
const [uploadedUrl, setUploadedUrl] = useState('');
const [status, setStatus] = useState('idle');
const manager = useMemo(() => createFirebaseUploadManager(3), []);
const onSelectFile = async (event: React.ChangeEvent<HTMLInputElement>) => {
const file = event.target.files?.[0];
if (!file) return;
const task = manager.add({
file,
metadata: { source: 'nextjs-client' },
});
task.events.subscribe((state) => {
setProgress(state.progress);
setStatus(state.status);
if (state.status === 'success') {
setUploadedUrl(state.response?.url || '');
}
});
// manager.add() starts automatically. Use task.cancel() to stop it.
};
return (
<main style={{ padding: 24 }}>
<h1>Firebase Upload</h1>
<input type="file" onChange={onSelectFile} />
<p>Status: {status}</p>
<p>Progress: {progress}%</p>
{uploadedUrl ? (
<p>
File URL: <a href={uploadedUrl} target="_blank" rel="noreferrer">{uploadedUrl}</a>
</p>
) : null}
</main>
);
}2️⃣ Node.js Example (server upload flow)
Use createFirebaseAdminUploadHandler with Firebase Admin SDK on the server.
Install:
npm install @signskart/uploader firebase-adminNext.js route handler example:
// app/api/firebase/upload/route.ts
import { NextRequest, NextResponse } from 'next/server';
import admin from 'firebase-admin';
import { createFirebaseAdminUploadHandler } from '@signskart/uploader/server';
export const runtime = 'nodejs';
if (!admin.apps.length) {
admin.initializeApp({
credential: admin.credential.cert({
projectId: process.env.FIREBASE_PROJECT_ID,
clientEmail: process.env.FIREBASE_CLIENT_EMAIL,
privateKey: process.env.FIREBASE_PRIVATE_KEY?.replace(/\\n/g, '\n'),
}),
storageBucket: process.env.FIREBASE_STORAGE_BUCKET,
});
}
const uploadToFirebase = createFirebaseAdminUploadHandler({
bucket: admin.storage().bucket(),
defaultFolder: 'uploads/server',
makePublic: true,
});
export async function POST(req: NextRequest) {
try {
const formData = await req.formData();
const file = formData.get('file');
if (!(file instanceof File)) {
return NextResponse.json({ error: 'File is required' }, { status: 400 });
}
const result = await uploadToFirebase({
file,
fileName: file.name,
contentType: file.type,
folder: String(formData.get('folder') || 'uploads/server'),
metadata: { source: 'next-api' },
});
return NextResponse.json(result);
} catch (error) {
console.error(error);
return NextResponse.json({ error: 'Upload failed' }, { status: 500 });
}
}Express example:
import express from 'express';
import multer from 'multer';
import admin from 'firebase-admin';
import { createFirebaseAdminUploadHandler } from '@signskart/uploader/server';
const app = express();
const upload = multer({ storage: multer.memoryStorage() });
admin.initializeApp({
credential: admin.credential.cert({
projectId: process.env.FIREBASE_PROJECT_ID,
clientEmail: process.env.FIREBASE_CLIENT_EMAIL,
privateKey: process.env.FIREBASE_PRIVATE_KEY?.replace(/\\n/g, '\n'),
}),
storageBucket: process.env.FIREBASE_STORAGE_BUCKET,
});
const uploadToFirebase = createFirebaseAdminUploadHandler({
bucket: admin.storage().bucket(),
defaultFolder: 'uploads/node',
makePublic: true,
});
app.post('/api/firebase/upload', upload.single('file'), async (req, res) => {
if (!req.file) return res.status(400).json({ error: 'File is required' });
const result = await uploadToFirebase({
file: req.file.buffer,
fileName: req.file.originalname,
contentType: req.file.mimetype,
folder: req.body.folder,
metadata: { source: 'express-api' },
});
res.json(result);
});
app.listen(3000, () => {
console.log('Server running on http://localhost:3000');
});Node.js env example:
FIREBASE_PROJECT_ID=...
FIREBASE_CLIENT_EMAIL=...
FIREBASE_PRIVATE_KEY="-----BEGIN PRIVATE KEY-----\n...\n-----END PRIVATE KEY-----\n"
FIREBASE_STORAGE_BUCKET=your-project-id.appspot.com🧠 API Reference
UploadManager
new UploadManager(uploader, concurrency?)Parameters
| Parameter | Type | Default | | ----------- | ------------ | -------- | | uploader | BaseUploader | required | | concurrency | number | 3 |
UploadTask
Returned from:
const task = manager.add(options);Properties
task.statetask.events.subscribe()
Methods
task.start()
task.cancel()Upload Options
{
file: File | Blob;
folder?: string;
fileName?: string;
contentType?: string;
metadata?: Record<string, any>;
}Upload Response
Every provider returns the same top-level shape so app code can read common fields without checking the provider first.
{
url: string;
provider: 's3' | 'cloudinary' | 'firebase' | string;
key?: string;
fileName?: string;
originalFileName?: string;
folder?: string;
contentType?: string;
size?: number;
providerData?: Record<string, unknown>;
}Common response fields
| Field | Description |
| ----- | ----------- |
| url | Final URL returned by the provider. Use this in your app/database. |
| provider | Provider that handled the upload: s3, cloudinary, or firebase. |
| key | Provider object key/path/public id when available. |
| fileName | File name used for upload. |
| originalFileName | Original browser/server file name when available. |
| folder | Folder passed in upload options or configured default folder. |
| contentType | MIME type used for the uploaded object. |
| size | Uploaded file size in bytes when available. |
| providerData | Provider-specific details and raw response data. |
S3 response example
{
url: 'https://cdn.example.com/uploads/logo.png',
provider: 's3',
key: 'uploads/1710000000000-logo.png',
fileName: 'logo.png',
originalFileName: 'logo.png',
folder: 'uploads',
contentType: 'image/png',
size: 48231,
providerData: {
bucket: 'my-bucket',
region: 'us-east-1',
signedUrl: 'https://my-bucket.s3.amazonaws.com/...',
publicUrl: 'https://cdn.example.com/uploads/logo.png',
presign: {
signedUrl: 'https://my-bucket.s3.amazonaws.com/...',
key: 'uploads/1710000000000-logo.png',
publicUrl: 'https://cdn.example.com/uploads/logo.png'
},
upload: {
status: 200,
statusText: 'OK',
etag: '"abc123"',
headers: {
etag: '"abc123"'
}
}
}
}Cloudinary response example
Cloudinary returns the richest media metadata for images and videos, including dimensions, format, resource type, bytes, ids, and the raw Cloudinary response.
{
url: 'https://res.cloudinary.com/demo/image/upload/v1710000000/designs/logo.png',
provider: 'cloudinary',
key: 'designs/logo',
fileName: 'logo',
originalFileName: 'logo.png',
folder: 'designs',
contentType: 'image/png',
size: 48231,
providerData: {
assetId: 'a1b2c3',
publicId: 'designs/logo',
version: 1710000000,
versionId: 'version-id',
signature: 'signature',
width: 1200,
height: 800,
format: 'png',
resourceType: 'image',
type: 'upload',
createdAt: '2026-05-24T12:00:00Z',
bytes: 48231,
etag: 'abc123',
url: 'http://res.cloudinary.com/demo/image/upload/...',
secureUrl: 'https://res.cloudinary.com/demo/image/upload/...',
originalFilename: 'logo',
response: {
// Full Cloudinary upload response
}
}
}Firebase client response example
FirebaseStorageUploader uses the browser Firebase SDK and returns Storage metadata from the upload snapshot.
{
url: 'https://firebasestorage.googleapis.com/v0/b/app.appspot.com/o/uploads%2Flogo.png?...',
provider: 'firebase',
key: 'uploads/logo.png',
fileName: 'logo.png',
originalFileName: 'logo.png',
folder: 'uploads',
contentType: 'image/png',
size: 48231,
providerData: {
bucket: 'app.appspot.com',
fullPath: 'uploads/logo.png',
name: 'logo.png',
contentType: 'image/png',
size: 48231,
generation: '1710000000000000',
metageneration: '1',
timeCreated: '2026-05-24T12:00:00.000Z',
updated: '2026-05-24T12:00:00.000Z',
md5Hash: '...',
cacheControl: undefined,
contentDisposition: undefined,
contentEncoding: undefined,
contentLanguage: undefined,
customMetadata: {
source: 'nextjs-client'
},
downloadUrl: 'https://firebasestorage.googleapis.com/...',
metadata: {
// Full Firebase Storage metadata object
}
}
}Firebase Admin response example
createFirebaseAdminUploadHandler returns the same shared shape on Node.js, plus Admin Storage metadata when getMetadata() is available.
{
url: 'https://storage.googleapis.com/app.appspot.com/uploads/server/logo.png',
provider: 'firebase',
key: 'uploads/server/1710000000000-logo.png',
fileName: 'logo.png',
originalFileName: 'logo.png',
folder: 'uploads/server',
contentType: 'image/png',
size: 48231,
providerData: {
bucket: 'app.appspot.com',
fullPath: 'uploads/server/1710000000000-logo.png',
name: '1710000000000-logo.png',
contentType: 'image/png',
size: 48231,
md5Hash: '...',
crc32c: '...',
generation: '1710000000000000',
metageneration: '1',
timeCreated: '2026-05-24T12:00:00.000Z',
updated: '2026-05-24T12:00:00.000Z',
customMetadata: {
source: 'next-api'
},
downloadUrl: 'https://storage.googleapis.com/app.appspot.com/uploads/server/logo.png',
saveResponse: undefined,
makePublicResponse: undefined,
metadata: {
// Full Firebase Admin Storage metadata object
}
}
}Reading response data in your app
task.events.subscribe((state) => {
if (state.status !== 'success') return;
const uploaded = state.response;
console.log(uploaded?.url);
console.log(uploaded?.provider);
console.log(uploaded?.key);
console.log(uploaded?.contentType);
console.log(uploaded?.size);
console.log(uploaded?.providerData);
});🔁 Retry Logic
- Automatic retry
- Exponential backoff
- Default max retries: 2
- Cancels if AbortController triggered
❌ Cancel Upload
task.cancel();📊 Listen to Progress
task.events.subscribe((state) => {
console.log(state.progress, state.status);
});State structure:
{
id: string;
progress: number;
status: 'queued' | 'uploading' | 'success' | 'error' | 'cancelled';
error?: string;
response?: UploadResponse;
}⚛ Example React Usage
const handleUpload = (file: File) => {
const task = manager.add({ file, folder: 'uploads' });
task.events.subscribe((state) => {
setProgress(state.progress);
});
};🔐 Security Notes (Important)
- AWS credentials must NEVER be exposed to frontend.
- Always generate presigned URLs server-side.
- Cloudinary unsigned preset must have restricted permissions.
- Set proper S3 bucket CORS configuration.
🛠 Requirements
- Modern browser (AbortController support)
- Node.js 18+ for server helper
- Backend endpoint for S3 presign (if using S3)
📦 Package Exports
Client:
import { UploadManager } from '@signskart/uploader';Server helper:
import { createS3PresignHandler } from '@signskart/uploader/server';📈 Roadmap
- Multipart uploads (100MB+)
- Image compression plugin
- Validation middleware
- Signed download URLs
- React hooks wrapper
📜 License
MIT © Signskart
