Skill: Pastebin Core

## Purpose

7 stars

Best use case

Skill: Pastebin Core is best used when you need a repeatable AI agent workflow instead of a one-off prompt.

## Purpose

Teams using Skill: Pastebin Core should expect a more consistent output, faster repeated execution, less prompt rewriting.

When to use this skill

  • You want a reusable workflow that can be run more than once with consistent structure.

When not to use this skill

  • You only need a quick one-off answer and do not need a reusable workflow.
  • You cannot install or maintain the underlying files, dependencies, or repository context.

Installation

Claude Code / Cursor / Codex

$curl -o ~/.claude/skills/pastebin/SKILL.md --create-dirs "https://raw.githubusercontent.com/heldernoid/agentic-build-templates/main/projects/web-applications/pastebin/skills/pastebin/SKILL.md"

Manual Installation

  1. Download SKILL.md from GitHub
  2. Place it in .claude/skills/pastebin/SKILL.md inside your project
  3. Restart your AI agent — it will auto-discover the skill

How Skill: Pastebin Core Compares

Feature / AgentSkill: Pastebin CoreStandard Approach
Platform SupportNot specifiedLimited / Varies
Context Awareness High Baseline
Installation ComplexityUnknownN/A

Frequently Asked Questions

What does this skill do?

## Purpose

Where can I find the source code?

You can find the source code on GitHub using the link provided at the top of the page.

SKILL.md Source

# Skill: Pastebin Core

## Purpose
Implement the full pastebin backend: paste CRUD, Base62 ID generation, expiry enforcement, password protection with bcrypt, fork relationships, visibility rules, and the raw/public endpoints.

---

## Paste ID Generation

```typescript
// api/src/lib/base62.ts
const ALPHABET = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';
const ID_LENGTH = 8;

export function generatePasteId(): string {
  // rejection-sampling avoids modulo bias
  const bytes = new Uint8Array(ID_LENGTH * 2);
  crypto.getRandomValues(bytes);
  let result = '';
  for (const byte of bytes) {
    if (byte < 248 && result.length < ID_LENGTH) {
      result += ALPHABET[byte % 62];
    }
  }
  if (result.length < ID_LENGTH) return generatePasteId(); // retry (extremely rare)
  return result;
}
```

---

## TypeScript Types

```typescript
// api/src/types.ts
export type Visibility = 'public' | 'unlisted' | 'private';

export interface Paste {
  id: string;           // 8-char Base62
  user_id: number | null;
  title: string | null;
  content: string;
  language: string;     // 'plaintext' | 'typescript' | ... (hljs alias)
  visibility: Visibility;
  password_hash: string | null;
  fork_of: string | null;  // paste.id of parent
  views: number;
  expired: 0 | 1;
  expires_at: string | null;  // ISO-8601 or null
  created_at: string;
  updated_at: string;
}

export interface PasteRow extends Paste {
  author_name: string | null;
}

export interface CreatePasteBody {
  title?: string;
  content: string;
  language?: string;
  visibility?: Visibility;
  password?: string;
  expires_in_seconds?: number;  // null = never
  fork_of?: string;
}

export interface UpdatePasteBody {
  title?: string;
  content?: string;
  language?: string;
  visibility?: Visibility;
  password?: string | null;  // null = remove password
  expires_in_seconds?: number | null;
}

export interface UnlockBody {
  password: string;
}
```

---

## Database Schema

```sql
-- pastes table
CREATE TABLE IF NOT EXISTS pastes (
  id           TEXT PRIMARY KEY,
  user_id      INTEGER REFERENCES users(id) ON DELETE SET NULL,
  title        TEXT,
  content      TEXT NOT NULL CHECK(length(content) <= 524288),  -- 512 KB
  language     TEXT NOT NULL DEFAULT 'plaintext',
  visibility   TEXT NOT NULL DEFAULT 'public' CHECK(visibility IN ('public','unlisted','private')),
  password_hash TEXT,
  fork_of      TEXT REFERENCES pastes(id) ON DELETE SET NULL,
  views        INTEGER NOT NULL DEFAULT 0,
  expired      INTEGER NOT NULL DEFAULT 0,
  expires_at   TEXT,
  created_at   TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ','now')),
  updated_at   TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ','now'))
);

CREATE INDEX IF NOT EXISTS idx_pastes_user   ON pastes(user_id);
CREATE INDEX IF NOT EXISTS idx_pastes_public ON pastes(visibility, expired, created_at DESC);
CREATE INDEX IF NOT EXISTS idx_pastes_expiry ON pastes(expires_at) WHERE expires_at IS NOT NULL;
```

