Repository files navigation

Bin 21

Modern No-Strings-Attached Pastebin

Share code and text instantly. No account required. Privacy-first.

MIT LicenseStarsNext.js 16TypeScript


Features

  • 150+ Languages - Syntax highlighting powered by Shiki (same engine as VS Code)
  • Markdown Rendering - Full GitHub Flavored Markdown with code blocks
  • Client-Side Encryption - AES-256-GCM encryption. Your password never leaves the browser
  • Burn After Reading - Self-destructing pastes after first view
  • Expiration Options - Never, 10 min, 1 hour, 1 day, 1 week, 1 month
  • Zero Authentication - No account, no email, no friction
  • Dark Mode - Pure black OLED-optimized dark theme
  • Bot Protection - Honeypot fields, time-based detection, JS challenges
  • Rate Limiting - In-memory sliding window rate limiter
  • Admin Analytics - Optional /admin dashboard (paste/view totals, language mix, size distribution, activity over time). Disabled unless ADMIN_TOKEN is set
  • Open Source - MIT licensed. Self-host it, fork it, contribute to it

Tech Stack

LayerTechnology
FrameworkNext.js 16.3 (App Router, Server Actions, Cache Components)
LanguageTypeScript 5 (strict mode)
StylingTailwind CSS 4
DatabaseSQLite via Drizzle ORM + better-sqlite3
Object StorageCloudflare R2 (S3-compatible)
Syntax HighlightingShiki (150+ languages)
MarkdownReact Markdown + remark-gfm
EncryptionWeb Crypto API (AES-256-GCM + PBKDF2)
Rate LimitingIn-memory sliding window (single-instance by design)
ChartsRecharts + Evil Charts (admin analytics)
ValidationZod
DeploymentRailway — one service + a volume

Architecture

bin-21/
app/ # Next.js App Router pages
[id]/ # View paste page + raw endpoint
admin/ # Analytics dashboard (env-gated)
layout.tsx # Root layout with theme provider
page.tsx # Home / create paste
server/
actions/ # Server Actions (entry points, Zod validation)
services/ # Core business logic (DB, R2, stats)
lib/
db/ # Drizzle schema, SQLite client, migrations
shiki.ts # Syntax highlighter (bounded language cache)
languages.ts # 150+ language definitions
rate-limit.ts # In-memory rate limiter
janitor.ts # Reclaims expired pastes
admin-auth.ts # Admin token verification
bot-detection.ts # Bot detection utilities
components/
ui/ # Vendored chart primitives
admin/ # Analytics dashboard components
types/ # Shared TypeScript types
instrumentation.ts # Server startup hook (starts the janitor)
proxy.ts # Next.js 16 proxy (rate limiting, admin gate)

Data flow: Client form -> Server Action (validates with Zod) -> Service (uploads content to R2, saves metadata to SQLite) -> Returns paste ID -> Redirect to view page.

Paste content is stored in Cloudflare R2, never in the database. Encrypted pastes use client-side AES-256-GCM - the password never reaches the server.

Single-instance by design. The SQLite file lives on a mounted volume, and Railway does not allow replicas on a service with a volume. That is what makes in-memory rate limiting and the in-process janitor correct. The trade-offs: no horizontal scaling, and brief downtime on redeploy.

Getting Started

Prerequisites

  • Node.js 20+
  • Cloudflare R2 bucket

No database server to run — SQLite is a file, created automatically on first boot.

Setup

git clone https://github.com/t21dev/bin-21.git
cd bin-21
npm install
cp .env.example .env

Edit .env with your credentials:

# Optional locally — defaults to ./.data/bin21.dbDATABASE_PATH=./.data/bin21.dbR2_ACCOUNT_ID=your-account-idR2_ACCESS_KEY_ID=your-access-keyR2_SECRET_ACCESS_KEY=your-secret-keyR2_BUCKET_NAME=bin21-pastesNEXT_PUBLIC_APP_URL=http://localhost:3000

Start the dev server — migrations run automatically on boot:

npm run dev

Open http://localhost:3000.

Leave JANITOR_ENABLED unset locally. It deletes expired pastes and their R2 objects, so a dev machine pointed at a production bucket would reclaim live storage.

Commands

npm run dev # Start dev server
npm run build # Production build
npm start # Start production server
npm run lint # ESLint
npm run db:push # Push schema to database
npm run db:generate # Generate Drizzle migrations
npm run db:studio # Open Drizzle Studio

Deployment

Self-hosting? See the full Self-Hosting Guide for Docker Compose, MinIO, reverse proxy, and more.

Railway (Recommended)

  1. Create a new project on Railway
  2. Connect your GitHub repo
  3. Attach a volume to the service, mounted at /data
  4. Set environment variables from .env.example — in particular DATABASE_PATH=/data/bin21.db and JANITOR_ENABLED=true
  5. Enable automated volume backups. The volume is now the only copy of your paste metadata; there is no managed database to fall back on
  6. Deploy

No database or cache service is needed — one service plus a volume.

Docker

FROM node:20-alpine AS base
WORKDIR /app
FROM base AS deps
COPY package*.json ./
RUN npm ci
FROM base AS builder
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN npm run build
FROM base AS runner
ENV NODE_ENV=production
COPY --from=builder /app/.next/standalone ./
COPY --from=builder /app/.next/static ./.next/static
COPY --from=builder /app/public ./public
EXPOSE 3000
CMD ["node", "server.js"]

Privacy & Security

  • Client-side encryption: Password-protected pastes are encrypted in your browser using AES-256-GCM with PBKDF2 key derivation (100,000 iterations). The plaintext password never reaches the server.
  • No tracking: No analytics, no cookies (beyond theme preference), no user accounts.
  • IP hashing: IPs are salted and hashed for rate limiting. Raw IPs are never stored.
  • Burn after reading: Paste is permanently deleted from both database and R2 after first view.
  • Expiration: Expired pastes are automatically cleaned up.

Rate Limits

ActionLimitWindow
Create paste10 requests1 minute
View paste60 requests1 minute
Failed password5 attempts5 minutes

Contributing

Contributions are welcome. Please open an issue first to discuss what you'd like to change.

  1. Fork the repo
  2. Create your branch (git checkout -b feature/my-feature)
  3. Commit your changes
  4. Push to the branch
  5. Open a Pull Request

License

MIT - do whatever you want with it.


Built by T21 Dev

About

Modern No-Strings-Attached Pastebin

Topics

Resources

Stars

