Building a $0 Platform on the Edge
How static generation, Cloudflare Workers and D1 combine into a personal site that costs nothing to run.
Debanjan Saha
· 1 min read
On this page
Most of a personal site is content that never changes between requests. That observation drives the whole architecture: render everything ahead of time, and reach for a server only for the few things that genuinely need state.
The shape of the system
Pages are generated at build time and served from Cloudflare’s CDN. Only two features need a database: view counts and claps. Those live in D1 and are reached through small API routes that run as a Worker.
Why atomic upserts
Two readers clapping at once must not lose an update. A read-modify-write in application code races; a single SQL statement does not:
INSERT INTO post_stats (slug, claps) VALUES (?1, ?2)
ON CONFLICT(slug) DO UPDATE SET claps = claps + excluded.claps
RETURNING views, claps;
The same idea in TypeScript, as it appears in the API route:
const row = await env.DB.prepare(sql)
.bind(slug, count)
.first<{ views: number; claps: number }>();
return Response.json({ views: row?.views ?? 0, claps: row?.claps ?? 0 });
The best database round trip is the one you never make. The second best is the one that does everything in a single statement.
What it costs
| Piece | Free tier |
|---|---|
| Pages hosting | Unlimited static requests |
| Workers | 100k requests / day |
| D1 | 5M rows read / day |
Takeaways
- Prerender by default.
- Push state into a single atomic statement.
- Fetch dynamic numbers on the client so the page itself stays cacheable.
Discussion
Comments (Giscus) will appear here. Set
PUBLIC_GISCUS_REPO,PUBLIC_GISCUS_REPO_ID,PUBLIC_GISCUS_CATEGORYandPUBLIC_GISCUS_CATEGORY_IDto enable them.