Free personal blog: why I chose Next.js, Supabase and Vercel

A free personal blog with Next.js, Supabase and Vercel: how it compares with WordPress and static sites, the free plan limits, and how many posts fit in 500 MB.

Want a free personal blog that you can write in from the browser, with images and good SEO? Next.js + Supabase + Vercel is a sensible choice, because all three have free plans that are enough. This post explains why Tech Notes picked this trio, and where that "free" runs out.

Start from a list of requirements

This is the first post in the series Building a blog with Next.js + Supabase. What makes this series different is that the blog it builds is the blog you're reading right now. Every decision and every bug in the series comes from building Tech Notes.

Before choosing any technology, I sat down and wrote out what I needed. It sounds obvious, but skip this step and most people end up picking whatever was talked about most that week. So Tech Notes listed its needs in this order:

  1. It costs nothing while there are few readers, and I only pay once the blog really grows.
  2. I can write and publish from the browser, even on my phone, without opening code or typing commands.
  3. Posts have images, and I don't have to worry about running out of space.
  4. Good SEO: pages load fast, Google can read the content, and links shared on social media show a preview image.
  5. I own the data and the design, so I can add a newsletter or an interview question bank myself.
  6. I learn skills used in real jobs: React, SQL, permissions, deployment.

The last one is a very developer-specific reason. If you only want to write and don't care about code, the answer at the end of this post is completely different.

Four options I considered

The first option was a ready-made platform such as Medium or Substack (or Viblo, a popular Vietnamese developer platform). It's free, you can publish your first post in five minutes, and readers are already there. But the design, the data and the domain aren't yours, and if you want a feature of your own, you're stuck. It fails requirements 5 and 6.

The second option was WordPress. It has a good editor and thousands of plugins, and you don't need to know how to code. However, it needs a server running PHP and MySQL. Free hosting is usually slow, adds ads or limits all sorts of things; reliable hosting costs money every month. And customizing it deeply means learning PHP and WordPress's own plugin system, which is worth fairly little when you work at a company that uses React.

The third option was a static site: each post is a Markdown file in Git, built with Astro, Hugo or Next.js. It's free, very fast and great for SEO, and honestly it's the most attractive option.

What stopped Tech Notes from choosing it was the publishing flow: edit a file, commit, push. Imagine spotting a typo while you're on the bus and having to open your laptop to fix one word. Images also live in the repo, so the repo keeps growing year after year.

The fourth option was Next.js + Supabase + Vercel. Posts live in a database, so writing, editing and publishing all happen in the browser. Images live in a separate file store.

Pages are still generated ahead of time and cached, so they load just as fast as a static site. The price is having to build what WordPress gives you out of the box: an editor, login, permissions, SEO. Then again, that's exactly what this series is about.

Tech Notes chose the fourth option. Not because it's the best, but because it's the only one that met all six of my requirements.

What does each piece do?

Next.js is a React framework for building websites. Apart from the interface, the most important thing it does for a blog is to generate the HTML on the server before sending it to the browser. That way Google can read the content right away, and readers see the text before the JavaScript has finished loading. Tech Notes uses the App Router, the newer way of organizing a Next.js app, where every folder in src/app is a path on the website.

Supabase is PostgreSQL with a few services around it. The blog uses three of them. The database holds posts, topics, interview questions and newsletter subscribers. Auth handles login for the author, while readers don't need an account. Storage is the file store for images, and the database only keeps the path to each image.

Vercel takes the code from GitHub, builds it and puts it online. Every merge into the main branch produces a new version, and every pull request gets its own preview URL, which is very handy for checking things before merging.

Put together, here's what happens when someone reads a post on Tech Notes. You open a post's URL. Vercel immediately returns the HTML it generated last time. If that page is more than an hour old, you still get the old version, but in the background Vercel asks Next.js to regenerate it: read the latest post from Supabase, build fresh HTML, and cache it for the next reader. Images in the post load straight from Supabase Storage.

That "serve the old page, quietly build a new one" mechanism is called ISR. It's the reason a blog that reads from a database is still as fast as a static site.

What exactly is free?

Free always has limits. The numbers below come from the official pricing pages, checked on 10 November 2026. They change over time, so if you're reading this later, check them again.

