I Built a Full Blog Platform for $0 — Here's the Entire Stack
Author
Ananth
Date Published

I wanted a personal tech blog. Not a Medium page, not a subdomain on someone else's platform — my own site, my own admin panel, my own domain, comments, email subscribers, even ad revenue potential.
The catch: I didn't want to pay a single rupee for it.
Turns out, in 2026, you don't have to. Here's exactly how www.ananthks.com runs — completely free, every month, forever (well, until traffic gets big enough to blow past generous free-tier limits, which is a good problem to have).
## The Big Picture
Everything lives in **one repository, one deployment**. No juggling five different dashboards for five different servers. Here's the bird's-eye view:
- A **Next.js app** serves the public blog pages
- **Payload CMS** is bolted directly onto that same app, powering a `/admin` panel to write and publish posts
- When I hit publish, an automated hook **emails every subscriber** — no manual newsletter sending
- Comments, likes, ads, and analytics are all handled by free third-party tools that plug in with a few lines of code
Let's go layer by layer.
## 1. The Frontend + Admin Panel: Next.js + Payload CMS 3
This is the part that surprised me most. **Payload CMS 3** doesn't need its own server. It embeds directly *inside* your Next.js app, running at a route like `/admin`. That means:
- One codebase
- One deployment
- No separate backend to babysit
Public visitors hit the normal blog pages. I log into `/admin` and get a full content management dashboard — write posts, upload images, manage everything — without ever leaving the same app.
**Cost: ₹0** (it's open source)
## 2. Hosting: Vercel
The whole app deploys to **Vercel**, which builds and hosts both the public site and the admin panel together since they're the same app.
Free tier gives you:
- **100 GB bandwidth/month**
For a personal blog, that's a lot of headroom before you'd ever need to pay.
## 3. Database: MongoDB Atlas (Free M0 Tier)
Posts, subscriber emails, comment metadata — all of it needs to live somewhere. **MongoDB Atlas** has a permanently free tier (the M0 cluster) that gives:
- **512 MB storage**
Text-heavy data like blog posts is tiny in storage terms, so 512 MB goes a long way before you'd need to upgrade.
4. Images: Cloudinary
Every blog needs images, and images need somewhere to live that isn't your own server. **Cloudinary** handles upload, optimization, and CDN delivery — so images load fast for readers anywhere in the world.
Free tier:
- **25 GB storage/bandwidth**
That's thousands of blog images before you'd hit a limit.
## 5. Email Notifications: Resend
Here's the part I love most. When I publish a new post, I don't manually email anyone. Payload fires an `afterChange` hook the moment a post goes live, which calls the **Resend API** and automatically emails every subscriber.
Zero manual steps. Write, hit publish, and readers get notified within seconds.
Free tier:- **3,000 emails/month**
## 6. Comments and Likes: Giscus
Comments are usually the scariest part of running a blog — spam, moderation, another database table to maintain. I skipped all of that with **Giscus**.
Giscus turns **GitHub Discussions** into a comments system. Every comment on my blog is actually a GitHub Discussion thread under the hood. That means:
- **Zero database writes** — GitHub stores it all
- **Zero moderation backend** — I moderate directly on GitHub
- **Spam-resistant by default** — commenters need a GitHub login, which filters out most bots instantly
Free tier:- **Unlimited, forever free**
7. Analytics: Google Analytics 4
To actually know who's reading and what's working, **GA4** tracks traffic and behavior — completely free, another script tag added to the frontend.
## How It All Connects
Here's the flow in plain English:
1. **I write a post** in the Payload admin panel at `/admin`
2. **I hit publish** → a hook fires automatically
3. **Resend emails every subscriber** that a new post is live
4. **Readers land on the post**, tracked by GA4, served fast thanks to Cloudinary's CDN for images
5. **They comment** through Giscus, which quietly stores everything in GitHub Discussions
6. **Ads and a coffee button** sit on the page for anyone who wants to support or click through
No servers to patch. No cron jobs to babysit. No infrastructure bills arriving every month.
## The Full Stack at a Glance
Layer | Tool | Free Tier Limit |
|---|---|---|
Frontend + Admin | Next.js + Payload CMS 3 | Free |
Hosting | Vercel | 100 GB bandwidth/mo |
Database | MongoDB Atlas (M0) | 512 MB storage |
Media | Cloudinary | 25 GB storage/bandwidth |
Resend | 3,000 emails/mo | |
Comments + Likes | Giscus | Unlimited, free |
Analytics | Google Analytics 4 | Free |
## Total Monthly Cost: ₹0
Until traffic outgrows these limits — and if it does, that's a great problem, because it means people are actually reading.
If you're a developer thinking about starting a blog and assuming it needs a monthly hosting bill, a database subscription, or a comments-as-a-service plan — it doesn't, not anymore. The free tiers of modern infrastructure are generous enough to run a genuinely production-grade blog, end to end, for free.
## Migrating Into This Setup
I didn't build this from scratch on day one — I moved into it in stages, and that's probably the easier path for anyone starting from an existing blog too.
**Step 1 — Content first.** Before touching any infrastructure, I exported everything I had (old posts, images, comments if any existed) into plain markdown files. Markdown is the one format every platform can eventually read, so it made the actual migration reversible if something didn't work out.
**Step 2 — Stand up the new stack in parallel.** I didn't touch the DNS for `ananthks.dev` until Next.js + Payload + MongoDB Atlas were fully working on a temporary Vercel preview URL. Writing, publishing, and rendering posts all had to work end-to-end before anything went live.
**Step 3 — Bulk-import old posts.** Since Payload's rich text editor won't parse pasted markdown by default, I used Payload's `convertMarkdownToLexical` utility in a one-off script to convert every old markdown file into the Lexical JSON format the CMS expects, then seeded them directly into MongoDB via the Local API — no manual copy-pasting post by post.
**Step 4 — Cut over DNS last.** Only once posts were rendering correctly, images were loading through Cloudinary, and comments/email were wired up did I actually point the domain at the new deployment. That way, if anything broke, the old site was still live until the very last step.
**Step 5 — Redirects for old URLs.** If your old blog had different URL slugs, add redirects in `next.config.js` so old links (from Google, social shares, bookmarks) don't turn into dead 404s. A few lines of config saves you SEO ranking you've already earned.
## What Happens When Traffic Gets High
Every "free" limit in this stack is generous, but not infinite. Here's the upgrade path for each layer, roughly in the order you'd actually hit the ceiling:
**Vercel bandwidth (100 GB/month)** — usually the first thing to watch as traffic grows. The fix before paying anything: make sure images are served through Cloudinary (not Vercel itself), enable caching headers on static pages, and use Next.js's built-in image optimization. If you genuinely outgrow it, Vercel's Pro plan starts around $20/month and multiplies the limit significantly.
**MongoDB Atlas storage (512 MB)** — blog post text is tiny, so this usually isn't the bottleneck unless comments/analytics data is also stored here. If it fills up, the next tier (M2/M5 shared clusters) is only a few dollars a month, and migration is a simple connection-string swap — no schema changes needed.
**Resend emails (3,000/month)** — this is the one that scales directly with subscriber count, not traffic. At 3,000 free emails a month, that's roughly 100 sends/day — fine for occasional posting, tight if you post daily with a large list. Their next tier is usage-based and cheap, and no code changes are needed to upgrade, just a billing change.
**Cloudinary (25 GB storage/bandwidth)** — image-heavy blogs will hit this before text-heavy ones. Before upgrading, check if images are being served at unnecessarily large resolutions — Cloudinary's automatic format/quality optimization (`f_auto,q_auto`) alone often cuts bandwidth use by half or more.
**Giscus (GitHub Discussions)** — effectively has no real ceiling for a personal blog; GitHub's own rate limits are far beyond what any individual blog would hit.
**The general pattern:** none of these require a re-architecture when you outgrow them — every single one is a tier upgrade or a billing change within the *same* service, not a migration to a different tool. That's really the payoff of picking free tiers on infrastructure that also has a credible paid tier behind it: you get to start at ₹0 and grow into cost only when the traffic actually justifies it, without ever having to rebuild.
---
## Is It Actually Scalable?
Short answer: **yes for a personal blog, with a few real caveats worth knowing upfront** — not "infinite scale" marketing talk, but the honest architectural picture.
**What scales well without any effort:**
- **Vercel's serverless functions** auto-scale horizontally — a sudden traffic spike (say, a post goes semi-viral on Hacker News) just spins up more function instances. You don't provision anything.
- **Cloudinary and Giscus** are third-party services built for scale far beyond what a personal blog needs — you'll never architect around them.
- **Static pages** (most blog posts, once published, don't change) get cached at the edge automatically by Next.js + Vercel, so repeat traffic to the same post barely touches your backend at all.
**Where it genuinely gets tighter:**
- **MongoDB Atlas M0 connections** — the free tier caps concurrent connections (typically around 500), and serverless functions can open a *lot* of short-lived connections under bursty traffic if you're not careful. The fix isn't difficult (connection pooling via a library like `mongodb`'s built-in pooling, or a proxy like Atlas Data API), but it's a real thing to set up once traffic gets serious, not something that "just works" by default.
- **Payload's admin panel living in the same app** means every deploy rebuilds both the public site and the admin dashboard together. At blog-scale this is a non-issue (builds take seconds to a couple minutes), but it's a design tradeoff — you can't scale or deploy the CMS independently from the frontend, because they're not actually separate services.
- **Resend's email sending is not the bottleneck for reads** — it only fires once per published post, so subscriber count matters more than site traffic. This scales with *how often you post and how many people subscribe*, which is a much slower-growing number than pageviews.
**The honest ceiling:** this architecture comfortably scales to what "successful personal blog" traffic looks like — tens of thousands of monthly readers, a few thousand subscribers, regular publishing. It is **not** the architecture you'd want for a high-traffic media publication with a large editorial team, real-time breaking news, or millions of monthly visitors — at that point you'd want a dedicated database tier with proper connection pooling from day one, a CMS decoupled from the frontend (so editorial work doesn't compete with a public traffic spike for build resources), and a CDN strategy beyond what Vercel's free tier gives you.
For what this is — one person's tech blog — it scales exactly as far as it needs to, and every bottleneck above has a known, well-documented fix rather than requiring a rebuild.
---
Now I just have to actually write the posts.
Get new posts in your inbox
No spam — one email when something ships.
165 total visits