4 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

Repository files navigation

Bin 21

Modern No-Strings-Attached Pastebin

Share code and text instantly. No account required. Privacy-first.

MIT LicenseStarsNext.js 16TypeScript


Features

  • 150+ Languages - Syntax highlighting powered by Shiki (same engine as VS Code)
  • Markdown Rendering - Full GitHub Flavored Markdown with code blocks
  • Client-Side Encryption - AES-256-GCM encryption. Your password never leaves the browser
  • Burn After Reading - Self-destructing pastes after first view
  • Expiration Options - Never, 10 min, 1 hour, 1 day, 1 week, 1 month
  • Zero Authentication - No account, no email, no friction
  • Dark Mode - Pure black OLED-optimized dark theme
  • Bot Protection - Honeypot fields, time-based detection, JS challenges
  • Rate Limiting - In-memory sliding window rate limiter
  • Admin Analytics - Optional /admin dashboard (paste/view totals, language mix, size distribution, activity over time). Disabled unless ADMIN_TOKEN is set
  • Open Source - MIT licensed. Self-host it, fork it, contribute to it

Tech Stack

LayerTechnology
FrameworkNext.js 16.3 (App Router, Server Actions, Cache Components)
LanguageTypeScript 5 (strict mode)
StylingTailwind CSS 4
DatabaseSQLite via Drizzle ORM + better-sqlite3
Object StorageCloudflare R2 (S3-compatible)
Syntax HighlightingShiki (150+ languages)
MarkdownReact Markdown + remark-gfm
EncryptionWeb Crypto API (AES-256-GCM + PBKDF2)
Rate LimitingIn-memory sliding window (single-instance by design)
ChartsRecharts + Evil Charts (admin analytics)
ValidationZod
DeploymentRailway — one service + a volume

Architecture

bin-21/
app/ # Next.js App Router pages
[id]/ # View paste page + raw endpoint
admin/ # Analytics dashboard (env-gated)
layout.tsx # Root layout with theme provider
page.tsx # Home / create paste
server/
actions/ # Server Actions (entry points, Zod validation)
services/ # Core business logic (DB, R2, stats)
lib/
db/ # Drizzle schema, SQLite client, migrations
shiki.ts # Syntax highlighter (bounded language cache)
languages.ts # 150+ language definitions
rate-limit.ts # In-memory rate limiter
janitor.ts # Reclaims expired pastes
admin-auth.ts # Admin token verification
bot-detection.ts # Bot detection utilities
components/
ui/ # Vendored chart primitives
admin/ # Analytics dashboard components
types/ # Shared TypeScript types
instrumentation.ts # Server startup hook (starts the janitor)
proxy.ts # Next.js 16 proxy (rate limiting, admin gate)

Data flow: Client form -> Server Action (validates with Zod) -> Service (uploads content to R2, saves metadata to SQLite) -> Returns paste ID -> Redirect to view page.

Paste content is stored in Cloudflare R2, never in the database. Encrypted pastes use client-side AES-256-GCM - the password never reaches the server.

Single-instance by design. The SQLite file lives on a mounted volume, and Railway does not allow replicas on a service with a volume. That is what makes in-memory rate limiting and the in-process janitor correct. The trade-offs: no horizontal scaling, and brief downtime on redeploy.

Getting Started

Prerequisites

  • Node.js 20+
  • Cloudflare R2 bucket

No database server to run — SQLite is a file, created automatically on first boot.

Setup

git clone https://github.com/t21dev/bin-21.git
cd bin-21
npm install
cp .env.example .env

Edit .env with your credentials:

# Optional locally — defaults to ./.data/bin21.dbDATABASE_PATH=./.data/bin21.dbR2_ACCOUNT_ID=your-account-idR2_ACCESS_KEY_ID=your-access-keyR2_SECRET_ACCESS_KEY=your-secret-keyR2_BUCKET_NAME=bin21-pastesNEXT_PUBLIC_APP_URL=http://localhost:3000

Start the dev server — migrations run automatically on boot:

npm run dev

Open http://localhost:3000.

Leave JANITOR_ENABLED unset locally. It deletes expired pastes and their R2 objects, so a dev machine pointed at a production bucket would reclaim live storage.

Commands

npm run dev # Start dev server
npm run build # Production build
npm start # Start production server
npm run lint # ESLint
npm run db:push # Push schema to database
npm run db:generate # Generate Drizzle migrations
npm run db:studio # Open Drizzle Studio

Deployment

Self-hosting? See the full Self-Hosting Guide for Docker Compose, MinIO, reverse proxy, and more.

Railway (Recommended)

  1. Create a new project on Railway
  2. Connect your GitHub repo
  3. Attach a volume to the service, mounted at /data
  4. Set environment variables from .env.example — in particular DATABASE_PATH=/data/bin21.db and JANITOR_ENABLED=true
  5. Enable automated volume backups. The volume is now the only copy of your paste metadata; there is no managed database to fall back on
  6. Deploy

No database or cache service is needed — one service plus a volume.

Docker

FROM node:20-alpine AS base
WORKDIR /app
FROM base AS deps
COPY package*.json ./
RUN npm ci
FROM base AS builder
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN npm run build
FROM base AS runner
ENV NODE_ENV=production
COPY --from=builder /app/.next/standalone ./
COPY --from=builder /app/.next/static ./.next/static
COPY --from=builder /app/public ./public
EXPOSE 3000
CMD ["node", "server.js"]

Privacy & Security

  • Client-side encryption: Password-protected pastes are encrypted in your browser using AES-256-GCM with PBKDF2 key derivation (100,000 iterations). The plaintext password never reaches the server.
  • No tracking: No analytics, no cookies (beyond theme preference), no user accounts.
  • IP hashing: IPs are salted and hashed for rate limiting. Raw IPs are never stored.
  • Burn after reading: Paste is permanently deleted from both database and R2 after first view.
  • Expiration: Expired pastes are automatically cleaned up.

Rate Limits

ActionLimitWindow
Create paste10 requests1 minute
View paste60 requests1 minute
Failed password5 attempts5 minutes

Contributing

Contributions are welcome. Please open an issue first to discuss what you'd like to change.

  1. Fork the repo
  2. Create your branch (git checkout -b feature/my-feature)
  3. Commit your changes
  4. Push to the branch
  5. Open a Pull Request

License

MIT - do whatever you want with it.


Built by T21 Dev

About

Modern No-Strings-Attached Pastebin

Topics

Resources

