Minimal drag & drop file handoff with expirations, passwords & download limits.
Quick Start · Features · Configuration · Security · Cleanup · Roadmap
- For now i'm hosting a private instance at https://pastry.6A31.com
Pastry is a lightweight, self-hostable "pastebin for files" designed for fast, temporary hand-offs between devices or teammates. It purposely avoids accounts, large dependency stacks and heavy persistence logic. Files are short-lived, constrained, and always treated as untrusted.
Use cases:
- Move a build artifact from a dev machine to a laptop quickly.
- Share a screenshot or log bundle that should self-expire.
- Provide a one-off download with a burn-after-read limit.
| Strength | Description |
|---|---|
| Minimal UX | Single page drag & drop; no clutter, no marketing chrome. |
| Security-first defaults | Forced attachment downloads, randomized filenames, optional global password policy. |
| Ephemeral by design | Expirations + max download counts + automatic background cleanup. |
| Zero external build deps | SQLite (via better-sqlite3) by default; optional MongoDB if you outgrow local storage. |
| Simple deploy | A plain Next.js 14 app (App Router) - drop into any Node hosting target. |
| Extendable | Clear hooks for adding scanning, additional auth, storage backends, signing, etc. |
| Upload Panel | Download Prompt | Recent List |
|---|---|---|
![]() |
![]() |
![]() |
Core capabilities shipped in this repository:
- Drag & drop or file picker uploads
- Per-file controls:
- Expiration presets (2m → 30d, server clamps at 30 days)
- Optional download password (bcrypt hashed)
- Max downloads (burn after N) or Unlimited toggle
- Optional global policies:
- Require password on every upload (
PASTRY_REQUIRE_FILE_PASSWORDS) - Admin-only uploads lock (
PASTRY_ADMIN_ONLY_UPLOADS+PASTRY_ADMIN_PASSWORD)
- Require password on every upload (
- Automatic background cleanup (every minute) removing expired / exhausted files (overridable)
- Secure download flow (POST with password if required, counters increment atomically)
- Session-scoped "recent uploads" (via opaque cookie
psid- users cannot see others' files) - Randomized, non-guessable stored filenames (nanoid)
- Strict server-side validation (size, expiry clamp, password length, positive max downloads)
- Streaming downloads with attachment headers & sanitized filenames
- Basic per-IP upload & download rate limiting (configurable, in-memory) with optional enumeration hiding
- Tailwind UI with accessible custom select + refined controls
| Layer | Responsibility |
|---|---|
| Next.js API Routes | Upload, download, metadata, recent, cleanup. |
lib/db.ts |
Storage abstraction (SQLite or Mongo), metadata CRUD, TTL index creation (Mongo). |
| Blob Store | Local filesystem directory (PASTRY_STORAGE_DIR). |
| Session | Lightweight cookie to map recent uploads (no authentication). |
| Cleanup | Scheduled in-process minute interval + manual /api/cleanup endpoint. |
cp .env.example .env.local # Adjust secrets & limits
npm install
npm run dev
# Open http://localhost:3000- Clone repo & install deps.
- Copy
.env.exampleto.env.localand customize:
- Set
PASTRY_ADMIN_PASSWORDif enabling admin-only uploads. - Adjust
PASTRY_MAX_FILE_SIZE(bytes) for your environment. - Provide
MONGODB_URIif you'd like server-enforced TTL cleanup of metadata (files still cleaned by scheduler). - Optionally set
PASTRY_CLEANUP_TOKENto protect manual cleanup requests.
- Run
npm run dev(ornpm run build && npm startfor production). - Point reverse proxy / ingress at the Next.js server (ensure HTTPS if exposing publicly).
- Run behind HTTPS; passwords are posted with form data.
- Consider isolating the storage directory on a low-privilege volume.
- Enable MongoDB if you need automatic metadata expiration at DB level (TTL index is created automatically).
- Provide a process manager (systemd, PM2, docker) to ensure the in-process cleanup interval remains active.
Key environment variables (see .env.example for the full list):
| Variable | Purpose |
|---|---|
PASTRY_MAX_FILE_SIZE |
Absolute max upload size in bytes. |
PASTRY_ADMIN_ONLY_UPLOADS |
Gate uploads behind admin password. |
PASTRY_ADMIN_PASSWORD |
Password value required when admin lock enabled. |
PASTRY_ALLOWED_MIME_REGEX |
Optional server regex to allowlist MIME types. |
PASTRY_STORAGE_DIR |
Directory path for blob storage. |
PASTRY_JWT_SECRET |
(Reserved/legacy) Secret for potential future signed links; presently unused. |
MONGODB_URI |
Switch metadata store to Mongo (adds TTL index). |
PASTRY_CLEANUP_TOKEN |
Bearer token required to call /api/cleanup manually. |
PASTRY_DISABLE_SCHEDULER |
Set to true to disable minute cleanup (not recommended). |
PASTRY_LOG_LEVEL |
Log verbosity: silent, error, warn, info, debug. |
PASTRY_SCHEDULER_INTERVAL_MS |
Interval (ms) for the in-process cleanup loop (default: 60000). |
PASTRY_FORCE_SCHEDULER |
Force enable the scheduler even if disabled or in test/CI contexts. |
PASTRY_UPLOAD_RATE_LIMIT |
Max uploads allowed per IP per window. |
PASTRY_UPLOAD_RATE_WINDOW_MS |
Window length for upload limiting. |
PASTRY_DOWNLOAD_RATE_LIMIT |
Max download attempts per IP per window. |
PASTRY_DOWNLOAD_RATE_WINDOW_MS |
Window length for download limiting. |
PASTRY_DOWNLOAD_ENUM_HIDE |
If true, many download failure modes return 404 to reduce enumeration signals. |
CLEANUP_PURGE_DOWNLOADS_EXCEEDED |
If true, delete DB record immediately when max downloads reached (default keeps record until expiry). |
Legacy variable names with the spelling PATRY_* are auto-mapped at startup and emit a deprecation warning (see lib/config.ts). Prefer the PASTRY_* forms.
| Control | Rationale |
|---|---|
| Randomized stored names | Prevent path enumeration. |
| Attachment download | Block inline execution in browsers. |
| Expiry & max downloads | Reduce exposure window and footprint. |
| Password hashing (bcrypt) | Avoid storing plain download secrets. |
| Size & MIME guard | Bound resource use; optional type narrowing. |
| Session isolation cookie | Prevent cross-user recent listing leakage. |
| Cleanup loop | Frees disk and prunes stale data quickly. |
- Large file floods: mitigated by
PASTRY_MAX_FILE_SIZEand per-IP upload rate limiting (augment with upstream reverse proxy / WAF for stronger guarantees). - Malware: project intentionally treats all content as hostile; integrate AV / content scanning hook if required.
- Brute forcing passwords: basic per-IP download rate limiting + optional enumeration hiding (
PASTRY_DOWNLOAD_ENUM_HIDE=true) reduce guess velocity & information leakage. For higher assurance, add proxy-level global & distributed limits.
If not set explicitly, upload and download windows default to 60s. The default upload limit is 30 per 60s (PASTRY_UPLOAD_RATE_LIMIT=30). The default download limit is 120 per 60s (PASTRY_DOWNLOAD_RATE_LIMIT=120). Adjust conservatively; extremely tight windows can frustrate legitimate use.
Files are removed in two ways:
- Automatic minute scheduler: server issues an internal POST to
/api/cleanupevery minute (unless disabled) deleting expired / exhausted files and their metadata. - External/manual trigger: You (or external automation) can POST to
/api/cleanupdirectly. Protect this withPASTRY_CLEANUP_TOKENin production so only authorized jobs (Cron, GitHub Actions, k8s CronJob) can invoke it. This means cleanup is not coupled to user traffic - your storage will still shrink even when the UI is idle.
MongoDB deployments additionally expire metadata via TTL index; the scheduler still deletes the physical file.
The built-in cleanup loop is intentionally simple and idempotent. You can tune or override its behavior with these variables:
| Variable | Behavior | Notes |
|---|---|---|
PASTRY_SCHEDULER_INTERVAL_MS |
Sets how often the internal loop runs. | Default 60000 (60s). Use a lower value (e.g. 5000) only for tests / demos. |
PASTRY_DISABLE_SCHEDULER |
Disables automatic loop entirely. | Pair with an external job calling /api/cleanup + token. |
PASTRY_FORCE_SCHEDULER |
Forces the loop to start even when other logic (e.g. test env heuristics) would skip it. | Useful in CI integration tests to exercise lifecycle behavior. |
Precedence: PASTRY_FORCE_SCHEDULER=true overrides PASTRY_DISABLE_SCHEDULER=true (force wins). If both are unset, the loop starts with the default interval.
Example .env.local snippet:
# Run cleanup every 30s instead of 60s
PASTRY_SCHEDULER_INTERVAL_MS=30000
# (Optional) Force enable in a CI job that sets PASTRY_DISABLE_SCHEDULER elsewhere
PASTRY_FORCE_SCHEDULER=true
Running multiple Pastry instances (e.g. behind a load balancer) means each instance will attempt cleanup on its own interval. This is safe because:
- Deleting an already-deleted file is ignored.
- Metadata removal uses primary key constraints; duplicate delete attempts are no-ops.
However, for large scale you may prefer a single external cleanup job:
- Set
PASTRY_DISABLE_SCHEDULER=trueon all app instances. - Schedule a secure job (Cron, Cloud Scheduler, GitHub Action) that POSTs to
/api/cleanupwith theAuthorization: Bearer <PASTRY_CLEANUP_TOKEN>header.
- Keep intervals >= 30s in production to avoid unnecessary churn.
- Very short intervals (< 5s) are only recommended for automated tests where fast expiry feedback matters.
- Monitor logs at
infoordebuglevel to view each run summary. - If storage pressure is critical, you can temporarily lower the interval, then restore to 60s.
- Always set
PASTRY_CLEANUP_TOKENif the service is network-reachable; otherwise anyone could trigger aggressive cleanup bursts. - The internal loop does not use the token - it calls the handler directly - so forgetting to set it will not break automatic cleanup.
- Avoid exposing the cleanup endpoint publicly without a token even if you believe obscurity suffices.
- Set
CLEANUP_PURGE_DOWNLOADS_EXCEEDED=false(default) if you want clients to still receive a specific "download limit reached" response after the blob has been removed.
Pastry ships with lightweight, in-memory (per-process) rate limiting to slow automated abuse without adding external dependencies.
| Aspect | Upload | Download |
|---|---|---|
| Env toggle | PASTRY_UPLOAD_RATE_LIMIT / PASTRY_UPLOAD_RATE_WINDOW_MS |
PASTRY_DOWNLOAD_RATE_LIMIT / PASTRY_DOWNLOAD_RATE_WINDOW_MS |
| Scope | Per IP (uses x-forwarded-for first) |
Per IP |
| Defaults | 30 uploads / 60s | 120 downloads / 60s |
| Headers | X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset |
Same |
Because limits are in memory they apply separately to each instance; in horizontal deployments you should:
- Keep these as a soft backstop.
- Add a shared store (Redis) or upstream gateway limits for a hard global cap.
Setting PASTRY_DOWNLOAD_ENUM_HIDE=true causes the download endpoint to reply with a generic 404 for many failure modes (expired, wrong password, over limit, rate limited) making it harder to distinguish valid IDs. This trades some user clarity for reduced information leakage - enable only if enumeration pressure is a demonstrated concern.
- Keep windows >= 30s to avoid burst thrash; prefer raising limit vs. shrinking window for legitimate high-volume use.
- High-throughput trusted networks can set very large limits or disable by patching the limiter.
- Observe limiter headers to adjust thresholds before users encounter 429s.
- Chunked / resumable uploads
- Pluggable antivirus / content scanning hook
- Signed temporary "one click" share links
- Optional S3 / object storage backend
- Rate limiting & abuse throttling
- Multi-file batch uploads
- Progress bars + pause/resume
Vitest integration tests exercise security, validation, lifecycle and cleanup behavior (test/security.spec.ts). The suite spins up ephemeral dev servers with modified environment variables to:
- Verify password requirement enforcement and validation bounds (size, max downloads, expiry clamp).
- Exercise download flows (missing/empty/incorrect password, single-use burn-after-read).
- Confirm rate limiting headers and rejection after configured thresholds.
- Validate cleanup scheduler behavior (including forced fast interval in tests) and manual
/api/cleanupendpoint with token auth. - Test the
CLEANUP_PURGE_DOWNLOADS_EXCEEDEDtoggle in both retain and purge modes. - Track and assert storage directory cleanliness after all tests; any orphaned files are manually removed and reported.
Run tests:
npm run test:security| Script | Purpose |
|---|---|
npm run dev |
Start Next dev server. |
npm run build |
Build production bundle. |
npm start |
Start production server (after build). |
npm run lint |
Lint with ESLint/Next config. |
npm run test:security |
Run Vitest security/integration suite. |
Post-install hook (postinstall) runs scripts/prepare.js (if present) for any lightweight setup tasks.
Issues & PRs welcome. Please:
- Open an issue for substantial feature proposals first.
- Add tests (Vitest) for new server behaviors.
- Keep dependencies minimal; prefer small, audited libs.
Released under the MIT License. See LICENSE.