On Supabase's Free plan: a 500 MB database per project, 1 GB of file storage, 5 GB of egress (data downloaded) plus 5 GB of cached egress, and at most 2 active projects. There are 50,000 monthly active users, but on a blog only the author logs in, so you'll barely touch that number.

One Supabase rule is easy to miss: free projects are paused after 1 week of inactivity. A blog with regular readers doesn't need to worry. A blog left alone for a week has to be switched back on from the dashboard.

On Vercel's Hobby plan: 100 GB of Fast Data Transfer a month, 1 million function invocations and 1 million CDN requests, 5,000 image transformations through Image Optimization, and runtime logs kept for only 1 hour.

The most important Vercel rule isn't a number at all: the Hobby plan is for personal, non-commercial use only. A personal blog is fine. Running ads, selling courses or using it for a company means moving to the Pro plan. If you plan to make money from your blog, sorting this out at the start is much easier than doing it once you already have readers.

How many posts fit in 500 MB?

Everyone asks this, and it's usually answered by gut feeling. I prefer to measure, so I wrote a small script called estimate.ts. It does three calculations. First, it works out how many bytes one post takes. Then it divides the free plan's limits by that number, for three ways of storing posts: text only, original images stored in the database too, and compressed images kept in a separate file store.

estimate.ts
// Estimate: how many posts fit in Supabase's Free plan (500 MB database, 1 GB file storage)?
//   node estimate.ts                  # use the default numbers
//   node estimate.ts my-post.md       # measure a real Markdown post of yours
import { readFileSync } from "node:fs";

const MB = 1024 * 1024;
const LIMITS = { databaseMb: 500, storageMb: 1024 }; // supabase.com/pricing, Free plan (11/2026)

// Measured on a real Tech Notes draft: 1,302 words ≈ 7,972 bytes (Vietnamese letters with diacritics take 2–3 bytes each).
const MEASURED = { words: 1302, bytes: 7972 };

const file = process.argv[2];
const bytesPerWord = file
  ? Buffer.byteLength(readFileSync(file, "utf8")) / readFileSync(file, "utf8").split(/\s+/).filter(Boolean).length
  : MEASURED.bytes / MEASURED.words;

// Assumptions (change them to fit your blog):
const WORDS_PER_POST = 3000; // a long, tutorial-style post
const OVERHEAD = 2; // double it for title, description, indexes, old versions… (a cautious guess)
const IMAGES_PER_POST = 3;
const PHOTO_MB = 4; // an uncompressed phone photo (example)
const COMPRESSED_KB = 300; // the same photo as WebP, 1600px wide (example)

const postBytes = bytesPerWord * WORDS_PER_POST * OVERHEAD;
const fmt = (n: number) => Math.floor(n).toLocaleString("en-US");

console.log(`One word ≈ ${bytesPerWord.toFixed(1)} bytes → a ${WORDS_PER_POST}-word post ≈ ${fmt(postBytes / 1024)} KB (with x${OVERHEAD} overhead)`);
console.log(`\n1) Text only, in a 500 MB database: ~${fmt((LIMITS.databaseMb * MB) / postBytes)} posts`);

const rawImages = IMAGES_PER_POST * PHOTO_MB * MB;
console.log(`2) ${IMAGES_PER_POST} original photos (${PHOTO_MB} MB each) stored in the database too: ~${fmt((LIMITS.databaseMb * MB) / (postBytes + rawImages))} posts`);

const compressed = IMAGES_PER_POST * COMPRESSED_KB * 1024;
console.log(`3) ${COMPRESSED_KB} KB compressed images in 1 GB Storage, database keeps only the path:`);
console.log(`   database: ~${fmt((LIMITS.databaseMb * MB) / postBytes)} posts · storage: images for ~${fmt((LIMITS.storageMb * MB) / compressed)} posts`);

The bytes per word were measured on a real draft from this blog, which is written in Vietnamese, where every letter with a diacritic takes 2–3 bytes in UTF-8. The numbers under "Assumptions" are my own estimates; change them to fit your blog.

The script runs directly with Node.js 22.18 or later, with nothing to install. With the default numbers, the output is:

node estimate.ts
One word ≈ 6.1 bytes → a 3000-word post ≈ 35 KB (with x2 overhead)