Stars

4 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

Bin 21

Modern No-Strings-Attached Pastebin

Share code and text instantly. No account required. Privacy-first.

MIT LicenseStarsNext.js 16TypeScript


Features

  • 150+ Languages - Syntax highlighting powered by Shiki (same engine as VS Code)
  • Markdown Rendering - Full GitHub Flavored Markdown with code blocks
  • Client-Side Encryption - AES-256-GCM encryption. Your password never leaves the browser
  • Burn After Reading - Self-destructing pastes after first view
  • Expiration Options - Never, 10 min, 1 hour, 1 day, 1 week, 1 month
  • Zero Authentication - No account, no email, no friction
  • Dark Mode - Pure black OLED-optimized dark theme
  • Bot Protection - Honeypot fields, time-based detection, JS challenges
  • Rate Limiting - In-memory sliding window rate limiter
  • Admin Analytics - Optional /admin dashboard (paste/view totals, language mix, size distribution, activity over time). Disabled unless ADMIN_TOKEN is set
  • Open Source - MIT licensed. Self-host it, fork it, contribute to it

Tech Stack

LayerTechnology
FrameworkNext.js 16.3 (App Router, Server Actions, Cache Components)
LanguageTypeScript 5 (strict mode)
StylingTailwind CSS 4
DatabaseSQLite via Drizzle ORM + better-sqlite3
Object StorageCloudflare R2 (S3-compatible)
Syntax HighlightingShiki (150+ languages)
MarkdownReact Markdown + remark-gfm
EncryptionWeb Crypto API (AES-256-GCM + PBKDF2)
Rate LimitingIn-memory sliding window (single-instance by design)
ChartsRecharts + Evil Charts (admin analytics)
ValidationZod
DeploymentRailway — one service + a volume

Architecture

bin-21/
app/ # Next.js App Router pages
[id]/ # View paste page + raw endpoint
admin/ # Analytics dashboard (env-gated)
layout.tsx # Root layout with theme provider
page.tsx # Home / create paste
server/
actions/ # Server Actions (entry points, Zod validation)
services/ # Core business logic (DB, R2, stats)
lib/
db/ # Drizzle schema, SQLite client, migrations
shiki.ts # Syntax highlighter (bounded language cache)
languages.ts # 150+ language definitions
rate-limit.ts # In-memory rate limiter
janitor.ts # Reclaims expired pastes
admin-auth.ts # Admin token verification
bot-detection.ts # Bot detection utilities
components/
ui/ # Vendored chart primitives
admin/ # Analytics dashboard components
types/ # Shared TypeScript types
instrumentation.ts # Server startup hook (starts the janitor)
proxy.ts # Next.js 16 proxy (rate limiting, admin gate)

Data flow: Client form -> Server Action (validates with Zod) -> Service (uploads content to R2, saves metadata to SQLite) -> Returns paste ID -> Redirect to view page.

Paste content is stored in Cloudflare R2, never in the database. Encrypted pastes use client-side AES-256-GCM - the password never reaches the server.

Single-instance by design. The SQLite file lives on a mounted volume, and Railway does not allow replicas on a service with a volume. That is what makes in-memory rate limiting and the in-process janitor correct. The trade-offs: no horizontal scaling, and brief downtime on redeploy.

Getting Started

Prerequisites

  • Node.js 20+
  • Cloudflare R2 bucket

No database server to run — SQLite is a file, created automatically on first boot.

Setup

git clone https://github.com/t21dev/bin-21.git
cd bin-21
npm install
cp .env.example .env

Edit .env with your credentials:

# Optional locally — defaults to ./.data/bin21.dbDATABASE_PATH=./.data/bin21.dbR2_ACCOUNT_ID=your-account-idR2_ACCESS_KEY_ID=your-access-keyR2_SECRET_ACCESS_KEY=your-secret-keyR2_BUCKET_NAME=bin21-pastesNEXT_PUBLIC_APP_URL=http://localhost:3000

Start the dev server — migrations run automatically on boot:

npm run dev

Open http://localhost:3000.

Leave JANITOR_ENABLED unset locally. It deletes expired pastes and their R2 objects, so a dev machine pointed at a production bucket would reclaim live storage.

Commands

npm run dev # Start dev server
npm run build # Production build
npm start # Start production server
npm run lint # ESLint
npm run db:push # Push schema to database
npm run db:generate # Generate Drizzle migrations
npm run db:studio # Open Drizzle Studio

Deployment

Self-hosting? See the full Self-Hosting Guide for Docker Compose, MinIO, reverse proxy, and more.

Railway (Recommended)

  1. Create a new project on Railway
  2. Connect your GitHub repo
  3. Attach a volume to the service, mounted at /data
  4. Set environment variables from .env.example — in particular DATABASE_PATH=/data/bin21.db and JANITOR_ENABLED=true
  5. Enable automated volume backups. The volume is now the only copy of your paste metadata; there is no managed database to fall back on
  6. Deploy

No database or cache service is needed — one service plus a volume.

Docker

FROM node:20-alpine AS base
WORKDIR /app
FROM base AS deps
COPY package*.json ./
RUN npm ci
FROM base AS builder
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN npm run build
FROM base AS runner
ENV NODE_ENV=production
COPY --from=builder /app/.next/standalone ./
COPY --from=builder /app/.next/static ./.next/static
COPY --from=builder /app/public ./public
EXPOSE 3000
CMD ["node", "server.js"]

Privacy & Security

  • Client-side encryption: Password-protected pastes are encrypted in your browser using AES-256-GCM with PBKDF2 key derivation (100,000 iterations). The plaintext password never reaches the server.
  • No tracking: No analytics, no cookies (beyond theme preference), no user accounts.
  • IP hashing: IPs are salted and hashed for rate limiting. Raw IPs are never stored.
  • Burn after reading: Paste is permanently deleted from both database and R2 after first view.
  • Expiration: Expired pastes are automatically cleaned up.

Rate Limits

ActionLimitWindow
Create paste10 requests1 minute
View paste60 requests1 minute
Failed password5 attempts5 minutes

Contributing

Contributions are welcome. Please open an issue first to discuss what you'd like to change.

  1. Fork the repo
  2. Create your branch (git checkout -b feature/my-feature)
  3. Commit your changes
  4. Push to the branch
  5. Open a Pull Request

License

MIT - do whatever you want with it.


Built by T21 Dev

About

Modern No-Strings-Attached Pastebin

Topics

Resources

Stars

