From Zero to Live: A Complete Deployment Log for a Personal Website

This is a complete deployment log for a personal website: I built a trilingual (Chinese/English/Japanese) blog at zero cost using the fully managed stack of Astro + Supabase + Cloudflare Workers. The article has two halves — the first is a from-scratch deployment walkthrough you can follow to get an identical site; the second is nine real pitfalls, each with its cause and fix. The core selling point: zero ops, publish-and-it's-live, ¥0 a month.

From Zero to Live: A Complete Deployment Log for My Personal Site

Before We Start

This piece is the site's own confession — a full account of how it went from a few lines of code on a local machine to the pages you're reading right now. It comes in two parts: the first is a step-by-step deployment walkthrough (follow it and you'll end up with an identical site); the second is an honest log of everything that broke (I hit every one of these, and I wrote down both the cause and the fix).

Who it's for: anyone who wants a personal blog without spending money — or my future self, so that the next time I feel like tearing everything down and starting over, I don't have to figure it out from scratch again.

At a Glance: What This Site Actually Is

Start with the finished product, so you have the big picture:

Item Current state
Framework Astro 4 (SSR / server-rendered mode)
Database Supabase (Postgres — posts, comments, categories, site settings all live here)
Hosting Cloudflare Workers (free tier: 100k requests/day, unlimited bandwidth, 3,000 build minutes/month)
Domain Bought separately, DNS on Cloudflare, HTTPS issued automatically
Languages Chinese / English / Japanese, one codebase
Authoring Built-in /admin panel — write and publish straight from the browser
Monthly cost $0 (the domain is the only money I've ever spent; everything else is on a free tier)

Here's the whole thing in one sentence: readers always hit Cloudflare, and Cloudflare queries Supabase live on every single request. So the moment you hit "Publish" in the admin panel, the post is live — no build queue, no redeploy. That's my favourite part of the entire design.

Reader's browser ──► Cloudflare Worker (SSR rendering + static assets)
                          │ live query on every request
                          ▼
                   Supabase (posts / comments / categories / settings)
                          ▲
Admin /admin ─────────────┘  (browser talks to Supabase directly — publish = live)

Comparing Options: Why This Stack, and Where It Falls Short

A stack comparison that only lists upsides is useless. Here's an honest side-by-side, so you can judge whether this fits you.

Against the mainstream options

Dimension This stack (Astro + Supabase + CF Workers) WordPress Next.js + Vercel Hexo / Hugo (pure static)
Server ops None, fully managed Required (or pay for managed hosting) None None (but you build locally to publish)
Publishing flow Publish from the browser, instantly live Publish from the dashboard Same Write locally → build → push
Write in the browser ✅ Built in ✅ Needs a separate CMS ❌
Database Supabase (Postgres, full SQL) MySQL Depends (often an external CMS) None (files are the content)
Free tier CF 100k req/day + Supabase free plan None (needs paid hosting) Yes (commercial use restricted) Static hosting is effectively unlimited
Access speed in your target region Depends on the region — CF's edge is patchy in some (mainland China is noticeably slow); solid in most Depends on the host Region-dependent, equally uneven Fastest if a regional CDN is available
Ecosystem / docs Astro's ecosystem is smaller; less Chinese-language material Largest — plugins and themes for days Next.js has the biggest ecosystem Static-generation ecosystem is mature
Who holds the data Supabase, exportable at any time Your own DB Depends on what you wire up It's just local files

The real advantages

  • Zero cost and zero ops. No server to patch, no backup cron to write, certificates renew themselves. The biggest hidden cost of a personal site is usually "worrying that it'll fall over" — and this stack zeroes that out too.
  • Publish and it's live. SSR queries on every request, so the second you save, readers see it. The whole "build locally → push → wait for deploy" chain simply doesn't exist here.
  • Built-in admin plus a real database. Posts, comments, a category registry, site settings — all in Postgres, with RLS drawing a very clear line around who can read what. You can also just write SQL for stats, which file-based setups can't give you.
  • Three languages from one source, SEO baked in. RSS, sitemap, canonical and hreflang come from the architecture itself, not from a pile of plugins.

Where it genuinely falls short

  • Astro's ecosystem is smaller than Next.js's. When you hit an obscure problem, there isn't much written about it in Chinese; more often than not you'll be reading English docs or asking an AI directly.
  • Access speed depends on your target region. A CDN edge is only fast where it has points of presence, so whether Cloudflare is "fast" is a question about your readers, not about Cloudflare. For a site aimed at mainland China, CF's edge there is workable but noticeably slower than Vercel/Netlify or a regional CDN. I built this site to record my own thinking and didn't chase peak speed in any one region — but if your site is meant for readers concentrated in a specific region, run that region's numbers before committing.
  • Publishing depends on your connection. The admin page talks to Supabase straight from the browser, so on a flaky network, login or save may spin. Switch to a better connection — readers never notice.
  • Free Supabase projects pause after 7 days of inactivity. You have to open the dashboard and wake it up. Irrelevant for a weekly blog; worth remembering if you post once a year.
  • No page-level caching by default. The price of zero-latency publishing is rendering on every request (in practice, SSR lands well under the 10ms ceiling). Add s-maxage yourself if you ever need caching — a personal site doesn't.
  • Long-tail risk. If Cloudflare or Supabase change their free-tier terms down the road, you may need to migrate or start paying. That's the shared risk of every free-tier setup, and it's worth going in with eyes open.

In one line: if you want a personal site that's yours, that you can keep writing on for years, and that doesn't demand maintenance — this is a great fit. If access speed in your target region is critical, or you need a rich plugin ecosystem, WordPress or a static generator will probably be less trouble.

A Short History of My Sites (Why This Is the Third One)

This isn't my first personal website.

In 2018 I stumbled into the open-source world on GitHub and followed a blog project's README step by step on my local machine. The moment it finally ran, a door opened. Later I bought my own domain and a server on Alibaba Cloud, scraped together free object storage and CDN, and deployed the site. Because of regulatory requirements, it only officially went live after completing both the MIIT ICP filing and the public-security filing. For a few years I wrote about life outside of code — until cost made me shut the site down five years later and cancel the filings.

In October 2026, AI-assisted programming had matured to the point where an ordinary person can build a system just by talking to it. I decided to try again — but this time I changed the approach completely. The usual WordPress/Hexo routes didn't fit what I wanted (one needs server ops, the other needs a local build for every post). I settled on an all-managed, no-server combination: code on GitHub, hosting on Cloudflare, data in Supabase, a static front end plus an admin page that publishes directly. For a personal site with modest traffic, the free tiers are more than enough.

And so you have the site you're reading. On to the actual steps.

Step 1: Getting Ready

Accounts you'll need

Service Purpose Cost
GitHub Code repository, deploy trigger Free
Cloudflare Hosts the Worker, DNS, HTTPS Free
Supabase Database + authentication Free
A domain Your address on the internet Nothing new if you already own one

Local environment

  • Node.js 22 (pinned by .nvmrc in the repo — just run nvm use)
  • git, npm
  • A network that can reach GitHub and Supabase without drama (more on why later)

Step 2: Set Up the Database (Supabase)

  1. Sign up at Supabase and create a New project. Pick any region (distance barely matters for a personal site), and remember the database password.
  2. Once the project is ready, open SQL Editor and run the five SQL files in the repo's supabase/ directory in this order, one at a time (all of them are idempotent — an "already exists" error is expected and fine). You can't skip any of them; leave one out and the matching admin feature will break:
    • schema.sql — the foundation. Creates the core tables (posts, comments, site settings, legacy category-link mappings), sets up the RLS policies, the comment-related RPC functions, and a public-read images bucket for pasted images. Everything else layers on top of it, so it has to run first.
    • categories.sql — the category registry. The registry is the source of truth for categories, and it supports create/rename/merge/delete; after a rename, old links 301 automatically instead of 404ing.
    • posts_author.sql — adds an author column to the posts table. Don't skip this one, or publishing from the admin panel will error out.
    • media_library.sql — the bookkeeping table behind file management. Every uploaded image or attachment records which post it belongs to, so deleting a post cleans up precisely and you don't end up with a pile of orphaned files in storage.
    • posts_trash.sql — soft deletes for posts. Deleting a post moves it to the trash, where it sits for 5 days before being purged; in the meantime you can restore it or delete it permanently.
  3. Go to Project Settings → API and copy two things you'll need shortly:
    • Project URL — looks like https://xxxx.supabase.co
    • anon public key — a long string starting with eyJ...
  4. Create the admin user: Authentication → Users → Add user, enter your email and password, and tick auto-confirm. That's your login for /admin — the site has no separate sign-up system, it just uses Supabase auth. For a one-person site there's no reason to build anything fancier.

On the safety of the anon key: it's designed to be public and ships to the browser. The real security boundary is RLS — readers can only see published posts and approved comments, and write operations are validated by the policy layer (nickname/email/content length and format). Sensitive material (API keys for AI services, etc.) goes into Cloudflare Secrets instead and never reaches the browser.

Step 3: Run the Site Locally

git clone https://github.com/<your-username>/<repo-name>.git
cd <repo-name>
nvm use          # or just make sure you're on Node 22
npm install

Copy .env.example to .env and fill in the two values from step 2:

PUBLIC_SUPABASE_URL=https://xxxx.supabase.co
PUBLIC_SUPABASE_ANON_KEY=eyJ...your anon key
# Public site URL (affects sitemap / RSS / canonical); can stay empty during local dev
PUBLIC_SITE_URL=

Then:

npm run dev      # http://localhost:4321

It'll run without a .env too — the site falls back to built-in sample posts in all three languages, so you can see the thing working first. Fill in .env when you want to test against the real database.

To preview the production build locally, use npm run build && npx wrangler pages dev dist (local Workers emulation, matching the production runtime).

Step 4: Push to GitHub

git add .
git commit -m "init"
git remote add origin https://github.com/<your-username>/<repo-name>.git
git push -u origin main

Every future "change the code" cycle looks the same: edit locally → commit → push. The push is the deploy trigger.

Step 5: Deploy to Cloudflare Workers (the core step)

⚠️ One big trap to flag up front: when you create the project, choose Workers, not Pages. This is an SSR app (it depends on the Worker runtime that the build produces), so it has to go through Workers. Choose wrong and you'll get Workers-specific command in a Pages project later — you'll have to delete the project and start over.

  1. Cloudflare Dashboard → Workers & Pages → Create → Connect to Git, authorize GitHub, pick the repo, and set the production branch to main.

  2. Build settings (choosing the Astro framework preset usually fills these in):

    • Build command: npm run build
    • Build output directory: dist
    • Deploy command: npx wrangler deploy ← driven by wrangler.toml in the repo root, don't change it
  3. Settings → Builds → Build environment variables (note: the build variable section, not Runtime variables) — add four:

    Variable Value Notes
    PUBLIC_SUPABASE_URL https://xxxx.supabase.co Required
    PUBLIC_SUPABASE_ANON_KEY eyJ... Required
    PUBLIC_SITE_URL Start with https://<project>.workers.dev Recommended; change it after binding the domain
    NODE_VERSION 22 Belt and braces with .nvmrc

    The three PUBLIC_* values are build-time inlined — they're baked into the output at the moment npm run build runs, so changing them during setup requires a redeploy to take effect. NODE_VERSION is different: it only decides which Node the build stage uses, never enters the output, and changing it also needs a redeploy.

  4. Save and Deploy, wait 1–3 minutes, and you'll get https://<project>.workers.dev.

  5. From here on: push only when you change code. Publishing a post triggers no build at all.

wrangler.toml ships with the repo. The core of it is just these lines — no manual setup needed:

name = "mystyle"
main = "dist/_worker.js/index.js"
compatibility_date = "2025-07-18"
compatibility_flags = ["nodejs_compat"]

[assets]
directory = "./dist"

The real file also has account_id and a [vars] section (the latter holds non-sensitive runtime config). account_id is your own Cloudflare account value — don't copy the one in this article; remember to update it if you switch repos or accounts.

nodejs_compat is the lifeline of SSR — without it, any Node built-in call inside the Worker returns a straight 500.

Step 6: Bind a Custom Domain

I bought my domain (a .cn) on Alibaba Cloud years ago. To hook it up:

  1. Put the domain on Cloudflare (the new path after the 2025 console redesign): in the dashboard sidebar, Domains → Onboard a domain → enter the apex domain → Continue. Choose the Free plan.
  2. Cloudflare gives you two nameservers (xxx.ns.cloudflare.com). At your registrar, change the domain's nameservers to those two addresses — every registrar has this screen somewhere, usually under something like "Domain management → DNS modification / Nameservers."
  3. Wait for the domain's status in the CF dashboard to turn Active (usually minutes to a few hours, depending on NS propagation).
  4. Workers project → Settings → Triggers → Custom Domains → Add, and enter your domain. With DNS on CF the record is configured for you and the HTTPS certificate is issued automatically — free the whole way.
  5. One last thing: change PUBLIC_SITE_URL in the build environment variables to https://your-domain, then Retry deployment (Deployments page → latest deployment → ··· → Retry). That's what makes the domain correct in the sitemap, RSS and canonical tags. Post content is unaffected.

Step 7: Log In and Publish Your First Post

Open <your-domain>/admin in a browser and sign in with the Supabase account from step 2. The admin panel has four modules:

  • Posts: title, language (Chinese/English/Japanese), category, body (Markdown with live preview), then "Publish." Publishing is live — refresh the front end and the post is already there. The editor's "More settings" also lets you change the publish time (which updates front-end display, ordering and RSS). Two things that tend to confuse people:
    • Clicking a language button switches versions. It loads that language's published title, excerpt, body and slug — it doesn't just flip a language flag. So seeing content in another language after switching is expected.
    • Deleting a post sends it to the trash. It doesn't vanish immediately: it stays for 5 days, restorable at any point, and you can also purge it by hand.
  • Files: upload images and attachments; each file automatically records which post it belongs to, and deleting the post cleans up accordingly — no orphaned files left behind in storage.
  • Comments: moderate, reply (admin replies get a distinct badge and are shown nested), delete.
  • Other: homepage countdown, excerpt generation settings, category management (renaming triggers an automatic 301), and site avatar upload (including switching between and reviewing previous avatars).

A quick note on how comments are designed: the comments table collects an email but never exposes it — only the site owner sees it in the admin panel. Anonymous commenters get a deletion token so they can manage their own comments. Nobody has to register.

Step 8: Three Languages and SEO

Multilingual support is native to this architecture — no extra service required: each language has its own RSS feed, there's a whole-site sitemap, and canonical plus hreflang are wired up. Each language version is a separate record, so you write and publish them separately from the admin panel. If a language version hasn't been published yet, the front end shows a notice banner and keeps the versions that do exist — no blank pages.

What Actually Broke (the Most Valuable Part of This Article)

Every item below is something I hit for real, written up as symptom → cause → fix.

1. Created the project as Pages; the deploy failed immediately Symptom: Workers-specific command in a Pages project. Cause: This is an SSR app (the output includes a Worker runtime), and the deploy command is npx wrangler deploy — that's the Workers deployment model. The Pages build model isn't compatible with it. Fix: Choose Workers → Connect to Git when creating the project. Not Pages.

2. The whole site returned 500; the logs mentioned a Node built-in module Cause: The nodejs_compat compatibility flag was missing, so the Node capabilities SSR depends on weren't available in the Worker runtime. Fix: Add compatibility_flags = ["nodejs_compat"] to wrangler.toml (already in the repo).

3. Wrong domain in the sitemap / RSS Symptom: A your-domain.com placeholder in the output, or a live canonical pointing at example.com. Cause: PUBLIC_SITE_URL wasn't set. And there's a subtler layer — Astro doesn't inject .env into process.env at the time astro.config.mjs loads; only import.meta.env sees those values. So reading process.env.PUBLIC_SITE_URL directly in the config file silently gets an empty string and falls back to the default site address. Fix: Use Vite's loadEnv in astro.config.mjs to read file-based env vars explicitly, while still honouring process.env build variables from CI — cover both paths. Then Retry deployment.

4. Publishing from the admin panel errored (missing author column) Cause: The posts table didn't originally have an author column, but the publish logic requires it on write. Fix: Run supabase/posts_author.sql to add it. This is exactly why step 2 insists that not one of the five SQL files can be skipped — drop any one and some admin feature breaks.

5. Environment variables in the dashboard "mysteriously reverted" Symptom: You change a config variable's value in the Cloudflare dashboard, and some time later it's back to the old value. Cause: The deploy command is npx wrangler deploy, and wrangler.toml's [vars] gets pushed on every deploy — so a plain variable edited in the dashboard can be overwritten by the value in the repo. (Secrets aren't affected, which is why the pattern often looks like "the public variable rolled back but the key didn't" — an odd combination.) Fix: Keep wrangler.toml in the repo and the dashboard variables in sync. I ended up setting the toml to the same values as the dashboard so neither side can quietly overwrite the other. And remember to Retry deployment for config changes to take effect.

6. In some regions, the admin panel depends on your connection Cause: The admin page talks to Supabase (*.supabase.co) directly from the browser, and a direct connection to that endpoint is unreliable from a number of networks — mainland China is the case I know best, but it isn't the only one. Readers go through Cloudflare's edge and are completely unaffected. Fix: Just log into the admin panel on a good connection when you post — readers never notice. This is also a consequence of the architecture: the read and write paths are separated, so the reader side doesn't depend on Supabase being reachable.

7. Do I need to worry about Workers' free-tier limits? Real-world numbers: builds take 1–3 minutes each, and 3,000 minutes/month is effectively infinite for a personal site; the runtime allows 100k requests/day and 10ms CPU per invocation, and SSR rendering is typically far below that 10ms. The only thing to remember is "builds only happen when code changes" — publishing a post burns no build minutes.

Cost Breakdown

Item Cost
Domain The only money ever spent (bought years ago, renewed annually)
Cloudflare Workers / DNS / HTTPS $0
Supabase (database + auth + image hosting) $0 (free tier)
GitHub $0 (public repo)
Total monthly cost $0

Day-to-Day Maintenance

  • Writing or editing posts: do it in /admin. Instant, zero deploys.
  • Changing code, styles or features: edit locally → push → CF builds automatically (1–3 minutes) → done.
  • Changing configuration (env vars, build settings): edit in the Cloudflare dashboard → Retry deployment.
  • Backups: Supabase lets you export at any time. The posts themselves are Markdown text — own the database and you own everything.
  • Certificates and renewals: Cloudflare handles it. Nothing to do.

Closing

From a 2018 Alibaba Cloud server that needed manual ops to this 2026 all-managed setup where "push is deploy, publish is live" — what changed isn't just the stack, it's the bar for owning a personal website. It used to require being a sysadmin; now a GitHub account is enough. And this rebuild was done entirely with an AI at my side: I'm no Supabase expert, but I knew which questions to ask.

If you build one by following this, come say hi in the comments.

As always. One thread through it all.

Comments0

    ← Back to home