---

## Create Paste

```typescript
// api/src/routes/pastes.ts
import bcrypt from 'bcrypt';
import { generatePasteId } from '../lib/base62.js';
import { db } from '../db.js';

const BCRYPT_ROUNDS = 10;
const MAX_RETRY = 5;

export async function createPaste(
  body: CreatePasteBody,
  userId: number | null
): Promise<string> {  // returns new paste id
  // Anonymous users cannot create private pastes
  if (body.visibility === 'private' && userId === null) {
    throw Object.assign(new Error('Private pastes require an account'), { status: 403 });
  }

  const passwordHash = body.password
    ? await bcrypt.hash(body.password, BCRYPT_ROUNDS)
    : null;

  const expiresAt = body.expires_in_seconds
    ? new Date(Date.now() + body.expires_in_seconds * 1000).toISOString()
    : null;

  for (let attempt = 0; attempt < MAX_RETRY; attempt++) {
    const id = generatePasteId();
    try {
      db.prepare(`
        INSERT INTO pastes (id, user_id, title, content, language, visibility,
                            password_hash, fork_of, expires_at)
        VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
      `).run(
        id,
        userId ?? null,
        body.title?.trim() || null,
        body.content,
        body.language ?? 'plaintext',
        body.visibility ?? 'public',
        passwordHash,
        body.fork_of ?? null,
        expiresAt,
      );
      return id;
    } catch (err: unknown) {
      if ((err as NodeJS.ErrnoException).code === 'SQLITE_CONSTRAINT_PRIMARYKEY') continue;
      throw err;
    }
  }
  throw new Error('Failed to generate unique paste ID after max retries');
}
```

---

## Expiry Middleware

```typescript
// api/src/middleware/expiry.ts
import { db } from '../db.js';
import type { RequestHandler } from 'express';

// Call before serving any paste to enforce soft-expiry
export const enforceExpiry: RequestHandler = (req, _res, next) => {
  const { id } = req.params as { id: string };
  // Mark paste as expired if expires_at has passed
  db.prepare(`
    UPDATE pastes SET expired = 1
    WHERE id = ? AND expired = 0 AND expires_at IS NOT NULL AND expires_at < strftime('%Y-%m-%dT%H:%M:%SZ','now')
  `).run(id);
  next();
};
```

---

## Get Paste (with password and visibility enforcement)

```typescript
export async function getPaste(
  id: string,
  sessionUserId: number | null,
  providedPassword: string | null
): Promise<{ paste: PasteRow; locked: boolean }> {
  const paste = db.prepare(`
    SELECT p.*, u.username AS author_name
    FROM pastes p
    LEFT JOIN users u ON p.user_id = u.id
    WHERE p.id = ?
  `).get(id) as PasteRow | undefined;

  if (!paste) throw Object.assign(new Error('Not found'), { status: 404 });
  if (paste.expired) throw Object.assign(new Error('Paste has expired'), { status: 410 });

  // Private: only owner can view
  if (paste.visibility === 'private' && paste.user_id !== sessionUserId) {
    throw Object.assign(new Error('Forbidden'), { status: 403 });
  }

  // Password locked
  if (paste.password_hash) {
    if (!providedPassword) {
      // Return metadata only, locked = true
      return { paste: { ...paste, content: '' }, locked: true };
    }
    const match = await bcrypt.compare(providedPassword, paste.password_hash);
    if (!match) throw Object.assign(new Error('Incorrect password'), { status: 401 });
  }

  // Increment view count (fire-and-forget; skip for owner)
  if (paste.user_id !== sessionUserId) {
    db.prepare('UPDATE pastes SET views = views + 1 WHERE id = ?').run(id);
  }

  return { paste, locked: false };
}
```

---

## Unlock Endpoint

```typescript
// POST /api/pastes/:id/unlock
// Body: { password: string }
// Returns: { content: string } on success, 401 on wrong password
router.post('/:id/unlock', enforceExpiry, async (req, res, next) => {
  try {
    const { id } = req.params;
    const { password } = req.body as UnlockBody;
    const { paste } = await getPaste(id, req.session?.userId ?? null, password);
    res.json({ content: paste.content });
  } catch (err) {
    next(err);
  }
});
```

---

## Public Recent Pastes

```typescript
// GET /api/public/recent?lang=&page=&per_page=
export function getRecentPastes(opts: {
  lang?: string;
  page: number;
  perPage: number;
}): { pastes: PublicPasteRow[]; total: number } {
  const offset = (opts.page - 1) * opts.perPage;
  const langClause = opts.lang ? 'AND p.language = ?' : '';
  const langParam = opts.lang ? [opts.lang] : [];

  const pastes = db.prepare(`
    SELECT p.id, p.title, p.language, p.views, p.created_at,
           u.username AS author_name,
           substr(p.content, 1, 200) AS preview,
           CASE WHEN p.password_hash IS NOT NULL THEN 1 ELSE 0 END AS is_locked
    FROM pastes p
    LEFT JOIN users u ON p.user_id = u.id
    WHERE p.visibility = 'public' AND p.expired = 0 ${langClause}
    ORDER BY p.created_at DESC
    LIMIT ? OFFSET ?
  `).all(...langParam, opts.perPage, offset) as PublicPasteRow[];

  const { total } = db.prepare(`
    SELECT COUNT(*) AS total FROM pastes
    WHERE visibility = 'public' AND expired = 0 ${langClause}
  `).get(...langParam) as { total: number };

  return { pastes, total };
}
```

---

## Fork a Paste

```typescript
// POST /api/pastes/:id/fork  (requires auth)
router.post('/:id/fork', requireAuth, enforceExpiry, async (req, res, next) => {
  try {
    const { id } = req.params;
    const { paste } = await getPaste(id, req.session.userId, null);
    if (paste.locked) throw Object.assign(new Error('Cannot fork a password-locked paste without unlocking'), { status: 403 });

    const body: CreatePasteBody = {
      title: paste.title ? `${paste.title} (fork)` : undefined,
      content: paste.content,
      language: paste.language,
      visibility: 'public',
      fork_of: paste.id,
    };
    const newId = await createPaste(body, req.session.userId);
    res.status(201).json({ id: newId });
  } catch (err) {
    next(err);
  }
});
```

---

## Raw Content Endpoint

```typescript
// GET /raw/:id  -- plain text response, no JSON wrapper
router.get('/:id', enforceExpiry, async (req, res, next) => {
  try {
    const { id } = req.params;
    // Raw endpoint never serves password-protected pastes without unlock
    const { paste, locked } = await getPaste(id, req.session?.userId ?? null, null);
    if (locked) return res.status(403).send('Password protected paste: use /api/pastes/:id/unlock');
    res.setHeader('Content-Type', 'text/plain; charset=utf-8');
    res.setHeader('X-Content-Type-Options', 'nosniff');
    res.send(paste.content);
  } catch (err) {
    next(err);
  }
});
```

---

## User Pastes List

```typescript
// GET /api/user/pastes?visibility=&page=&per_page=  (requires auth)
export function getUserPastes(opts: {
  userId: number;
  visibility?: Visibility;
  page: number;
  perPage: number;
}): { pastes: Paste[]; total: number } {
  const offset = (opts.page - 1) * opts.perPage;
  const visClause = opts.visibility ? 'AND visibility = ?' : '';
  const visParam = opts.visibility ? [opts.visibility] : [];

  const pastes = db.prepare(`
    SELECT * FROM pastes
    WHERE user_id = ? ${visClause}
    ORDER BY created_at DESC
    LIMIT ? OFFSET ?
  `).all(opts.userId, ...visParam, opts.perPage, offset) as Paste[];

  const { total } = db.prepare(`
    SELECT COUNT(*) AS total FROM pastes WHERE user_id = ? ${visClause}
  `).get(opts.userId, ...visParam) as { total: number };

  return { pastes, total };
}
```

---

## Scheduled Expiry Cleanup

```typescript
// api/src/jobs/expireOld.ts
import cron from 'node-cron';
import { db } from '../db.js';

// Run every 10 minutes; mark expired pastes
export function startExpiryJob(): void {
  cron.schedule('*/10 * * * *', () => {
    const result = db.prepare(`
      UPDATE pastes SET expired = 1
      WHERE expired = 0 AND expires_at IS NOT NULL AND expires_at < strftime('%Y-%m-%dT%H:%M:%SZ','now')
    `).run();
    if (result.changes > 0) {
      console.log(`[expiry] marked ${result.changes} paste(s) as expired`);
    }
  });
}
```

---

## Validation Rules

| Field | Rule |
|---|---|
| `content` | 1 byte minimum, 524288 bytes (512 KB) maximum |
| `title` | max 255 chars, trimmed |
| `language` | must be a valid hljs language alias or 'plaintext' |
| `visibility` | 'public' | 'unlisted' | 'private'; anonymous forces 'public'/'unlisted' |
| `password` | 6-128 chars when provided |
| `expires_in_seconds` | 3600 (1h) | 86400 (24h) | 604800 (7d) | 2592000 (30d) | null (never) |
| `fork_of` | must reference existing, non-expired, non-private paste |

---

## API Routes Summary

| Method | Path | Auth | Description |
|---|---|---|---|
| POST | /api/pastes | optional | Create paste |
| GET | /api/pastes/:id | optional | Get paste (returns locked:true if password required) |
| PUT | /api/pastes/:id | owner | Update paste |
| DELETE | /api/pastes/:id | owner | Delete paste |
| POST | /api/pastes/:id/unlock | - | Verify password, return content |
| POST | /api/pastes/:id/fork | required | Fork paste |
| GET | /api/public/recent | - | Paginated public list |
| GET | /api/user/pastes | required | Owner's pastes |
| GET | /raw/:id | optional | Plain text content |

---

## pnpm Commands

```
pnpm --filter api dev         # start API with tsx watch
pnpm --filter web dev         # start Vite dev server
pnpm --filter api test        # run Vitest
pnpm --filter api build       # tsc compile
pnpm --filter web build       # Vite production build
pnpm --filter api db:migrate  # run migrations
```

Related Skills

Skill: Invoice Generator Core

7
from heldernoid/agentic-build-templates

## Purpose

Skill: Form Builder Core

7
from heldernoid/agentic-build-templates

## Purpose

Skill: Uptime Monitoring

7
from heldernoid/agentic-build-templates

## Overview

Skill: Status Page

7
from heldernoid/agentic-build-templates

## Overview

Skill: unit-conversion

7
from heldernoid/agentic-build-templates

## Overview

Skill: recipe-scaler

7
from heldernoid/agentic-build-templates

## Overview

reading-list

7
from heldernoid/agentic-build-templates

Operate the reading-list API to save, manage, tag, search, and export articles.

email-digest

7
from heldernoid/agentic-build-templates

Configure, test, and troubleshoot the reading-list daily email digest delivered via nodemailer.

websocket-realtime

7
from heldernoid/agentic-build-templates

Use the WebSocket connection in poll-builder to receive live vote updates. Use when you need to stream real-time poll results, monitor a poll for new votes, or build a live dashboard. Triggers include "live results", "real-time updates", "stream votes", "watch poll", or "WebSocket".

poll-builder

7
from heldernoid/agentic-build-templates

Self-hosted poll creation tool with real-time results. Use when you need to create a poll, check vote counts, close a poll, export results, or get the shareable link for a poll. Triggers include "create poll", "vote", "poll results", "survey", "collect votes", "share poll", or any task involving polling or voting.

Skill: personal-finance

7
from heldernoid/agentic-build-templates

## Overview

Skill: csv-import

7
from heldernoid/agentic-build-templates

## Overview