4 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

Bin 21

Modern No-Strings-Attached Pastebin

Share code and text instantly. No account required. Privacy-first.

MIT LicenseStarsNext.js 16TypeScript


Features

  • 150+ Languages - Syntax highlighting powered by Shiki (same engine as VS Code)
  • Markdown Rendering - Full GitHub Flavored Markdown with code blocks
  • Client-Side Encryption - AES-256-GCM encryption. Your password never leaves the browser
  • Burn After Reading - Self-destructing pastes after first view
  • Expiration Options - Never, 10 min, 1 hour, 1 day, 1 week, 1 month
  • Zero Authentication - No account, no email, no friction
  • Dark Mode - Pure black OLED-optimized dark theme
  • Bot Protection - Honeypot fields, time-based detection, JS challenges
  • Rate Limiting - In-memory sliding window rate limiter
  • Admin Analytics - Optional /admin dashboard (paste/view totals, language mix, size distribution, activity over time). Disabled unless ADMIN_TOKEN is set
  • Open Source - MIT licensed. Self-host it, fork it, contribute to it

Tech Stack

LayerTechnology
FrameworkNext.js 16.3 (App Router, Server Actions, Cache Components)
LanguageTypeScript 5 (strict mode)
StylingTailwind CSS 4
DatabaseSQLite via Drizzle ORM + better-sqlite3
Object StorageCloudflare R2 (S3-compatible)
Syntax HighlightingShiki (150+ languages)
MarkdownReact Markdown + remark-gfm
EncryptionWeb Crypto API (AES-256-GCM + PBKDF2)
Rate LimitingIn-memory sliding window (single-instance by design)
ChartsRecharts + Evil Charts (admin analytics)
ValidationZod
DeploymentRailway — one service + a volume

Architecture

bin-21/
app/ # Next.js App Router pages
[id]/ # View paste page + raw endpoint
admin/ # Analytics dashboard (env-gated)
layout.tsx # Root layout with theme provider
page.tsx # Home / create paste
server/
actions/ # Server Actions (entry points, Zod validation)
services/ # Core business logic (DB, R2, stats)
lib/
db/ # Drizzle schema, SQLite client, migrations
shiki.ts # Syntax highlighter (bounded language cache)
languages.ts # 150+ language definitions
rate-limit.ts # In-memory rate limiter
janitor.ts # Reclaims expired pastes
admin-auth.ts # Admin token verification
bot-detection.ts # Bot detection utilities
components/
ui/ # Vendored chart primitives
admin/ # Analytics dashboard components
types/ # Shared TypeScript types
instrumentation.ts # Server startup hook (starts the janitor)
proxy.ts # Next.js 16 proxy (rate limiting, admin gate)

Data flow: Client form -> Server Action (validates with Zod) -> Service (uploads content to R2, saves metadata to SQLite) -> Returns paste ID -> Redirect to view page.

Paste content is stored in Cloudflare R2, never in the database. Encrypted pastes use client-side AES-256-GCM - the password never reaches the server.

Single-instance by design. The SQLite file lives on a mounted volume, and Railway does not allow replicas on a service with a volume. That is what makes in-memory rate limiting and the in-process janitor correct. The trade-offs: no horizontal scaling, and brief downtime on redeploy.

Getting Started

Prerequisites

  • Node.js 20+
  • Cloudflare R2 bucket

No database server to run — SQLite is a file, created automatically on first boot.

Setup

git clone https://github.com/t21dev/bin-21.git
cd bin-21
npm install
cp .env.example .env

Edit .env with your credentials:

# Optional locally — defaults to ./.data/bin21.dbDATABASE_PATH=./.data/bin21.dbR2_ACCOUNT_ID=your-account-idR2_ACCESS_KEY_ID=your-access-keyR2_SECRET_ACCESS_KEY=your-secret-keyR2_BUCKET_NAME=bin21-pastesNEXT_PUBLIC_APP_URL=http://localhost:3000

Start the dev server — migrations run automatically on boot:

npm run dev

Open http://localhost:3000.

Leave JANITOR_ENABLED unset locally. It deletes expired pastes and their R2 objects, so a dev machine pointed at a production bucket would reclaim live storage.

Commands

npm run dev # Start dev server
npm run build # Production build
npm start # Start production server
npm run lint # ESLint
npm run db:push # Push schema to database
npm run db:generate # Generate Drizzle migrations
npm run db:studio # Open Drizzle Studio

Deployment

Self-hosting? See the full Self-Hosting Guide for Docker Compose, MinIO, reverse proxy, and more.

Railway (Recommended)

  1. Create a new project on Railway
  2. Connect your GitHub repo
  3. Attach a volume to the service, mounted at /data
  4. Set environment variables from .env.example — in particular DATABASE_PATH=/data/bin21.db and JANITOR_ENABLED=true
  5. Enable automated volume backups. The volume is now the only copy of your paste metadata; there is no managed database to fall back on
  6. Deploy

No database or cache service is needed — one service plus a volume.

Docker

FROM node:20-alpine AS base
WORKDIR /app
FROM base AS deps
COPY package*.json ./
RUN npm ci
FROM base AS builder
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN npm run build
FROM base AS runner
ENV NODE_ENV=production
COPY --from=builder /app/.next/standalone ./
COPY --from=builder /app/.next/static ./.next/static
COPY --from=builder /app/public ./public
EXPOSE 3000
CMD ["node", "server.js"]

Privacy & Security

  • Client-side encryption: Password-protected pastes are encrypted in your browser using AES-256-GCM with PBKDF2 key derivation (100,000 iterations). The plaintext password never reaches the server.
  • No tracking: No analytics, no cookies (beyond theme preference), no user accounts.
  • IP hashing: IPs are salted and hashed for rate limiting. Raw IPs are never stored.
  • Burn after reading: Paste is permanently deleted from both database and R2 after first view.
  • Expiration: Expired pastes are automatically cleaned up.

Rate Limits

ActionLimitWindow
Create paste10 requests1 minute
View paste60 requests1 minute
Failed password5 attempts5 minutes

Contributing

Contributions are welcome. Please open an issue first to discuss what you'd like to change.

  1. Fork the repo
  2. Create your branch (git checkout -b feature/my-feature)
  3. Commit your changes
  4. Push to the branch
  5. Open a Pull Request

License

MIT - do whatever you want with it.


Built by T21 Dev

About

Modern No-Strings-Attached Pastebin

Topics

Resources

Stars

4 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

