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

uplifty

v2.0.1

Published

Direct-to-storage file uploads from the browser via presigned URLs. Zero dependencies, works with S3, Cloudflare R2, DigitalOcean Spaces, MinIO, Backblaze, Wasabi, Tigris and Supabase.

Readme

Uplifty

Direct-to-storage file uploads from the browser. Your server mints a signed ticket. The browser sends the bytes straight to storage. Your credentials never leave your server, and the files never touch it.

CI npm bundle size zero dependencies types license

Quick start · Documentation · Connect an S3 bucket · Architecture · API


The problem

Uploading a file to cloud storage from a web app has exactly one safe shape, and every team rebuilds it from scratch:

flowchart LR
    subgraph bad ["❌ Proxying through your server"]
        direction LR
        B1[Browser] -->|100 MB| S1[Your server]
        S1 -->|100 MB again| T1[(Storage)]
    end

    subgraph good ["✅ Direct to storage"]
        direction LR
        B2[Browser] -.->|"a few bytes<br/>(ask for a ticket)"| S2[Your server]
        B2 -->|100 MB, once| T2[(Storage)]
    end

Proxying burns your bandwidth, your memory, and your request timeout on bytes you only forward. Direct upload avoids all three — but doing it safely means presigned URLs, SigV4 signing, CORS, progress tracking, retries, and cancellation. That is a week of work per project, and the failure mode of getting it wrong is leaked credentials.

Uplifty is that week, packaged.

How it works

sequenceDiagram
    autonumber
    participant B as Browser<br/>(uplifty)
    participant S as Your server<br/>(uplifty/server)
    participant T as Storage<br/>(S3, R2, Supabase…)

    B->>S: POST /api/upload<br/>{ fileName, contentType, size }
    Note over S: Authenticate the user.<br/>Enforce size + type limits.<br/>Decide the object key.
    S->>S: Sign a presigned URL<br/>(credentials stay here)
    S-->>B: UploadTicket<br/>{ url, headers, key, expiresAt }
    B->>T: PUT the file bytes ──────────►
    Note over B,T: onProgress fires as bytes leave.<br/>Your server is not in this path.
    T-->>B: 200 OK + ETag
    B-->>S: (your code) save `key` to the database

The browser half never learns which provider is on the other end. It receives a URL, some headers, and a method — it PUTs the bytes and reports progress. That is the whole contract, and it is why swapping S3 for R2 is a server-side config change with no client rebuild.

Why the split matters

The package ships as three separate entry points, and that boundary is enforced in CI:

flowchart TB
    subgraph browser ["🌐 Browser bundle"]
        C["<b>uplifty</b><br/>Uplifty client<br/><i>3.9 kB gzipped</i>"]
        R["<b>uplifty/react</b><br/>useUpload hook<br/><i>4.7 kB gzipped</i>"]
        R --> C
    end

    subgraph server ["🔒 Server / edge only"]
        SV["<b>uplifty/server</b><br/>S3 + SigV4 signer<br/><i>5.5 kB gzipped</i>"]
        K["🔑 accessKeyId<br/>🔑 secretAccessKey"]
        SV -.holds.-> K
    end

    C -.->|"UploadTicket (JSON over HTTP)<br/>no import, no coupling"| SV

    style browser fill:#e8f4ff,stroke:#4a90d9
    style server fill:#fff4e6,stroke:#d99b4a
    style K fill:#ffe0e0,stroke:#d94a4a

uplifty and uplifty/server never import each other. They communicate only through the UploadTicket JSON contract. CI greps the built browser bundle for signing code and fails the build if any appears — so the credential leak that this architecture prevents cannot be reintroduced by an accidental import.

Upgrading from 1.x? Version 1.0.0 accepted your AWS keys in browser code, which exposed them to every visitor. See MIGRATION.mdrotate those credentials first.

Install

npm install uplifty

Zero runtime dependencies. Works in Node 20+, Bun, Deno, Cloudflare Workers, Vercel Edge, and every modern browser. React is an optional peer dependency, needed only for uplifty/react.

Quick start

1. Mint tickets on your server

// app/api/upload/route.ts  (Next.js App Router)
import { S3 } from 'uplifty/server';

const storage = new S3({
	accessKeyId: process.env.AWS_ACCESS_KEY_ID!,
	secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY!,
	region: 'us-east-1',
	bucket: 'my-app-uploads',
	maxSize: 10 * 1024 * 1024,
	allowedContentTypes: ['image/*', 'application/pdf'],
});

export async function POST(request: Request) {
	const session = await auth(); // ← your auth. This is the real access control.
	if (!session) return new Response('Unauthorized', { status: 401 });

	const { fileName, contentType, size } = await request.json();

	const ticket = await storage.createUploadTicket({
		fileName,
		contentType,
		size,
		// Scopes the key to this user. Note it REPLACES any instance-level
		// `prefix` rather than nesting inside it.
		prefix: `users/${session.userId}/`,
	});

	return Response.json(ticket);
}

