Installation
PasteHere is a self-hosted pastebin that runs entirely on Cloudflare's edge. There is no server to provision, no Docker container to babysit, and no database to tune — Workers serves the app, D1 stores paste metadata, and R2 stores the paste bodies. You pay Cloudflare nothing on the free plan for low traffic, and the Workers paid plan ($5/mo) covers millions of requests.
Get the source
- Direct purchase: buy direct ($9.99 one-time) — you receive a download link to the source archive by email
- Product page: /pastehere/
Prerequisites
| Requirement | Details |
|---|---|
| Cloudflare account | Free tier works for personal use; Workers Paid ($5/mo) for higher limits |
| Node.js | 20.x or newer — for running Wrangler locally |
| Wrangler CLI | v3.60+ — installed via the included package.json |
| macOS / Linux / WSL2 | Developed and tested on macOS Apple Silicon; the deploy commands work cross-platform |
First deploy
- Unpack the archive and run
npm install. - Run
npx wrangler loginand authenticate your Cloudflare account. - Create the backing resources (script included):
npm run db:initprovisions the D1 database and R2 bucket and writes their IDs intowrangler.toml. - Run the schema migration:
npm run db:migrate— appliesschema/0001_init.sqlto the new D1 instance. - Deploy:
npm run deploy. Wrangler publishes the Worker; your pastebin is live athttps://pastehere.<your-subdomain>.workers.devwithin seconds.
paste.yourdomain.com. Cloudflare provisions the certificate automatically — no DNS wrangling.
Quick Start
1. Create your first paste
- Open
https://paste.yourdomain.com(or theworkers.devURL above). - Paste your snippet into the editor. Syntax highlighting and line numbers render live.
- Pick a language (auto-detect handles the common ones) and choose an expiration: 10 min, 1 hour, 1 day, 1 week, 1 month, or Never.
- Click Create. The URL
https://paste.yourdomain.com/<slug>is returned — share it.
2. Create a paste from the CLI (curl)
For terminal-driven workflows — error logs, build outputs, sharing a snippet in chat — use the HTTP API:
curl -X POST https://paste.yourdomain.com/api/paste \
-H "Authorization: Bearer $PASTEHERE_TOKEN" \
-H "Content-Type: text/plain" \
--data-binary @error.log
The response JSON contains the url, slug, and expiresAt. Pipe it through jq -r .url and onto your clipboard with pbcopy.
3. Set up an API token
Without authentication, anyone who finds your instance can create pastes. Enable token auth:
- Set
AUTH_MODE = "token"inwrangler.tomlunder[vars]. - Generate a secret:
openssl rand -hex 32. - Store it as a Worker secret:
npx wrangler secret put PASTEHERE_TOKENand paste the value. - Redeploy (
npm run deploy). Clients must now sendAuthorization: Bearer <token>on POST/PUT/DELETE.
4. Run locally with Wrangler dev
Before redeploying, test changes locally with the same bindings as production:
npm run dev
This starts Wrangler's local runtime with a local D1 (Miniflare SQLite) and a local R2 simulator at http://localhost:8787. Changes to the Worker code hot-reload instantly.
Configuration
All runtime configuration lives in wrangler.toml under [vars]. Secrets (tokens, signing keys) go through wrangler secret put and never appear in the file.
wrangler.toml vars
| Variable | Default | Purpose |
|---|---|---|
AUTH_MODE | "none" | none, token (single shared token), or password (HTTP Basic prompt) |
DEFAULT_EXPIRY | "1d" | Expiration applied when the client doesn't send one |
MAX_BODY_BYTES | 524288 | Hard cap on paste body size — 512 KiB. Raise on Workers Paid for up to 10 MiB per request |
SLUG_LENGTH | 8 | Random slug length in characters from the Crockford base-32 alphabet |
ENABLE_BURN_AFTER_READ | "true" | Allow clients to mark a paste as one-shot (deletes on first GET) |
PUBLIC_LISTING | "false" | If true, / shows recent pastes — leave false for private deployments |
SITE_TITLE | "PasteHere" | Title shown in the header and <title> |
Worker secrets
| Secret | Required when |
|---|---|
PASTEHERE_TOKEN | AUTH_MODE = "token" |
PASTEHERE_PASSWORD | AUTH_MODE = "password" |
DELETE_KEY | Always — used to sign delete URLs so they cannot be guessed |
Bindings
The Worker binds to one D1 database and one R2 bucket. Both are declared in wrangler.toml:
[[d1_databases]]
binding = "DB"
database_name = "pastehere"
database_id = "<generated>"
[[r2_buckets]]
binding = "BUCKET"
bucket_name = "pastehere-bodies"
Why R2 + D1? D1 holds lightweight metadata (slug, language, expiry, MIME, content hash) for fast listing and search. R2 holds the paste bodies — there is no per-byte egress fee on R2, so a popular paste can be read a million times without charging you anything beyond the storage cost.
Expiry schedule
A scheduled Worker (Cron Trigger) sweeps expired pastes once an hour. Configure it under [triggers] in wrangler.toml:
[triggers]
crons = ["0 * * * *"]
The sweep deletes the D1 row and the R2 object in the same run, then logs a per-object count. Burn-after-read pastes delete on first GET and don't wait for the sweep.
Key Features
Zero-operations self-hosting
No server, no Docker, no Kubernetes, no database administration. Deploy is one command (npm run deploy); Cloudflare handles TLS, scaling, DDoS mitigation, and edge caching. Total maintenance cost: bumping Wrangler when a new minor ships.
Cloudflare R2 storage with zero egress fees
Paste bodies live in R2, not D1 — meaning reads don't count against the D1 row-read quota and don't incur the per-GB egress fees that S3 and GCS charge. A hot paste that's read a million times costs the same as one read once.
D1 metadata for fast search and listing
Every paste's metadata (slug, language, size, expiry, hash, created/updated timestamps) lives in D1 — a distributed SQLite database that runs at the edge. Listing recent pastes and filtering by language is a sub-10 ms indexed query, not an R2 LIST call.
Burn-after-read and expirations
One-shot pastes (popular for sharing secrets) delete themselves on first GET. Combined with explicit expiration (10 min to Never), PasteHere handles both ephemeral chat snippets and long-lived reference material without manual cleanup.
Token or password authentication
Single shared bearer token for API/CLI workflows, or HTTP Basic password prompt for browser-only users. Both modes hide behind the same check; mixing them is supported (token wins on API routes, password protects the web UI).
Syntax-highlighted editor and raw endpoints
The web UI uses a CodeMirror editor with auto-language detection. The same paste is also retrievable raw (/<slug>/raw) and as downloadable text (/<slug>/download?filename=snippet.go) for piping into curl | sh or sharing with CI pipelines.
Troubleshooting
Deploy fails: "D1 database not found"
Symptom: npm run deploy aborts with D1_ERROR: no such database.
Fix: The database_id in wrangler.toml doesn't match an existing D1 instance. Re-run npm run db:init — it prints the correct database_id and writes it into the file. If you manually provisioned, run npx wrangler d1 list and copy the ID verbatim.
R2 binding works locally but errors in production
Symptom: npm run dev uploads and reads pastes; production returns R2 bucket not found on POST.
Fix: The bucket name in wrangler.toml has a typo or belongs to a different Cloudflare account. Buckets are account-scoped — confirm with npx wrangler r2 bucket list that the name matches exactly, then redeploy. Buckets created in a different account must be migrated or re-created in the Worker's account.
POST returns 401 even with correct token
Symptom: API returns {"error":"unauthorized"} with a valid-looking Authorization: Bearer … header.
Fix: The PASTEHERE_TOKEN secret was set in .dev.vars but never pushed to production. Secrets are environment-specific — run npx wrangler secret put PASTEHERE_TOKEN against production explicitly. Verify with npx wrangler secret list.
Expired pastes aren't being deleted
Symptom: Pastes past their expires_at are still retrievable days later.
Fix: The Cron Trigger isn't firing. Check the Cloudflare dashboard → Worker → Triggers → Cron Triggers — verify the schedule is registered. If missing, the [triggers] block in wrangler.toml wasn't applied; redeploy. Also verify the expire handler is exported from src/index.ts — Cron dispatches via scheduled(event, env, ctx).
Body larger than expected rejected
Symptom: Upload of a 1 MB log fails with 413 Payload Too Large.
Fix: The Workers free plan caps request body at 100 KiB; the paid plan ($5/mo) raises it to 10 MiB. Either upgrade or lower MAX_BODY_BYTES to enforce the limit explicitly with a friendlier error message. Bodies over 10 MiB can't go through Workers — direct R2 upload via presigned URL is the workaround.
D1 row reads quota exhausted
Symptom: High-traffic listing pages return D1_ERROR: quota exceeded.
Fix: The free D1 plan caps at 5M row reads/day; paid caps at 25B then bills per-million. Two fixes: (1) Cache the public listing response at the edge with Cache-Control: s-maxage=60 — most visitors never hit D1. (2) Move the public listing off D1 entirely and pre-generate it into an R2 object that the cron updates hourly.
Support
- Email: support@voltrus.id
- Product page: /pastehere/
When reporting a deployment issue, include the redacted wrangler.toml, the output of npx wrangler --version, your Cloudflare plan tier (Free / Workers Paid), and the relevant excerpt from npx wrangler tail logs.