Repository files navigation

Bin 21

Modern No-Strings-Attached Pastebin

Share code and text instantly. No account required. Privacy-first.

MIT LicenseStarsNext.js 16TypeScript


Features

  • 150+ Languages - Syntax highlighting powered by Shiki (same engine as VS Code)
  • Markdown Rendering - Full GitHub Flavored Markdown with code blocks
  • Client-Side Encryption - AES-256-GCM encryption. Your password never leaves the browser
  • Burn After Reading - Self-destructing pastes after first view
  • Expiration Options - Never, 10 min, 1 hour, 1 day, 1 week, 1 month
  • Zero Authentication - No account, no email, no friction
  • Dark Mode - Pure black OLED-optimized dark theme
  • Bot Protection - Honeypot fields, time-based detection, JS challenges
  • Rate Limiting - In-memory sliding window rate limiter
  • Admin Analytics - Optional /admin dashboard (paste/view totals, language mix, size distribution, activity over time). Disabled unless ADMIN_TOKEN is set
  • Open Source - MIT licensed. Self-host it, fork it, contribute to it

Tech Stack

LayerTechnology
FrameworkNext.js 16.3 (App Router, Server Actions, Cache Components)
LanguageTypeScript 5 (strict mode)
StylingTailwind CSS 4
DatabaseSQLite via Drizzle ORM + better-sqlite3
Object StorageCloudflare R2 (S3-compatible)
Syntax HighlightingShiki (150+ languages)
MarkdownReact Markdown + remark-gfm
EncryptionWeb Crypto API (AES-256-GCM + PBKDF2)
Rate LimitingIn-memory sliding window (single-instance by design)
ChartsRecharts + Evil Charts (admin analytics)
ValidationZod
DeploymentRailway — one service + a volume

Architecture

bin-21/
app/ # Next.js App Router pages
[id]/ # View paste page + raw endpoint
admin/ # Analytics dashboard (env-gated)
layout.tsx # Root layout with theme provider
page.tsx # Home / create paste
server/
actions/ # Server Actions (entry points, Zod validation)
services/ # Core business logic (DB, R2, stats)
lib/
db/ # Drizzle schema, SQLite client, migrations
shiki.ts # Syntax highlighter (bounded language cache)
languages.ts # 150+ language definitions
rate-limit.ts # In-memory rate limiter
janitor.ts # Reclaims expired pastes
admin-auth.ts # Admin token verification
bot-detection.ts # Bot detection utilities
components/
ui/ # Vendored chart primitives
admin/ # Analytics dashboard components
types/ # Shared TypeScript types
instrumentation.ts # Server startup hook (starts the janitor)
proxy.ts # Next.js 16 proxy (rate limiting, admin gate)

Data flow: Client form -> Server Action (validates with Zod) -> Service (uploads content to R2, saves metadata to SQLite) -> Returns paste ID -> Redirect to view page.

Paste content is stored in Cloudflare R2, never in the database. Encrypted pastes use client-side AES-256-GCM - the password never reaches the server.

Single-instance by design. The SQLite file lives on a mounted volume, and Railway does not allow replicas on a service with a volume. That is what makes in-memory rate limiting and the in-process janitor correct. The trade-offs: no horizontal scaling, and brief downtime on redeploy.

Getting Started

Prerequisites

  • Node.js 20+
  • Cloudflare R2 bucket

No database server to run — SQLite is a file, created automatically on first boot.

Setup

git clone https://github.com/t21dev/bin-21.git
cd bin-21
npm install
cp .env.example .env

Edit .env with your credentials:

# Optional locally — defaults to ./.data/bin21.dbDATABASE_PATH=./.data/bin21.dbR2_ACCOUNT_ID=your-account-idR2_ACCESS_KEY_ID=your-access-keyR2_SECRET_ACCESS_KEY=your-secret-keyR2_BUCKET_NAME=bin21-pastesNEXT_PUBLIC_APP_URL=http://localhost:3000

Start the dev server — migrations run automatically on boot:

npm run dev

Open http://localhost:3000.

Leave JANITOR_ENABLED unset locally. It deletes expired pastes and their R2 objects, so a dev machine pointed at a production bucket would reclaim live storage.

Commands

npm run dev # Start dev server
npm run build # Production build
npm start # Start production server
npm run lint # ESLint
npm run db:push # Push schema to database
npm run db:generate # Generate Drizzle migrations
npm run db:studio # Open Drizzle Studio

Deployment

Self-hosting? See the full Self-Hosting Guide for Docker Compose, MinIO, reverse proxy, and more.

Railway (Recommended)

  1. Create a new project on Railway
  2. Connect your GitHub repo
  3. Attach a volume to the service, mounted at /data
  4. Set environment variables from .env.example — in particular DATABASE_PATH=/data/bin21.db and JANITOR_ENABLED=true
  5. Enable automated volume backups. The volume is now the only copy of your paste metadata; there is no managed database to fall back on
  6. Deploy

No database or cache service is needed — one service plus a volume.

Docker

FROM node:20-alpine AS base
WORKDIR /app
FROM base AS deps
COPY package*.json ./
RUN npm ci
FROM base AS builder
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN npm run build
FROM base AS runner
ENV NODE_ENV=production
COPY --from=builder /app/.next/standalone ./
COPY --from=builder /app/.next/static ./.next/static
COPY --from=builder /app/public ./public
EXPOSE 3000
CMD ["node", "server.js"]

Privacy & Security

  • Client-side encryption: Password-protected pastes are encrypted in your browser using AES-256-GCM with PBKDF2 key derivation (100,000 iterations). The plaintext password never reaches the server.
  • No tracking: No analytics, no cookies (beyond theme preference), no user accounts.
  • IP hashing: IPs are salted and hashed for rate limiting. Raw IPs are never stored.
  • Burn after reading: Paste is permanently deleted from both database and R2 after first view.
  • Expiration: Expired pastes are automatically cleaned up.

Rate Limits

ActionLimitWindow
Create paste10 requests1 minute
View paste60 requests1 minute
Failed password5 attempts5 minutes

Contributing

Contributions are welcome. Please open an issue first to discuss what you'd like to change.

  1. Fork the repo
  2. Create your branch (git checkout -b feature/my-feature)
  3. Commit your changes
  4. Push to the branch
  5. Open a Pull Request

License

MIT - do whatever you want with it.


Built by T21 Dev

About

Modern No-Strings-Attached Pastebin

Topics

Resources

Stars

