DEV Community

solca
solca

Posted on

How I built a suite of self-destructing web tools on Cloudflare (solo, free tier)

Most cloud tools are built around saving things. But a lot of the time I don't want to save anything — I just want to hand something off for a few minutes and have it disappear on its own.

A mock API for the frontend I'm testing. A file that's too big for chat. A password I need to send a teammate without pasting it into Slack forever. Every existing tool wants me to create an account, and then clean up after myself later.

So I built TempTools — a small set of free, no-signup web tools that expire and delete themselves:

  • Temp API — paste JSON/CSV, get a live mock endpoint
  • Temp QR / Temp Link — expiring QR codes and short links
  • Temp File — share a file (up to 100MB) that self-deletes
  • Temp Note / Temp Password — a note or a secret that can self-destruct after a single view

It's all built solo, on the Cloudflare free tier. Here are the parts I found interesting to build.

The stack

  • Next.js 16 (App Router) deployed to Cloudflare Workers via OpenNext
  • D1 (SQLite) for metadata + the text payloads
  • R2 for files
  • A separate Workers Cron worker for garbage collection
  • Free tier the whole way

Expiry done right: lazy expiry + a cron janitor

The obvious way to expire things is a cron job that deletes stuff on a schedule. But if you rely only on cron, an item stays "alive" until the next run — a 5-minute window where an expired secret is still readable. Not great.

So the correctness lives on the read path, and cron is just the janitor:

export async function readAndConsume(id: string, kind: EntryKind) {
  const row = await db.prepare(`SELECT * FROM entries WHERE id = ?`).bind(id).first();
  if (!row || row.kind !== kind) return { status: "not_found" };

  const now = Date.now();
  if (row.consumed === 1 || row.expires_at <= now) {
    await db.prepare(`DELETE FROM entries WHERE id = ?`).bind(id).run(); // lazy delete
    return { status: "gone" };
  }
  // ...serve it
}
Enter fullscreen mode Exit fullscreen mode

Expiry is exact because it's checked the moment someone reads. The 5-minute cron then sweeps up anything that expired but was never accessed (and deletes the matching R2 objects for files):

DELETE FROM entries WHERE expires_at <= ? OR consumed = 1;
Enter fullscreen mode Exit fullscreen mode

Burn-after-read

For one-time secrets, the row is deleted the moment it's opened. For files, the R2 object is deleted with ctx.waitUntil after the response streams out:

if (row.burn_after_read === 1) {
  ctx.waitUntil(env.BUCKET.delete(id)); // delete the file after serving it
}
return new Response(obj.body, { /* ...headers */ });
Enter fullscreen mode Exit fullscreen mode

The Next.js 16 gotcha that cost me a deploy

I wanted / to redirect to /en, so I reached for middleware. In Next.js 16 middleware was renamed to proxy — and here's the catch:

Proxy defaults to the Node.js runtime. The runtime config option is not available in Proxy files.

OpenNext on Cloudflare doesn't support Node.js middleware, so the build failed with "Node.js middleware is not currently supported." You can't force it to edge either.

The fix was to drop the proxy entirely and do the redirect in next.config.ts:

async redirects() {
  return [{ source: "/", destination: "/en", permanent: false }];
}
Enter fullscreen mode Exit fullscreen mode

No middleware needed. If you're moving a Next 16 app to Cloudflare, watch out for this one.

Analytics without a cookie banner

I wanted to know if anyone actually visited without slapping a GDPR consent banner on a tool that's supposed to be frictionless. Cloudflare Web Analytics is cookieless and doesn't fingerprint, so no consent banner is required — one snippet and done. It's less detailed than Google Analytics, but for country-level traffic and basic numbers it's plenty.

Staying inside the free tier on purpose

Free tiers are generous, but files can blow through them, so I added guardrails:

  • Per-file cap of 100MB
  • Per-IP upload limit (a small upload_log table + a rolling 24h count)
  • Total R2 cap — I track each file's size in D1 and reject uploads once the sum would exceed a configurable limit (well under R2's 10GB free storage)

Cloudflare's free limits that made this viable:

  • Workers: 100,000 requests/day (static assets served by Workers Assets don't count)
  • D1: 5M reads/day, 100k writes/day, 5GB
  • R2: 10GB storage, egress free

Bonus: it ships in 10 languages

Everything lives under a [lang] segment with one dictionary per locale (en, ja, zh, es, de, fr, ko, pt, it, tr). hreflang + a sitemap handle the SEO side. Doing it from day one was far less painful than retrofitting it later.

Wrapping up

None of this is groundbreaking on its own, but it's a fun reminder of how much one person can run on a free serverless stack today: a multi-tool app, in 10 languages, with real storage, a cron janitor, rate limiting, and analytics — for $0.

If you want to poke at it, it's live at temptools.webcli.jp (no signup, everything expires). I'd genuinely love feedback — especially on the developer bits like Temp API.

I'll be writing more about the individual pieces (the AI JSON helper, the i18n setup, the deploy pipeline) — follow along if that's your kind of thing. 🛠️

Top comments (0)