When I was building B&B websites for small Taiwanese guesthouses, a recurring problem came up: the owners wanted to update their own room information, pricing, and seasonal promotions. Not through me. Not by waiting a day and paying for an hour of engineering time. Themselves.
The standard answer to this problem is a CMS. But I kept running into a mismatch between what CMS products assume and what these owners actually needed. Most of them had never used a content management system. The mental model of “content collections” and “entries” doesn’t map naturally to how a guesthouse owner thinks about their business. What they understood was: “I want to change the price for this room type for summer.”
So I tried a different approach: what if the admin panel just wrote directly to the repo?
The Idea
GitHub is already storing the site’s content and triggering deploys on push. So instead of adding a database layer, I can use the GitHub API as the persistence layer. The admin panel reads the current config from the repo, lets the client edit it, and writes the changes back as a commit. Firebase Hosting picks it up via CI and redeploys in about a minute.
No database to maintain. Changes are version-controlled automatically. Rollback is just a git revert. For a guesthouse owner updating seasonal promotions four times a year, the one-minute deploy lag is completely invisible — they hit save, go answer a customer call, come back and refresh, and the change is live.
Auth First
The admin is a separate Astro page protected by Google OAuth. The client logs in with their Google account — whichever email I’ve allowlisted. Once authenticated, the app exchanges tokens and stores a refresh token so they don’t have to re-login every session.
This is the part that took the most time to get right. Google’s OAuth refresh token only comes through on the first authorization, so I had to make sure to store it on the initial handshake and not throw it away.
Using Google OAuth served a practical purpose beyond security: every one of these owners already had a Google account for Gmail or Maps. No new credentials to manage, no “forgot my CMS password” support calls.
Reading the Current Config
On load, the admin fetches the current banner config from GitHub using Octokit:
import { Octokit } from "@octokit/rest";
const octokit = new Octokit({ auth: process.env.GITHUB_TOKEN });
async function getBannerConfig() {
const { data } = await octokit.repos.getContent({
owner: "wayne2002tw",
repo: "my-client-site",
path: "src/data/banner.json",
ref: "deploy",
});
if ("content" in data) {
const content = Buffer.from(data.content, "base64").toString("utf-8");
return { config: JSON.parse(content), sha: data.sha };
}
}
The sha is important — you need it to update the file later. GitHub uses it to detect conflicts.
Saving Changes
When the client hits save, two things happen: the image gets uploaded (if they changed it), then the JSON config gets updated. Both go in as separate commits, or I could batch them — I went with separate for clarity.
Image upload is where the base64 encoding comes in. GitHub’s API expects file contents as base64:
async function uploadImage(file: File, path: string) {
const buffer = await file.arrayBuffer();
const base64 = Buffer.from(buffer).toString("base64");
await octokit.repos.createOrUpdateFileContents({
owner: "wayne2002tw",
repo: "my-client-site",
path,
message: "chore: update banner image",
content: base64,
branch: "deploy",
});
}
Then updating the JSON config follows the same pattern, including the sha from the earlier read to avoid overwriting concurrent changes.
What Actually Works Well
The version control aspect is genuinely useful. When the client accidentally uploaded a blurry image and asked me to “undo it,” I just ran a git revert. No backup system needed. The commit history also doubles as an audit log — I can see exactly when prices were changed and what they were before.
The GitHub API rate limits are 5000 requests per hour for authenticated apps, which is completely fine for an admin panel that maybe sees 10 requests on a busy day.
Deploys are automatic and fast. The client edits the banner, hits save, waits 60 seconds, refreshes the live site. That feedback loop is actually shorter than most CMS setups I’ve worked with — there’s no publish/draft workflow to navigate, no cache purge to trigger manually.
The Rough Edges
Conflict handling is last-write-wins. If two people edit simultaneously — which never happens on a single-owner B&B site, but still — one of them will get a 409 from GitHub and the app shows an error. For this use case that’s fine. For a multi-editor scenario, I’d need to rethink.
The base64 encoding for images adds some awkward size considerations. GitHub has a 100MB file limit, but more practically, large images slow down the repo clone over time. I added a client-side resize step to cap uploads at 1MB before encoding.
Setting up the GitHub token with the right scopes (contents: write on the specific repo) took a bit of fiddling. I ended up using a fine-grained personal access token scoped to just that one repository, which felt safer than a classic token with broad permissions.
The Bigger Point
The real shift this created wasn’t technical. Before this system, updating room pricing or seasonal promotions required the owner to contact me, wait for my availability, and pay for a change that took fifteen minutes to make. It put a professional dependency on something they needed to do several times a year.
After: they open the admin page, make the change, and it’s live in sixty seconds. They own their own content.
For the guesthouses I worked with, that shift in ownership — not the Astro build, not the Cloudflare CDN, not the WebP images — is the actual deliverable. The GitHub API approach made it possible to give them that without the overhead of a CMS platform neither of us wanted to maintain.
Overall, for the right use case — small static site, single owner, infrequent updates — this approach is surprisingly clean. The absence of a database to maintain is a feature, not a limitation.