4 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

Bin 21

Modern No-Strings-Attached Pastebin

Share code and text instantly. No account required. Privacy-first.

MIT LicenseStarsNext.js 16TypeScript


Features

  • 150+ Languages - Syntax highlighting powered by Shiki (same engine as VS Code)
  • Markdown Rendering - Full GitHub Flavored Markdown with code blocks
  • Client-Side Encryption - AES-256-GCM encryption. Your password never leaves the browser
  • Burn After Reading - Self-destructing pastes after first view
  • Expiration Options - Never, 10 min, 1 hour, 1 day, 1 week, 1 month
  • Zero Authentication - No account, no email, no friction
  • Dark Mode - Pure black OLED-optimized dark theme
  • Bot Protection - Honeypot fields, time-based detection, JS challenges
  • Rate Limiting - In-memory sliding window rate limiter
  • Admin Analytics - Optional /admin dashboard (paste/view totals, language mix, size distribution, activity over time). Disabled unless ADMIN_TOKEN is set
  • Open Source - MIT licensed. Self-host it, fork it, contribute to it

Tech Stack

LayerTechnology
FrameworkNext.js 16.3 (App Router, Server Actions, Cache Components)
LanguageTypeScript 5 (strict mode)
StylingTailwind CSS 4
DatabaseSQLite via Drizzle ORM + better-sqlite3
Object StorageCloudflare R2 (S3-compatible)
Syntax HighlightingShiki (150+ languages)
MarkdownReact Markdown + remark-gfm
EncryptionWeb Crypto API (AES-256-GCM + PBKDF2)
Rate LimitingIn-memory sliding window (single-instance by design)
ChartsRecharts + Evil Charts (admin analytics)
ValidationZod
DeploymentRailway — one service + a volume

Architecture

bin-21/
app/ # Next.js App Router pages
[id]/ # View paste page + raw endpoint
admin/ # Analytics dashboard (env-gated)
layout.tsx # Root layout with theme provider
page.tsx # Home / create paste
server/
actions/ # Server Actions (entry points, Zod validation)
services/ # Core business logic (DB, R2, stats)
lib/
db/ # Drizzle schema, SQLite client, migrations
shiki.ts # Syntax highlighter (bounded language cache)
languages.ts # 150+ language definitions
rate-limit.ts # In-memory rate limiter
janitor.ts # Reclaims expired pastes
admin-auth.ts # Admin token verification
bot-detection.ts # Bot detection utilities
components/
ui/ # Vendored chart primitives
admin/ # Analytics dashboard components
types/ # Shared TypeScript types
instrumentation.ts # Server startup hook (starts the janitor)
proxy.ts # Next.js 16 proxy (rate limiting, admin gate)

Data flow: Client form -> Server Action (validates with Zod) -> Service (uploads content to R2, saves metadata to SQLite) -> Returns paste ID -> Redirect to view page.

Paste content is stored in Cloudflare R2, never in the database. Encrypted pastes use client-side AES-256-GCM - the password never reaches the server.

Single-instance by design. The SQLite file lives on a mounted volume, and Railway does not allow replicas on a service with a volume. That is what makes in-memory rate limiting and the in-process janitor correct. The trade-offs: no horizontal scaling, and brief downtime on redeploy.

Getting Started

Prerequisites

  • Node.js 20+
  • Cloudflare R2 bucket

No database server to run — SQLite is a file, created automatically on first boot.

Setup

git clone https://github.com/t21dev/bin-21.git
cd bin-21
npm install
cp .env.example .env

Edit .env with your credentials:

# Optional locally — defaults to ./.data/bin21.dbDATABASE_PATH=./.data/bin21.dbR2_ACCOUNT_ID=your-account-idR2_ACCESS_KEY_ID=your-access-keyR2_SECRET_ACCESS_KEY=your-secret-keyR2_BUCKET_NAME=bin21-pastesNEXT_PUBLIC_APP_URL=http://localhost:3000

Start the dev server — migrations run automatically on boot:

npm run dev

Open http://localhost:3000.

Leave JANITOR_ENABLED unset locally. It deletes expired pastes and their R2 objects, so a dev machine pointed at a production bucket would reclaim live storage.

Commands

npm run dev # Start dev server
npm run build # Production build
npm start # Start production server
npm run lint # ESLint
npm run db:push # Push schema to database
npm run db:generate # Generate Drizzle migrations
npm run db:studio # Open Drizzle Studio

Deployment

Self-hosting? See the full Self-Hosting Guide for Docker Compose, MinIO, reverse proxy, and more.

Railway (Recommended)

  1. Create a new project on Railway
  2. Connect your GitHub repo
  3. Attach a volume to the service, mounted at /data
  4. Set environment variables from .env.example — in particular DATABASE_PATH=/data/bin21.db and JANITOR_ENABLED=true
  5. Enable automated volume backups. The volume is now the only copy of your paste metadata; there is no managed database to fall back on
  6. Deploy

No database or cache service is needed — one service plus a volume.

Docker

FROM node:20-alpine AS base
WORKDIR /app
FROM base AS deps
COPY package*.json ./
RUN npm ci
FROM base AS builder
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN npm run build
FROM base AS runner
ENV NODE_ENV=production
COPY --from=builder /app/.next/standalone ./
COPY --from=builder /app/.next/static ./.next/static
COPY --from=builder /app/public ./public
EXPOSE 3000
CMD ["node", "server.js"]

Privacy & Security

  • Client-side encryption: Password-protected pastes are encrypted in your browser using AES-256-GCM with PBKDF2 key derivation (100,000 iterations). The plaintext password never reaches the server.
  • No tracking: No analytics, no cookies (beyond theme preference), no user accounts.
  • IP hashing: IPs are salted and hashed for rate limiting. Raw IPs are never stored.
  • Burn after reading: Paste is permanently deleted from both database and R2 after first view.
  • Expiration: Expired pastes are automatically cleaned up.

Rate Limits

ActionLimitWindow
Create paste10 requests1 minute
View paste60 requests1 minute
Failed password5 attempts5 minutes

Contributing

Contributions are welcome. Please open an issue first to discuss what you'd like to change.

  1. Fork the repo
  2. Create your branch (git checkout -b feature/my-feature)
  3. Commit your changes
  4. Push to the branch
  5. Open a Pull Request

License

MIT - do whatever you want with it.


Built by T21 Dev

About

Modern No-Strings-Attached Pastebin

Topics

Resources

Stars

4 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