1) Text only, in a 500 MB database: ~14,271 posts
2) 3 original photos (4 MB each) stored in the database too: ~41 posts
3) 300 KB compressed images in 1 GB Storage, database keeps only the path:
   database: ~14,271 posts · storage: images for ~1,165 posts

The second line is the one worth a close look. Text takes almost no space: write one long post every day and it would take decades to fill 500 MB. But put the original images in the database and you're done after about 41 posts. Images are what eat the space, not text.

That leads straight to a design decision: images must live outside the database, they must be compressed before upload, and the database only keeps their path. Do that and the real limit becomes the 1 GB of Storage, enough images for over a thousand posts, with more room if you compress harder.

If you want to measure with your own writing, save any post as a Markdown file, say my-post.md, and run node estimate.ts my-post.md. The script then counts the words and bytes in that file instead of using Tech Notes' default measurement, and recalculates all three lines.

Five minutes a month to avoid surprises

An estimate is only the first step. Once the blog is running, look at the real numbers once a month. On Supabase, that's your organization's Usage page, which shows how much database, storage and egress you've used this billing period. For a blog, the number most worth watching is storage.

On Vercel, it's the Usage tab, which shows data transfer, function invocations and image transformations. A blog with few readers usually only touches a small corner of these limits.

When a number passes about half its limit, that's the time to look for the cause, not to upgrade. The usual suspects are images uploaded without compression, old images no longer used but still in storage, or a page that queries the database on every view instead of using a pre-generated page. All three can be fixed in code, and each gets its own post in the series.

One more small habit: write down the date you checked the limits, as I did above. Free plans change all the time. A number that's right today can be wrong a while later, and readers need to know when it was checked.

Three things that weren't in the plan

The requirements list at the top of this post looks tidy, but building the real thing always brings surprises. Here are three that happened while building Tech Notes, and all three come from this very choice of stack.

First, images have to be compressed in the browser. After the estimate above, it was obvious that phone photos of several MB each would use up 1 GB very quickly. So the editor now resizes images and converts them to WebP before uploading, and Storage only ever receives the compressed version.

Why not compress on the server, which would be simpler? Because then the original image would still have to travel over the network and run through a Vercel function first, using both bandwidth and function time, exactly the two things the free plan limits.

Second, share images only existed on post pages. Every post has its own preview image when you paste its link on social media. But after deploying I found that pasting the link to the home page or the interview questions page showed no image at all.

No error, no warning. A missing share image doesn't break the post, but without that visual preview the link looks bare. The lesson I took from it is that things that "don't throw an error when they're missing" have to be checked on purpose, on every kind of page. A later post in the series goes into this in more detail.

Third, Vercel's analytics only work on Vercel. After adding Vercel Analytics, running the site locally made the browser report a 404 when loading the analytics script, because that script is only served on Vercel's infrastructure. The fix is simple: only add the script when the code is running on Vercel, that is, when the VERCEL environment variable is set. A small thing, but it reminded me that whenever you rely on a platform's own services, your local environment and the real one will differ in unexpected places.

None of these is a reason to drop this stack. But they are the reason I always spend a few minutes checking after every deploy, instead of trusting "it works on my machine".

When shouldn't you choose this stack?

If you just want to write, don't build anything yourself. Substack, Medium or WordPress will give you far more time to write posts, which matters more than any technology choice.

If the blog is commercial, as I said, Vercel's Hobby plan doesn't allow it. Budget for the Pro plan or pick another host from the start.

If you don't want to run things yourself, think twice as well. The database, permissions and backups are all your job, and one permissions mistake can leak your drafts or let a stranger edit your posts. And if you don't know React yet, I'd suggest learning it on a smaller project first.

If you're preparing for interviews, "why not store images in the database?" comes up a lot, and there's a ready answer in the interview question bank.

Finally, setting up the project cleanly

The stack is chosen. The next question is where to start: how to create the Next.js project, which folder is for what, and how to write the first home page so that adding a few dozen pages later doesn't turn into a mess.

The next post creates the project with the App Router, explains the folder structure Tech Notes uses, and puts the first page on Vercel. The whole series is at Building a blog with Next.js + Supabase.

Get the next post by email.

One email a week, only when there is something worth reading. No spam.