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