5 MBmax upload size
20uploads per 30 days
30 daysfree retention
1 hourpresigned URL TTL
01
1Upload
One API Call,
One URL Back
Send your HTML to /api/v1/upload with a Bearer token — the response contains a ready-to-share URL.
🔑
API Key Authentication
Every upload needs a Bearer token in the format xp_live_… The 12-char prefix is looked up instantly; the full key is bcrypt-verified against the stored hash.
📦
5 MB Body Limit
Hono's bodyLimit middleware runs before auth — oversized requests are rejected with 413 before any credentials are checked or database touched.
📋
Multipart Parse
Extracts the HTML file plus optional title, description, and tags. If form fields are blank, metadata is auto-parsed from the HTML's own title tag and meta description.
🚦
Rate Limit Gate
Counts non-deleted uploads in the rolling 30-day window per user. Hard 429 at 20 uploads — the response includes an upgrade URL pointing to the Pro plan.
02
2Process
Scan, Sanitize,
Store
Every upload passes a two-stage security scanner before anything touches permanent storage.
Security Scanner — Two-Stage Pipeline
1
DOMPurify — Structural XSS Strips 20 forbidden tags (script, style, link, object, embed…) and 20 forbidden attributes including all event handlers
→ STRIP
2
Regex — Credential Leaks 7 patterns: AWS keys, SendGrid tokens, GitHub PATs, JWTs, private keys — gated by Shannon entropy to cut false positives
→ REDACT
3
Exfil Beacons & Prompt Injection 3 exfiltration patterns (tracking pixels, beacon URLs) + 13 LLM prompt injection payloads removed inline
→ STRIP
✓ Clean
Stored as-is, sanitized: false in response
⚠ Modified
Stored sanitized, sanitized: true + structured warn log
→ metadata + attribution
Post-Scan Processing
🏷️
Extract Metadata
Parses title and description from sanitized HTML; form fields override if provided
©️
Inject Attribution Footer
Appends an Explainers.fyi credit strip before the closing body tag on free-tier uploads
☁️
Upload to Cloudflare R2
Stored at {user_id}/{slug}.html in a private bucket — raw R2 URLs are never exposed to the public
🗄️
Insert DB Record
Slug, R2 key, size, expiry, and view count written to PostgreSQL. Up to 3 retries on 10-char slug collision
03
3Serve
Private Bucket,
Public Link
The HTML lives in a private R2 bucket — every view gets a fresh presigned URL so the content is always accessible but the bucket stays locked.
🔍
DB Lookup by Slug
Fetches the record WHERE slug matches AND deleted_at IS NULL — soft-deleted explainers return 404
Expiry Check
If expires_at is in the past, returns 410 Gone with a branded "upgrade to Pro" prompt — no redirect issued
👁️
Increment View Count
Fire-and-forget UPDATE — errors are logged as warnings but never block the response or delay the viewer
🔗
Presigned Redirect
Issues a 1-hour presigned R2 URL and returns 302 — the viewer's browser fetches directly from Cloudflare's edge network
guardrails
30 daysfree retention
1 hourpresigned TTL
private bucketR2 never public
soft delete— deleted_at stamp, files GC'd separately