Inspired by Cloudflare Drop (llms.txt). This post is an independent product teardown and open-source clone — not reverse-engineering of proprietary internals.
Drop a folder. Get a live URL. Claim it in sixty minutes or watch it vanish.
That is the pitch of Cloudflare Drop: instant static-site staging on the edge, no account required to start. For builders shipping previews, and for AI agents that need a deploy target with almost no ceremony, the product hits a nerve.
This is Part 1 of a four-part series. Today: product teardown + a portable open-source clone you can run locally in about five minutes. Next posts take the same codebase to Cloudflare Workers, then AWS, then an agent-first surface (llms.txt + MCP).
Clone: github.com/vovanduc/stage-drop
The ten-second experience
Public behavior (observed from the product page and llms.txt, not from private source):
- You drop a folder or
.zipof a static site (HTML/CSS/JS, with anindex.html). - Cloudflare distributes it on the global network.
- You get a live
workers.devURL. - Within 60 minutes, you must open a claim URL to keep the deployment. Miss the window and the temporary preview goes away.
There is also a first-class CLI path: wrangler deploy … --temporary (Wrangler ≥ 4.102.0) for local project workflows. Authenticated Wrangler uses a normal deploy — temporary mode is for the zero-account funnel.
Two URLs come back every time: the live URL (shareable preview) and the claim URL (sensitive ownership grant). Treat the claim link like a password with a TTL.
That loop is the whole product. Everything interesting — abuse resistance, metadata TTL, agent docs — hangs off it.
Product lens: why ship a free tool with no login?
From a product lead's perspective, Drop looks less like a side toy and more like a funnel + positioning play:
- Zero friction into Workers. The first deploy does not ask for OAuth. The second action (claim) creates ownership. That is classic "try before you commit," tuned for static assets on Workers.
-
Agent Experience (AX) as a first-class user. Shipping an
llms.txtthat tells agents how to upload, what to return, and how to prefer Wrangler over browser automation is a signal: AI agents are not an afterthought; they are a primary audience. - Toolchain honesty. Temporary deploy lives inside Wrangler, not only in a marketing UI. The web drop zone and the CLI share one mental model.
The hard product trade-off is obvious: zero-auth upload versus abuse, phishing, and claim-token theft. Production Drop must sit on Cloudflare's edge controls, rate limits, and account systems. A teaching clone should name that gap instead of pretending a weekend demo is a CDN.
Inferred architecture (behavior-only)
This is inference from public behavior and docs, not a claim about Cloudflare's internal implementation.
| Concern | What public behavior suggests |
|---|---|
| Compute | Edge Workers (live URL on workers.dev) |
| Blobs | Object storage for unzipped static files |
| Metadata | TTL-aware record: site id, expiry, claim state |
| Claim | One-shot secret; grant ownership / clear temporary expiry |
| Cleanup | Something that reaps unclaimed expired deploys |
CLI temporary deploy and the browser UI both emit live + claim URLs, so the control plane is shared even if the upload path differs.
What we deliberately do not do: decompile Workers, scrape private APIs, or copy UI/branding. The clone is inspired-by, with its own interfaces and security choices.
Design of stage-drop: portable core + adapters
The technical thesis of the series is simple:
One TypeScript core. Swap storage adapters. Same upload / claim / serve / expire semantics on laptop, Cloudflare, and AWS.
[Web UI / curl]
|
v
+-----------------+ +------------------------+
| Core (Hono) |---->| BlobStore (interface) | local FS | R2 (#2) | S3 (#3)
| upload/claim | +------------------------+
| serve/expire |---->| MetaStore (interface) | memory | KV (#2) | DynamoDB TTL (#3)
+-----------------+ +------------------------+
Why Hono? It runs on Node for local demos and on Workers / Lambda-style runtimes later without rewriting the HTTP surface.
Why interfaces? Parts 2 and 3 should be adapter work, not a rewrite. That is how you get an honest Cloudflare-vs-AWS comparison on the same product semantics.
Three hard decisions (local demo)
- Claim tokens — Generate >=128-bit random tokens; store SHA-256 hashes only; one-shot consume. The raw token lives in the claim URL, never in durable storage.
- TTL lifecycle — Unclaimed sites expire (~60 minutes). Serve returns 410 Gone after expiry. A periodic sweep deletes blobs + meta for expired unclaimed sites. Claim clears expiry.
- Abuse floor — Zip <=10 MB, <=500 files; zip-slip blocked; content-type allowlist by extension; in-memory IP rate limit on upload. Explicitly not included: malware scan, phishing heuristics, CAPTCHA. Those belong in a real edge product.
Architecture (lab + clone)
The lab Worker at lab.vovanduc.tech/stage-drop/ uses the same core with R2 for unzipped files and KV for metadata. Key layout:
| Store | Key pattern | Role |
|---|---|---|
R2 stage-drop-blobs
|
sites/{id}/... |
Static files after unzip |
KV META
|
meta:{id} |
Site record (claimed?, expiresAt, ...) |
| KV | claim:{sha256} |
One-shot claim lookup (hash only) |
| KV | exp:{expiresAt}:{id} |
Sweep index for unclaimed TTL |
TTL: unclaimed sites expire after 60 minutes. Cron: Worker trigger */5 * * * * runs sweepExpired (delete R2 objects + KV keys for expired unclaimed sites). Health: /stage-drop/health.
Request flow
flowchart TD
U["Upload zip"] --> V["Unzip + validate"]
V --> R2["R2 sites/{id}/"]
V --> KV["KV meta:{id} · claim:{sha256} · exp:{expiresAt}:{id}"]
KV --> URL["liveUrl + claimUrl"]
S["GET /s/:id"] --> M["Lookup meta"]
M -->|ok| SR["Serve from R2"]
M -->|unclaimed expired| G["410 Gone"]
C["Claim token"] --> H["SHA-256 → claim:{hash}"]
H --> CL["claimed=true · expiresAt=null · delete exp: · rotate hash"]
CR["Cron */5"] --> SW["sweepExpired → delete R2 + KV"]
Site state machine
stateDiagram-v2
[*] --> LiveUnclaimed: upload
LiveUnclaimed --> Claimed: claim one-shot
LiveUnclaimed --> Expired: TTL 60 min
Expired --> [*]: cron sweep
Claimed --> [*]
Claim in plain language
Upload returns two URLs. The live URL is safe to share — anyone can view the preview while it is still alive. The claim URL is the ownership grant: it carries a one-shot >=128-bit token. When you open it, the Worker hashes the token, looks up claim:{sha256}, marks the site Claimed, clears expiresAt, deletes the exp: index entry, and rotates the hash so the link cannot be replayed. Skip claim and, after 60 minutes, serve answers 410 Gone; within a few minutes the cron sweep removes blobs and meta.
That is the whole lifecycle: LiveUnclaimed → Claimed (keep forever on this demo) or LiveUnclaimed → Expired → swept.
Run the local demo (<=5 minutes)
git clone https://github.com/vovanduc/stage-drop.git
cd stage-drop
npm install
npm test # should be green
npm run dev # http://localhost:8787
- Open the UI and drop a zip that contains
index.html. - Open the live URL — files are served under
/s/:siteId/. - Open the claim URL within the TTL window. Token is one-shot; only a hash was stored.
Or upload with curl:
zip -r site.zip index.html style.css
curl -sS -X POST http://localhost:8787/api/upload \
-H 'content-type: application/zip' \
--data-binary @site.zip | jq
Expect JSON with liveUrl and claimUrl. Hit live → HTML. Claim → site survives beyond the expiry sweep. Skip claim → eventually 410.
Vitest covers claim one-shot, TTL → 410, zip-slip rejection, size limits, and content-type serving. If npm test is green, you have the same safety rails the README promises.
Or skip local setup and open the live lab: lab.vovanduc.tech/stage-drop/.
Agent-friendly Cloudflare setup for anyone working on the repo (Claude, OpenCode, Orca, Cursor, ... — not host-locked):
https://github.com/vovanduc/stage-drop/blob/master/docs/agent-setup.md
What this clone teaches that a screenshot cannot
Building the loop yourself forces the production questions:
- Where does expiry live — process sweep, cron, or datastore TTL?
- How do you serve path-prefixed sites (
/s/:id/...) without breaking relative assets? - What is the minimum abuse bar before you are embarrassed in public?
- How do you document the product for humans and agents without duplicating two truths?
Those questions are the spine of the series. Local FS + in-memory meta answer them for a laptop. Cloud adapters must answer them again with real SLOs and bills.
What's next — Part 2
Same core. Cloudflare adapters: Workers + R2 + KV, Wrangler in-repo, and a live lab at:
https://lab.vovanduc.tech/stage-drop/
GitHub Pages at vovanduc.github.io/stage-drop/ is only a redirect to the repo — not the upload API. The Worker owns upload/claim/serve.
Part 3 mirrors the stack on AWS (S3 + CloudFront + Lambda + DynamoDB TTL) and compares cost / latency / DX. Part 4 makes the clone agent-first: llms.txt, HTTP API, MCP.
Tip — try stage-drop
- Source / docs entry: https://vovanduc.github.io/stage-drop/ → redirects to the GitHub repo
- Live demo: https://lab.vovanduc.tech/stage-drop/
(Optional ops check: https://lab.vovanduc.tech/stage-drop/health)
Links
| Cloudflare Drop | https://www.cloudflare.com/drop/ |
| Drop llms.txt | https://www.cloudflare.com/drop/llms.txt |
| stage-drop repo | https://github.com/vovanduc/stage-drop |
| Pages → repo | https://vovanduc.github.io/stage-drop/ |
| Live lab | https://lab.vovanduc.tech/stage-drop/ |
| Agent setup (portable) | https://github.com/vovanduc/stage-drop/blob/master/docs/agent-setup.md |
| Official CF agent prompt | https://developers.cloudflare.com/agent-setup/prompt.md |
Top comments (0)