Bin 21

Modern No-Strings-Attached Pastebin

Share code and text instantly. No account required. Privacy-first.

MIT LicenseStarsNext.js 16TypeScript


Features

  • 150+ Languages - Syntax highlighting powered by Shiki (same engine as VS Code)
  • Markdown Rendering - Full GitHub Flavored Markdown with code blocks
  • Client-Side Encryption - AES-256-GCM encryption. Your password never leaves the browser
  • Burn After Reading - Self-destructing pastes after first view
  • Expiration Options - Never, 10 min, 1 hour, 1 day, 1 week, 1 month
  • Zero Authentication - No account, no email, no friction
  • Dark Mode - Pure black OLED-optimized dark theme
  • Bot Protection - Honeypot fields, time-based detection, JS challenges
  • Rate Limiting - In-memory sliding window rate limiter
  • Admin Analytics - Optional /admin dashboard (paste/view totals, language mix, size distribution, activity over time). Disabled unless ADMIN_TOKEN is set
  • Open Source - MIT licensed. Self-host it, fork it, contribute to it

Tech Stack

LayerTechnology
FrameworkNext.js 16.3 (App Router, Server Actions, Cache Components)
LanguageTypeScript 5 (strict mode)
StylingTailwind CSS 4
DatabaseSQLite via Drizzle ORM + better-sqlite3
Object StorageCloudflare R2 (S3-compatible)
Syntax HighlightingShiki (150+ languages)
MarkdownReact Markdown + remark-gfm
EncryptionWeb Crypto API (AES-256-GCM + PBKDF2)
Rate LimitingIn-memory sliding window (single-instance by design)
ChartsRecharts + Evil Charts (admin analytics)
ValidationZod
DeploymentRailway — one service + a volume

Architecture

bin-21/
app/ # Next.js App Router pages
[id]/ # View paste page + raw endpoint
admin/ # Analytics dashboard (env-gated)
layout.tsx # Root layout with theme provider
page.tsx # Home / create paste
server/
actions/ # Server Actions (entry points, Zod validation)
services/ # Core business logic (DB, R2, stats)
lib/
db/ # Drizzle schema, SQLite client, migrations
shiki.ts # Syntax highlighter (bounded language cache)
languages.ts # 150+ language definitions
rate-limit.ts # In-memory rate limiter
janitor.ts # Reclaims expired pastes
admin-auth.ts # Admin token verification
bot-detection.ts # Bot detection utilities
components/
ui/ # Vendored chart primitives
admin/ # Analytics dashboard components
types/ # Shared TypeScript types
instrumentation.ts # Server startup hook (starts the janitor)
proxy.ts # Next.js 16 proxy (rate limiting, admin gate)

Data flow: Client form -> Server Action (validates with Zod) -> Service (uploads content to R2, saves metadata to SQLite) -> Returns paste ID -> Redirect to view page.

Paste content is stored in Cloudflare R2, never in the database. Encrypted pastes use client-side AES-256-GCM - the password never reaches the server.

Single-instance by design. The SQLite file lives on a mounted volume, and Railway does not allow replicas on a service with a volume. That is what makes in-memory rate limiting and the in-process janitor correct. The trade-offs: no horizontal scaling, and brief downtime on redeploy.

Getting Started

Prerequisites

  • Node.js 20+
  • Cloudflare R2 bucket

No database server to run — SQLite is a file, created automatically on first boot.

Setup

git clone https://github.com/t21dev/bin-21.git
cd bin-21
npm install
cp .env.example .env

Edit .env with your credentials:

# Optional locally — defaults to ./.data/bin21.dbDATABASE_PATH=./.data/bin21.dbR2_ACCOUNT_ID=your-account-idR2_ACCESS_KEY_ID=your-access-keyR2_SECRET_ACCESS_KEY=your-secret-keyR2_BUCKET_NAME=bin21-pastesNEXT_PUBLIC_APP_URL=http://localhost:3000

Start the dev server — migrations run automatically on boot:

npm run dev

Open http://localhost:3000.

Leave JANITOR_ENABLED unset locally. It deletes expired pastes and their R2 objects, so a dev machine pointed at a production bucket would reclaim live storage.

Commands

npm run dev # Start dev server
npm run build # Production build
npm start # Start production server
npm run lint # ESLint
npm run db:push # Push schema to database
npm run db:generate # Generate Drizzle migrations
npm run db:studio # Open Drizzle Studio

Deployment

Self-hosting? See the full Self-Hosting Guide for Docker Compose, MinIO, reverse proxy, and more.

Railway (Recommended)

  1. Create a new project on Railway
  2. Connect your GitHub repo
  3. Attach a volume to the service, mounted at /data
  4. Set environment variables from .env.example — in particular DATABASE_PATH=/data/bin21.db and JANITOR_ENABLED=true
  5. Enable automated volume backups. The volume is now the only copy of your paste metadata; there is no managed database to fall back on
  6. Deploy

No database or cache service is needed — one service plus a volume.

Docker

FROM node:20-alpine AS base
WORKDIR /app
FROM base AS deps
COPY package*.json ./
RUN npm ci
FROM base AS builder
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN npm run build
FROM base AS runner
ENV NODE_ENV=production
COPY --from=builder /app/.next/standalone ./
COPY --from=builder /app/.next/static ./.next/static
COPY --from=builder /app/public ./public
EXPOSE 3000
CMD ["node", "server.js"]

Privacy & Security

  • Client-side encryption: Password-protected pastes are encrypted in your browser using AES-256-GCM with PBKDF2 key derivation (100,000 iterations). The plaintext password never reaches the server.
  • No tracking: No analytics, no cookies (beyond theme preference), no user accounts.
  • IP hashing: IPs are salted and hashed for rate limiting. Raw IPs are never stored.
  • Burn after reading: Paste is permanently deleted from both database and R2 after first view.
  • Expiration: Expired pastes are automatically cleaned up.

Rate Limits

ActionLimitWindow
Create paste10 requests1 minute
View paste60 requests1 minute
Failed password5 attempts5 minutes

Contributing

Contributions are welcome. Please open an issue first to discuss what you'd like to change.

  1. Fork the repo
  2. Create your branch (git checkout -b feature/my-feature)
  3. Commit your changes
  4. Push to the branch
  5. Open a Pull Request

License

MIT - do whatever you want with it.


Built by T21 Dev

About

Modern No-Strings-Attached Pastebin

Topics

Resources

Stars

4 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

Repository files navigation

