DEV Community

Cover image for I built a CMS that fits in one file — here's why and how
Gerwin Weiher
Gerwin Weiher

Posted on

I built a CMS that fits in one file — here's why and how

Every CMS I've tried for Astro comes with the same overhead: a database to provision, a media CDN to configure, an auth service to wire up. For small sites and personal projects, that's a lot of ops for not a lot of content.

So I built Orbiter — a CMS where everything lives in one .pod file. Content, media, schema, users, sessions. All of it.

The idea

your-site/
├── astro.config.mjs
├── content.pod    ← your entire CMS
└── src/pages/
Enter fullscreen mode Exit fullscreen mode

The .pod extension is just SQLite. You can open it with any SQLite browser. Copy it, your whole site moves with it. Back it up with cp.

Setup

npm install @a83/orbiter-integration
Enter fullscreen mode Exit fullscreen mode
// astro.config.mjs
import orbiter from '@a83/orbiter-integration';

export default defineConfig({
  output: 'server',
  integrations: [orbiter({ pod: './content.pod' })],
});
Enter fullscreen mode Exit fullscreen mode

That's it. Orbiter injects a full admin UI at /orbiter using Astro's injectRoute — no files added to your src/pages.

Reading content

The virtual module mirrors Astro's own content collection API:

---
import { getCollection, getEntry } from 'orbiter:collections';

const posts = await getCollection('posts');
const post  = await getEntry('posts', 'my-first-post');
---
Enter fullscreen mode Exit fullscreen mode

For multilingual sites there are getLocaleCollection and getLocaleEntry helpers using a slug--locale convention.

What's in the admin

  • Collection editor — rich text (Markdown + live preview), all common field types
  • Schema editor — add or change fields without migrations
  • Media library — stored as BLOBs directly in the pod
  • Version history — every save creates a snapshot
  • JSON APIGET /orbiter/api/[collection] with optional Bearer token
  • Import — WordPress WXR and Markdown files
  • GitHub sync — for serverless environments where the filesystem is ephemeral
  • PWA — the admin is installable on mobile

The tradeoffs I'm honest about

SQLite BLOB storage is convenient but not for large media libraries. If you have thousands of high-res images, this isn't the right tool. For a typical content site it's fine.

Serverless filesystems are ephemeral. Netlify and Vercel don't persist writes between deploys. The GitHub sync mode works around this — pack your pod and media into a git commit, rebuild on push — but it adds steps. I'm still not 100% happy with this DX.

WAL mode helps with concurrent reads but SQLite is still SQLite. For a site with hundreds of simultaneous admin users this is the wrong choice. For a team of 1–5 editing content, it's perfectly fine.

Why not Keystatic / Decap / Tina?

All great tools. The difference: they store content as files in your git repo, which means your database is your git history. That's a valid approach but it means every content change is a commit, and media files bloat your repo fast.

Orbiter's approach is the opposite: one binary blob, no git history for content, trivial to move.

Try it

git clone https://github.com/aeon022/orbiter.git
cd orbiter && npm install && npm run seed && npm run dev
Enter fullscreen mode Exit fullscreen mode

Admin at http://localhost:8080/orbiter — login: admin / admin

Or scaffold a new project:

npm install -g @a83/orbiter-cli
orbiter init my-site
cd my-site && npm install && npm run dev
Enter fullscreen mode Exit fullscreen mode

GitHub: https://github.com/aeon022/orbiter

Top comments (0)