2. Upload from the browser

import { Uplifty } from 'uplifty';

const uplifty = new Uplifty({ getTicket: '/api/upload' });

const result = await uplifty.upload(file, {
	onProgress: ({ percent }) => setProgress(percent),
});

console.log(result.key, result.url);

3. Or use the React hook

import { useUpload } from 'uplifty/react';

export function UploadButton() {
	const { upload, progress, isUploading, error, abort } = useUpload({
		getTicket: '/api/upload',
	});

	return (
		<>
			<input
				type="file"
				disabled={isUploading}
				onChange={e => e.target.files?.[0] && upload(e.target.files[0])}
			/>
			{isUploading && (
				<>
					<progress value={progress} max={100} />
					<button onClick={abort}>Cancel</button>
				</>
			)}
			{error && <p role="alert">{error.message}</p>}
		</>
	);
}

That is the whole integration. Connecting a real S3 bucket — the IAM policy and CORS rule — takes about five minutes.

What you get

| | | | ------------------------ | ---------------------------------------------------------------------------------------------------------- | | Never proxies bytes | Files go browser → storage. Your server handles a few hundred bytes of JSON per upload. | | Credentials stay put | Secret keys exist only in uplifty/server, which is unreachable from the browser entry point. | | Real enforcement | content-type and content-length are bound into the signature — the provider rejects mismatches, not you. | | Progress and cancel | Byte-accurate progress via XHR, AbortSignal cancellation, per-file and aggregate. | | Retries | Exponential backoff on transient failures, with automatic ticket refresh when one expires mid-retry. | | Concurrency | uploadAll runs a bounded worker pool and stops dispatching on the first failure. | | Eight backends | One signer, eight providers — S3, R2, Spaces, MinIO, Backblaze, Wasabi, Tigris, Supabase. | | Genuinely small | 3.9 kB gzipped in the browser, zero dependencies, tree-shakeable, ESM + CJS. | | Runs anywhere | WebCrypto only — Node, Bun, Deno, Workers, Edge. No aws-sdk, no Node built-ins. |

Providers

Every backend is the same S3 class with different configuration. Presets exist so you do not have to remember each vendor's endpoint format:

import { S3, R2, Spaces, MinIO, Backblaze, Wasabi, Tigris, Supabase } from 'uplifty/server';

const r2 = R2({ accountId: '...', accessKeyId, secretAccessKey, bucket: 'uploads' });
const supabase = Supabase({ projectRef: 'abcd', region: 'us-east-1', accessKeyId, secretAccessKey, bucket: 'avatars' });

Full configuration for each — including which credentials to generate and where — is in docs/providers.md.

Documentation

| Guide | What it covers | | -------------------------------------------------- | --------------------------------------------------------------------------- | | Getting started | A complete working integration, from install to first uploaded file. | | Connect an S3 bucket | Bucket, IAM user, least-privilege policy, CORS, and verification. Start here. | | Providers | Setup for all eight backends, with the gotchas each one has. | | API reference | Every option, method, type, and error code. | | Architecture | Why it is built this way, and how the signing actually works. | | Recipes | Image galleries, drag-and-drop, private files, post-upload processing. | | Troubleshooting | Every error this library throws, and what actually causes it. | | Security model | Threat model, what a ticket does and does not authorise, reporting a bug. | | Migrating from 1.x | What changed and why, plus the credential rotation you must do. |

Runnable examples live in examples/vanilla JS, Express, and Next.js.

Security model in one paragraph

A ticket is a capability: it authorises one upload, of one declared size and content type, to one object key, for a short window. Your server decides all four before signing, which is where your real access control belongs. Client-side maxSize and allowedContentTypes are UX conveniences — anyone can bypass them. The server-side equivalents are the enforcement, because they are bound into the signature and checked by the storage provider itself. Full detail in SECURITY.md.

Runtime support

| Runtime | Browser entry | Server entry | | -------------------------- | ------------- | ------------------------ | | Modern browsers | ✅ | — | | Node 20+ | — | ✅ | | Bun / Deno | — | ✅ | | Cloudflare Workers | — | ✅ | | Vercel / Netlify Edge | — | ✅ | | React Native | ⚠️ untested | — |

The server half needs only crypto.subtle and fetch, both of which are standard in every runtime above.

Roadmap

Google Cloud Storage, Azure Blob, Cloudflare Images, native Supabase, multipart uploads for very large files, and framework adapters beyond React are tracked as open issues with the research already done. Contributions welcome — start with CONTRIBUTING.md.

Contributing

git clone https://github.com/nitin-1926/UpLifty.git
cd UpLifty && npm install
npm run check   # typecheck + lint + tests
npm run smoke   # build, pack, and verify the real tarball

See CONTRIBUTING.md for the full workflow and CODE_OF_CONDUCT.md for community expectations.

License

MIT © Nitin Gupta