Bin 21

Modern No-Strings-Attached Pastebin

Share code and text instantly. No account required. Privacy-first.

MIT LicenseStarsNext.js 16TypeScript


Features

  • 150+ Languages - Syntax highlighting powered by Shiki (same engine as VS Code)
  • Markdown Rendering - Full GitHub Flavored Markdown with code blocks
  • Client-Side Encryption - AES-256-GCM encryption. Your password never leaves the browser
  • Burn After Reading - Self-destructing pastes after first view
  • Expiration Options - Never, 10 min, 1 hour, 1 day, 1 week, 1 month
  • Zero Authentication - No account, no email, no friction
  • Dark Mode - Pure black OLED-optimized dark theme
  • Bot Protection - Honeypot fields, time-based detection, JS challenges
  • Rate Limiting - In-memory sliding window rate limiter
  • Admin Analytics - Optional /admin dashboard (paste/view totals, language mix, size distribution, activity over time). Disabled unless ADMIN_TOKEN is set
  • Open Source - MIT licensed. Self-host it, fork it, contribute to it

Tech Stack

LayerTechnology
FrameworkNext.js 16.3 (App Router, Server Actions, Cache Components)
LanguageTypeScript 5 (strict mode)
StylingTailwind CSS 4
DatabaseSQLite via Drizzle ORM + better-sqlite3
Object StorageCloudflare R2 (S3-compatible)
Syntax HighlightingShiki (150+ languages)
MarkdownReact Markdown + remark-gfm
EncryptionWeb Crypto API (AES-256-GCM + PBKDF2)
Rate LimitingIn-memory sliding window (single-instance by design)
ChartsRecharts + Evil Charts (admin analytics)
ValidationZod
DeploymentRailway — one service + a volume

Architecture

bin-21/
app/ # Next.js App Router pages
[id]/ # View paste page + raw endpoint
admin/ # Analytics dashboard (env-gated)
layout.tsx # Root layout with theme provider
page.tsx # Home / create paste
server/
actions/ # Server Actions (entry points, Zod validation)
services/ # Core business logic (DB, R2, stats)
lib/
db/ # Drizzle schema, SQLite client, migrations
shiki.ts # Syntax highlighter (bounded language cache)
languages.ts # 150+ language definitions
rate-limit.ts # In-memory rate limiter
janitor.ts # Reclaims expired pastes
admin-auth.ts # Admin token verification
bot-detection.ts # Bot detection utilities
components/
ui/ # Vendored chart primitives
admin/ # Analytics dashboard components
types/ # Shared TypeScript types
instrumentation.ts # Server startup hook (starts the janitor)
proxy.ts # Next.js 16 proxy (rate limiting, admin gate)

Data flow: Client form -> Server Action (validates with Zod) -> Service (uploads content to R2, saves metadata to SQLite) -> Returns paste ID -> Redirect to view page.

Paste content is stored in Cloudflare R2, never in the database. Encrypted pastes use client-side AES-256-GCM - the password never reaches the server.

Single-instance by design. The SQLite file lives on a mounted volume, and Railway does not allow replicas on a service with a volume. That is what makes in-memory rate limiting and the in-process janitor correct. The trade-offs: no horizontal scaling, and brief downtime on redeploy.

Getting Started

Prerequisites

  • Node.js 20+
  • Cloudflare R2 bucket

No database server to run — SQLite is a file, created automatically on first boot.

Setup

git clone https://github.com/t21dev/bin-21.git
cd bin-21
npm install
cp .env.example .env

Edit .env with your credentials:

# Optional locally — defaults to ./.data/bin21.dbDATABASE_PATH=./.data/bin21.dbR2_ACCOUNT_ID=your-account-idR2_ACCESS_KEY_ID=your-access-keyR2_SECRET_ACCESS_KEY=your-secret-keyR2_BUCKET_NAME=bin21-pastesNEXT_PUBLIC_APP_URL=http://localhost:3000

Start the dev server — migrations run automatically on boot:

npm run dev

Open http://localhost:3000.

Leave JANITOR_ENABLED unset locally. It deletes expired pastes and their R2 objects, so a dev machine pointed at a production bucket would reclaim live storage.

Commands

npm run dev # Start dev server
npm run build # Production build
npm start # Start production server
npm run lint # ESLint
npm run db:push # Push schema to database
npm run db:generate # Generate Drizzle migrations
npm run db:studio # Open Drizzle Studio

Deployment

Self-hosting? See the full Self-Hosting Guide for Docker Compose, MinIO, reverse proxy, and more.

Railway (Recommended)

  1. Create a new project on Railway
  2. Connect your GitHub repo
  3. Attach a volume to the service, mounted at /data
  4. Set environment variables from .env.example — in particular DATABASE_PATH=/data/bin21.db and JANITOR_ENABLED=true
  5. Enable automated volume backups. The volume is now the only copy of your paste metadata; there is no managed database to fall back on
  6. Deploy

No database or cache service is needed — one service plus a volume.

Docker

FROM node:20-alpine AS base
WORKDIR /app
FROM base AS deps
COPY package*.json ./
RUN npm ci
FROM base AS builder
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN npm run build
FROM base AS runner
ENV NODE_ENV=production
COPY --from=builder /app/.next/standalone ./
COPY --from=builder /app/.next/static ./.next/static
COPY --from=builder /app/public ./public
EXPOSE 3000
CMD ["node", "server.js"]

Privacy & Security

  • Client-side encryption: Password-protected pastes are encrypted in your browser using AES-256-GCM with PBKDF2 key derivation (100,000 iterations). The plaintext password never reaches the server.
  • No tracking: No analytics, no cookies (beyond theme preference), no user accounts.
  • IP hashing: IPs are salted and hashed for rate limiting. Raw IPs are never stored.
  • Burn after reading: Paste is permanently deleted from both database and R2 after first view.
  • Expiration: Expired pastes are automatically cleaned up.

Rate Limits

ActionLimitWindow
Create paste10 requests1 minute
View paste60 requests1 minute
Failed password5 attempts5 minutes

Contributing

Contributions are welcome. Please open an issue first to discuss what you'd like to change.

  1. Fork the repo
  2. Create your branch (git checkout -b feature/my-feature)
  3. Commit your changes
  4. Push to the branch
  5. Open a Pull Request

License

MIT - do whatever you want with it.


Built by T21 Dev

About

Modern No-Strings-Attached Pastebin

Topics

Resources

Stars

4 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages