Building Markdrop: A Pastebin for the AI Era

November 5, 2025

If you've used ChatGPT, Claude, or any other AI assistant, you've faced this problem: How do you share the output?

The current workflow is painful:

  • Take a screenshot (loses formatting)
  • Copy-paste to Google Docs (breaks code blocks)
  • Share via email (messy formatting)
  • Post on Twitter (character limit kills context)

I built Markdrop to solve this: a modern pastebin specifically designed for sharing AI-generated content with proper markdown rendering, syntax highlighting, and team collaboration features.

The Problem with Screenshots

For Developers:

  • Code formatting breaks
  • Syntax highlighting lost
  • Can't copy-paste code
  • Screenshots aren't searchable
  • No version control

For Teams:

  • Hard to collaborate on AI outputs
  • No commenting or feedback
  • Can't track changes
  • Screenshots clutter chat
  • Lost in message history

For Everyone:

  • Static, can't edit
  • Hard to find later
  • No organization
  • Privacy concerns (public screenshots)

Why Existing Solutions Fall Short

I evaluated existing pastebin services:

Traditional Pastebins (Pastebin.com, Gist)

  • ❌ No markdown preview
  • ❌ Outdated UI
  • ❌ No team features
  • ❌ Ad-heavy

Note-taking Apps (Notion, Google Docs)

  • ❌ Too heavy for quick sharing
  • ❌ Requires account to view
  • ❌ Not optimized for code
  • ❌ Slow to load

Code Sharing (CodePen, JSFiddle)

  • ❌ Only for runnable code
  • ❌ Not for prose/markdown
  • ❌ Overkill for simple sharing

Enter Markdrop: The Solution

Markdrop is a markdown-first pastebin built for the AI era:

Anonymous paste creation (no account needed) ✅ Live markdown preview with syntax highlighting ✅ Team collaboration with projects and collections ✅ 30-day auto-expiration for free tier ✅ Rate limiting without requiring sign-up ✅ Like/unlike functionality ✅ Mobile-responsive split-pane editor

Tech Stack

I chose Next.js 15 with the App Router for modern React features and excellent DX:

  • Next.js 15: App Router, Server Components, Server Actions
  • Drizzle ORM: Type-safe SQL with PostgreSQL
  • Better Auth: Flexible authentication with social providers
  • shadcn/ui: Beautiful, accessible UI components
  • Vercel: Deployment and hosting
  • Neon PostgreSQL: Serverless Postgres database

Key Features Walkthrough

1. Anonymous Paste Creation

No account required. Just paste, save, share:

// app/actions/paste.ts
'use server'

import { nanoid } from 'nanoid'
import { db } from '@/lib/db'
import { pastes } from '@/lib/schema'

export async function createPaste(content: string, expiresInDays = 30) {
  const slug = nanoid(10) // Short, unique ID

  const expiresAt = new Date()
  expiresAt.setDate(expiresAt.getDate() + expiresInDays)

  const [paste] = await db.insert(pastes).values({
    slug,
    content,
    expiresAt,
    createdAt: new Date(),
  }).returning()

  return paste
}

2. Rate Limiting Without Accounts

IP-based rate limiting with hashed IPs for privacy:

// lib/rate-limit.ts
import { createHash } from 'crypto'
import { Redis } from '@upstash/redis'

const redis = new Redis({
  url: process.env.UPSTASH_REDIS_URL!,
  token: process.env.UPSTASH_REDIS_TOKEN!,
})

function hashIP(ip: string): string {
  return createHash('sha256')
    .update(ip + process.env.RATE_LIMIT_SALT!)
    .digest('hex')
}

export async function checkRateLimit(ip: string): Promise<boolean> {
  const hashedIP = hashIP(ip)
  const key = `ratelimit:${hashedIP}`

  const count = await redis.incr(key)

  if (count === 1) {
    // First request, set expiration
    await redis.expire(key, 3600) // 1 hour
  }

  return count <= 10 // Max 10 pastes per hour
}

3. Team Collaboration

Projects group related pastes:

// lib/schema.ts (Drizzle)
export const teams = pgTable('teams', {
  id: uuid('id').primaryKey().defaultRandom(),
  name: text('name').notNull(),
  slug: text('slug').notNull().unique(),
  createdAt: timestamp('created_at').defaultNow(),
})

export const projects = pgTable('projects', {
  id: uuid('id').primaryKey().defaultRandom(),
  teamId: uuid('team_id').references(() => teams.id),
  name: text('name').notNull(),
  description: text('description'),
})

export const pastes = pgTable('pastes', {
  id: uuid('id').primaryKey().defaultRandom(),
  slug: text('slug').notNull().unique(),
  content: text('content').notNull(),
  projectId: uuid('project_id').references(() => projects.id),
  userId: uuid('user_id').references(() => users.id),
  expiresAt: timestamp('expires_at'),
  createdAt: timestamp('created_at').defaultNow(),
})

4. Live Markdown Preview

Split-pane editor with Monaco-like experience:

// components/editor.tsx
'use client'

import { useState } from 'react'
import { Textarea } from '@/components/ui/textarea'
import { MarkdownPreview } from '@/components/markdown-preview'

export function Editor({ initialContent = '' }) {
  const [content, setContent] = useState(initialContent)

  return (
    <div className="grid grid-cols-1 md:grid-cols-2 gap-4 h-[600px]">
      <div className="border rounded-lg p-4">
        <Textarea
          value={content}
          onChange={(e)=> setContent(e.target.value)}
          placeholder="Paste your markdown here..."
          className="w-full h-full font-mono resize-none"
        />
      </div>

      <div className="border rounded-lg p-4 overflow-auto">
        <MarkdownPreview content={content} />
      </div>
    </div>
  )
}

5. GitHub-Flavored Markdown

Using react-markdown with syntax highlighting:

// components/markdown-preview.tsx
import ReactMarkdown from 'react-markdown'
import remarkGfm from 'remark-gfm'
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter'
import { oneDark } from 'react-syntax-highlighter/dist/esm/styles/prism'

export function MarkdownPreview({ content }: { content: string }) {
  return (
    <ReactMarkdown
      remarkPlugins={[remarkGfm]}
      components={{
        code({ node, inline, className, children, ...props }) {
          const match= /language-(\w+)/.exec(className || '')

          return !inline && match ? (
            <SyntaxHighlighter
              style={oneDark}
              language={match[1]}
              PreTag="div"
              {...props}
            >
              {String(children).replace(/\n$/, '')}
            </SyntaxHighlighter>
          ) : (
            <code className={className} {...props}>
              {children}
            </code>
          )
        },
      }}
    >
      {content}
    </ReactMarkdown>
  )
}

Architecture Decisions

Why Next.js App Router?

Server Components reduce client-side JavaScript:

// app/[slug]/page.tsx
import { db } from '@/lib/db'
import { pastes } from '@/lib/schema'
import { eq } from 'drizzle-orm'
import { MarkdownPreview } from '@/components/markdown-preview'

export default async function PastePage({ params }) {
  const paste = await db.query.pastes.findFirst({
    where: eq(pastes.slug, params.slug),
  })

  if (!paste) {
    return <div>Paste not found</div>
  }

  return (
    <div className="container mx-auto py-8">
      <MarkdownPreview content={paste.content} />
    </div>
  )
}

No client-side fetching needed—SSR by default!

Why Drizzle ORM?

Type-safe SQL with excellent TypeScript inference:

// Fully typed query
const userPastes = await db.query.pastes.findMany({
  where: eq(pastes.userId, user.id),
  with: {
    project: {
      with: {
        team: true,
      },
    },
  },
  orderBy: desc(pastes.createdAt),
})

// Type: Paste & { project: Project & { team: Team } }[]

Why Better Auth?

Flexible auth that doesn't lock you into a specific provider:

// lib/auth.ts
import { betterAuth } from 'better-auth'
import { drizzleAdapter } from 'better-auth/adapters/drizzle'

export const auth = betterAuth({
  database: drizzleAdapter(db, {
    provider: 'pg',
  }),
  socialProviders: {
    github: {
      clientId: process.env.GITHUB_ID!,
      clientSecret: process.env.GITHUB_SECRET!,
    },
    google: {
      clientId: process.env.GOOGLE_ID!,
      clientSecret: process.env.GOOGLE_SECRET!,
    },
  },
})

Monetization Strategy

Free Tier:

  • Anonymous pastes (30-day expiration)
  • Public pastes only
  • Rate limited (10/hour)

Pro Tier ($5/month):

  • Unlimited pastes
  • Private pastes
  • Team collaboration
  • Projects and collections
  • Custom expiration
  • No rate limits

This aligns with how developers think: try it free, upgrade when you need team features.

Deployment: Vercel + Neon

Vercel for hosting:

  • Zero-config deployment
  • Edge functions for rate limiting
  • Analytics built-in

Neon PostgreSQL for database:

  • Serverless (scales to zero)
  • Generous free tier
  • Automatic backups
  • Branch databases for preview deployments
# Deploy
vercel --prod

# Database migrations
pnpm drizzle-kit push:pg

Lessons Learned

1. Start Anonymous-First

Don't require sign-up for basic features. Lower friction = more users.

2. Rate Limiting is Hard

IP-based limiting works but has issues:

  • VPNs share IPs
  • Mobile networks rotate IPs
  • Hashed IPs lose some debuggability

Consider device fingerprinting for better accuracy.

3. Markdown Edge Cases

Users paste wild stuff:

  • 50,000+ line files (crashes browser)
  • Malicious HTML in markdown
  • Deeply nested blockquotes

Solutions:

  • Limit paste size (500KB max)
  • Sanitize HTML in preview
  • Lazy-load large pastes

4. Monaco vs Textarea

I tried Monaco editor first (like VS Code):

  • ❌ 2MB bundle size
  • ❌ Overkill for simple pasting
  • ❌ Mobile support issues

Switched to enhanced <textarea>:

  • ✅ 0KB (native)
  • ✅ Works everywhere
  • ✅ Accessible

Performance Optimizations

1. Server Components for Static Content

// Heavy markdown rendering happens server-side
export default async function PastePage({ params }) {
  const paste = await db.query.pastes.findFirst({
    where: eq(pastes.slug, params.slug),
  })

  // Pre-rendered HTML sent to client
  return <MarkdownPreview content={paste.content} />
}

2. Edge Caching

export const revalidate = 3600 // 1 hour

export async function generateStaticParams() {
  // Pre-render popular pastes
  const popular = await db.query.pastes.findMany({
    where: gt(pastes.views, 100),
    limit: 100,
  })

  return popular.map((paste) => ({
    slug: paste.slug,
  }))
}

3. Database Indexes

export const pastes = pgTable(
  'pastes',
  {
    id: uuid('id').primaryKey().defaultRandom(),
    slug: text('slug').notNull().unique(),
    userId: uuid('user_id'),
    createdAt: timestamp('created_at').defaultNow(),
  },
  (table) => ({
    slugIdx: index('slug_idx').on(table.slug),
    userIdIdx: index('user_id_idx').on(table.userId),
    createdAtIdx: index('created_at_idx').on(table.createdAt),
  })
)

Future Roadmap

Short-term:

  • API for programmatic paste creation
  • CLI tool (markdrop push file.md)
  • Embed support (<iframe> for pastes)
  • Syntax highlighting themes

Long-term:

  • Collaborative editing (multiplayer)
  • Version history/diffs
  • Comments and annotations
  • AI-powered search across pastes
  • Export to PDF/HTML

Conclusion

Markdrop solves a real problem: sharing AI-generated content is painful with existing tools. By focusing on markdown, anonymous creation, and team features, it fills a gap in the market.

The tech stack (Next.js 15, Drizzle, Better Auth) provides a solid foundation that's both performant and developer-friendly. Starting with a generous free tier and clear upgrade path creates a sustainable business model.

If you're building a SaaS, focus on:

  • Low friction (anonymous access)
  • Clear value prop (solves a specific pain)
  • Modern DX (fast, clean, no cruft)
  • Monetization from day one (even if small)

Whether you're sharing ChatGPT outputs, code snippets, or documentation, Markdrop makes it frictionless.

Try it: markdrop.app