Portable single-file CMS for Astro — everything in one
.podfile.
Content, media, schema, config, users — all in one SQLite database. Copy the file and your entire site moves with it. No cloud. No API keys. No external services.
your-site/
├── astro.config.mjs
├── content.pod ← your entire CMS lives here
└── src/
└── pages/
└── blog/
└── [slug].astro
Status: Active development — Phase 4 in progress. Not yet published to npm. Use the demo to try it locally (see Quick Start below).
The fastest way to try Orbiter is to clone this repository and run the demo app. You need Node.js 20+ and npm 10+ installed.
Step 1 — Clone the repository
git clone https://github.com/aeon022/orbiter.git
cd orbiterStep 2 — Install dependencies
npm installThis installs everything for all packages and the demo app in one step (npm workspaces).
Step 3 — Create the demo database
npm run seedThis creates apps/demo/demo.pod — a pre-filled SQLite database with:
- Sample collections: Posts, Pages, Authors, Events, Event Categories, Post Categories
- Demo content entries (published + drafts)
- Two placeholder images in the media library
- One admin user: admin / admin
Step 4 — Start the dev server
npm run devThe server starts at http://localhost:8080.
Step 5 — Open the admin
Go to http://localhost:8080/orbiter and log in:
Username: admin
Password: admin
You're in. Explore the dashboard, open a collection, edit an entry, upload a file, or try switching themes in Settings.
Step 6 — View the frontend
The demo site renders the content at http://localhost:8080. Browse posts and events to see the CMS content on a live Astro site.
Orbiter is a CMS that fits entirely into a single file. Most CMS tools depend on a hosted database, a separate API server, or a third-party cloud service. Orbiter uses SQLite — a single file on disk that contains everything.
What that means in practice:
- Zero infrastructure. No database server to run. No connection strings to configure. No cloud account required.
- Fully portable. Move your site to a new server by copying one file. Backup with
cp content.pod content.pod.bak. - Self-contained. Schema, content, media, users, sessions — everything in one place.
- Inspectable. Open the file with any SQLite tool and query your content directly.
- Admin included. A full admin UI at
/orbiter— editor, media library, build trigger, settings, schema editor.
Orbiter is built for Astro. It integrates directly with Astro's content layer and provides a virtual module (orbiter:collections) that works like Astro's own getCollection and getEntry APIs.
A .pod file is a standard SQLite database with a custom extension. The extension is purely cosmetic — any SQLite tool can open it.
When you add Orbiter to your Astro project, the integration reads from the .pod file at build time (to generate static pages) and at runtime (for the admin UI and media serving).
content.pod
├── _meta → site config, locale, build webhook URL
├── _collections → schema definitions (JSON per collection)
├── _entries → all content from all collections
├── _versions → full version history per entry
├── _media → uploaded files stored as BLOBs
├── _users → admin users (hashed passwords)
└── _sessions → active login sessions (auto-pruned)
The integration provides two virtual modules:
| Module | Usage |
|---|---|
orbiter:collections |
Read published content in Astro pages (build-time) |
orbiter:db |
Access the pod path in admin routes (runtime) |
orbiter:collections is a static snapshot — it reads all published entries from the pod when Astro builds and inlines them as a JavaScript module. Your Astro pages import from it the same way you would from astro:content.
The integration injects a complete admin UI under /orbiter using Astro's injectRoute API. No files are added to your src/pages directory.
| Route | Page |
|---|---|
/orbiter |
Dashboard |
/orbiter/[collection] |
Collection list |
/orbiter/[collection]/[slug] |
Entry editor |
/orbiter/media |
Media library |
/orbiter/media/[id] |
Serve a media file (BLOB → HTTP) |
/orbiter/schema |
Schema editor |
/orbiter/settings |
Site settings |
/orbiter/build |
Build trigger |
/orbiter/login |
Login page |
/orbiter/setup |
First-run setup wizard |
/orbiter/search |
Command palette search API |
If you want to add Orbiter to your own Astro project (rather than the demo):
npm install @a83/orbiter-core @a83/orbiter-integration @astrojs/node@^9@astrojs/node@^9 is the adapter for self-hosted Node.js deployments — it targets Astro 5. (@astrojs/node@^10 requires Astro 6 and is not compatible.) See Adapters & Deployment for alternatives (Netlify, Vercel, Docker).
Note: Not yet published to npm. Until the first release, install from this repository using workspaces or
npm link.
Run this once to initialize your .pod and create an admin user:
// scripts/setup.js
import { createPod, hashPassword } from '@a83/orbiter-core';
import { randomUUID } from 'node:crypto';
const db = createPod('./content.pod', {
site: {
name: 'My Site',
url: 'https://example.com',
description: 'My Astro site powered by Orbiter',
locale: 'en',
}
});
const hash = await hashPassword('change-me');
db.insertUser(randomUUID(), 'admin', hash, 'admin');
db.close();node scripts/setup.js// astro.config.mjs
import { defineConfig } from 'astro/config';
import orbiter from '@a83/orbiter-integration';
import node from '@astrojs/node';
export default defineConfig({
output: 'server', // required — admin UI needs SSR
adapter: node({ mode: 'standalone' }),
integrations: [
orbiter({ pod: './content.pod' }),
],
});
output: 'server'is required. The admin routes are server-rendered. See Adapters & Deployment for all supported hosting options.
Alternatively, use output: 'hybrid' to pre-render your public pages while keeping the admin dynamic:
export default defineConfig({
output: 'hybrid', // public pages static, admin routes SSR
adapter: node({ mode: 'standalone' }),
integrations: [
orbiter({ pod: './content.pod' }),
],
});In hybrid mode, your own Astro pages default to prerender: true (static). All Orbiter admin routes export prerender = false automatically — no extra configuration needed.
Collections can be created in the Schema editor in the admin UI, or programmatically:
import { openPod } from '@a83/orbiter-core';
const db = openPod('./content.pod');
db.db.prepare(`
INSERT OR IGNORE INTO _collections (id, label, schema)
VALUES (?, ?, ?)
`).run('posts', 'Posts', JSON.stringify({
title: { type: 'string', required: true, label: 'Title' },
excerpt: { type: 'string', required: false, label: 'Excerpt' },
body: { type: 'richtext', required: false, label: 'Body' },
tags: { type: 'array', required: false, label: 'Tags' },
image: { type: 'media', required: false, label: 'Cover Image' },
category: {
type: 'select',
required: true,
options: ['news', 'tutorial', 'opinion'],
optionLabels: { news: 'News', tutorial: 'Tutorial', opinion: 'Opinion' },
},
}));
db.close();Start your dev server:
npx astro devGo to http://localhost:4321/orbiter and log in with the credentials from step 1. Use the Schema editor to adjust field definitions, the Collection list to create entries, and the Editor to write content.
---
// src/pages/blog/index.astro
import { getCollection } from 'orbiter:collections';
const posts = await getCollection('posts');
// → array of all published entries, newest first
---
<ul>
{posts.map(post => (
<li>
<a href={`/blog/${post.slug}`}>{post.data.title}</a>
<p>{post.data.excerpt}</p>
</li>
))}
</ul>---
// src/pages/blog/[slug].astro
import { getCollection, getEntry } from 'orbiter:collections';
export async function getStaticPaths() {
const posts = await getCollection('posts');
return posts.map(post => ({ params: { slug: post.slug } }));
}
const { slug } = Astro.params;
const post = await getEntry('posts', slug);
---
<article>
<h1>{post.data.title}</h1>
<div set:html={post.data.body} />
</article>This module is the bridge between your CMS and your Astro pages. It is generated at build time and contains a frozen snapshot of all published entries.
// Get all published entries in a collection
getCollection(name: string): Promise<Entry[]>
// Get a single entry by slug
getEntry(collection: string, slug: string): Promise<Entry | null>
// Default locale (from Settings → Default Language)
locale: string // e.g. "de"
// All configured locales (from Settings → All Languages)
locales: string[] // e.g. ["de", "en"]{
id: string, // UUID
slug: string, // URL-safe identifier
status: 'published',
created_at: string, // ISO datetime
updated_at: string, // ISO datetime
data: {
// all fields defined in this collection's schema
title: string,
body: string, // richtext fields: Markdown-rendered HTML
image: string, // media fields: UUID → use /orbiter/media/{id}
tags: string[], // array fields: string array
// relation fields: resolved to full Entry objects (not just IDs)
author: Entry,
// ...
}
}Media files are served from /orbiter/media/[id]. Build a full URL like this:
<img src={`/orbiter/media/${post.data.image}`} alt={post.data.image_alt} />Relation fields are resolved at build time. The raw value (an array of UUIDs) is replaced with the actual referenced Entry objects before the module is generated. You can access them directly:
---
const posts = await getCollection('posts');
---
{posts.map(post => (
<div>
<h2>{post.data.title}</h2>
<!-- post.data.author is the full Author entry, not a UUID -->
<p>by {post.data.author?.data?.name}</p>
<!-- post.data.categories is an array of Category entries -->
{post.data.categories?.map(cat => (
<span style={`background:${cat.data.color}`}>{cat.data.name}</span>
))}
</div>
))}Fields are defined as a JSON object on each collection. Each key is a field name; the value is a field definition object.
| Type | Input | Stored as |
|---|---|---|
string |
Single-line text | TEXT |
richtext |
Block editor | Markdown TEXT |
number |
Numeric input | TEXT (as string) |
url |
URL input with validation | TEXT |
email |
Email input with validation | TEXT |
date |
Date picker | TEXT (ISO date) |
datetime |
Date + time picker | TEXT (ISO datetime) |
select |
Dropdown | TEXT (option key) |
array |
Tag input (comma-separated) | TEXT (JSON array) |
weekdays |
Weekday multi-select | TEXT (JSON array) |
media |
Media library picker | TEXT (media UUID) |
relation |
Entry picker from another collection | TEXT (JSON array of UUIDs) |
{
// All fields support:
type: 'string', // required — field type (see table above)
label: 'Post Title', // optional — display name in the editor (defaults to the key name)
required: true, // optional — marks field as required in the editor
// select fields also support:
options: ['news', 'event', 'review'],
optionLabels: { news: 'News', event: 'Event', review: 'Review' },
// relation fields also support:
collection: 'authors', // which collection to pick entries from
multiple: true, // allow multiple selections (default: true)
// Conditional visibility — show this field only when another field has a specific value:
showWhen: 'category:event', // syntax: 'fieldName:value'
}{
title: { type: 'string', required: true, label: 'Post Title' },
slug: { type: 'string', required: true, label: 'URL Slug' },
excerpt: { type: 'string', required: false, label: 'Short Summary' },
body: { type: 'richtext', required: false, label: 'Body' },
author: { type: 'relation', collection: 'authors', multiple: false, label: 'Author' },
categories: { type: 'relation', collection: 'post_categories', label: 'Categories' },
cover: { type: 'media', label: 'Cover Image' },
tags: { type: 'array', label: 'Tags' },
status_note:{ type: 'string', showWhen: 'status:draft', label: 'Internal Note' },
published_at: { type: 'datetime', label: 'Publish Date' },
category: {
type: 'select',
required: true,
options: ['news', 'tutorial', 'opinion'],
optionLabels: { news: 'News', tutorial: 'Tutorial', opinion: 'Opinion' },
},
}The dashboard shows:
- Stats — entry counts per top-level collection (published / draft)
- Recent — last edited entries across all collections
- Notes — a persistent scratchpad stored in
localStorage - Todos — a simple task list stored in
localStorage - Deploy status — last build trigger result + timestamp
Each collection has a list view showing all entries with their slug, status badge, last-updated date, and a quick-publish toggle. Click any row to open the editor. Use the New entry button to create a new entry (auto-generates a slug from the title field if one exists).
The editor provides:
- All defined fields rendered as the appropriate input type
- Richtext block editor — Markdown-based with live split-pane preview. The preview renders Markdown to HTML using
markedand updates as you type. - Autosave — changes are saved automatically after a short debounce. A save indicator shows the last-saved timestamp.
- Version history — every save creates a version snapshot. Access past versions from the "Versions" panel in the editor sidebar.
- Status toggle — switch between
draftandpublisheddirectly in the editor toolbar. - Media picker — for
mediafields: open the media library, search, and insert by UUID. - Relation picker — for
relationfields: search entries from the linked collection and select one or more. - Conditional fields — fields with
showWhenare hidden until the condition is met.
Upload, browse, and manage files:
- Supported formats — images (PNG, JPG, GIF, WebP, SVG), PDF, video (MP4, WebM), and any other file type
- Folder categories — assign a folder name on upload; filter the grid by folder
- Type filter — filter by All / Images / Video / PDF / Other
- Inline preview — images and SVGs shown as thumbnails; videos show an inline
<video>player - Copy URL — copies the full absolute URL of the file to the clipboard
- Alt text — set on upload; stored with the asset in the pod
- Storage — files stored as BLOBs directly in the
.podfile (no separate/publicassets needed)
Files are served at /orbiter/media/[id] with a Content-Type header from the stored MIME type.
Add, edit, and delete fields on any collection. Changes take effect immediately — no migration needed. Fields are stored as JSON in the _collections table.
- Add field — choose type, set label and options
- Reorder fields — drag-to-reorder (visual order only; does not affect data)
- Delete field — removes the field from the schema (does not delete existing data in entries)
- Relation setup — link a relation field to any other collection in the pod
- Site info — name, URL, description
- Language — default locale (
locale) and all supported locales (locales, comma-separated). These are exported fromorbiter:collectionsfor use in your Astro pages. - Admin language — switch the admin UI between English and German. Stored in a cookie (
orb_locale) and in the pod. - Interface theme — Orbiter Zen (default, warm serif) or Space Enso (terminal, cool blue/cyan). See Themes.
- Build webhook — paste a Netlify / Vercel / Cloudflare Pages webhook URL to enable the Build button.
⌘ K / Ctrl K opens a floating search palette. Type to fuzzy-search across all content entries and all navigation items. Works on every admin page.
On first launch (no collections exist), Orbiter redirects to /orbiter/setup. The wizard lets you:
- Choose an admin language before doing anything else
- Pick collection templates to pre-populate your schema (Posts, Pages, Events, Team, FAQ)
- Skip to the schema editor if you prefer to define everything manually
Orbiter ships two visual themes, selectable in Settings → Interface Theme:
Warm, minimal, calm. Inspired by Japanese editorial design.
- Light: warm off-white backgrounds, amber/gold accents, deep brown text
- Dark: deep charcoal backgrounds, muted amber accents
- Typography: Noto Serif JP for display headings, DM Mono for UI text
Futuristic, precise, terminal-feel.
- Light: cool blue-tinted backgrounds, electric blue accents, deep navy text
- Dark: near-black with deep blue tones, neon cyan accents, subtle glow effects
- Typography: Space Mono throughout — a full monospace typeset
Both themes support light and dark mode, toggled with the ● dark button in the topbar. Preferences are saved to localStorage. The theme class is applied to <html> before first paint to prevent flash.
Space Mono is lazy-loaded only when Space Enso is active — Orbiter Zen users load no extra fonts.
A .pod is a standard SQLite 3 database with a renamed extension.
# Inspect with the sqlite3 CLI
sqlite3 content.pod ".tables"
sqlite3 content.pod "SELECT slug, status, updated_at FROM _entries ORDER BY updated_at DESC LIMIT 10"
sqlite3 content.pod "SELECT key, value FROM _meta"Open with any SQLite GUI: TablePlus, DB Browser for SQLite, DBeaver, etc.
| Table | Contents |
|---|---|
_meta |
Key-value store for site config, build status, format version |
_collections |
One row per collection — id, label, schema (JSON) |
_entries |
All content from all collections — id, collection_id, slug, data (JSON), status, timestamps |
_versions |
Full JSON snapshots of each saved state per entry |
_media |
Uploaded files — id, filename, mime_type, size, data (BLOB), alt, folder, created_at |
_users |
Admin users — id, username, password (scrypt hash), role, timestamps |
_sessions |
Active sessions — token, user_id, expires_at |
# Full backup
cp content.pod content.pod.bak
# Move to a new server
scp content.pod user@server:/var/www/mysite/
# Restore
cp content.pod.bak content.podThe .pod file is the complete source of truth. Nothing else needs to be moved.
Since everything is SQL, you can run ad-hoc queries without the admin:
# Count published entries per collection
sqlite3 content.pod "
SELECT collection_id, COUNT(*) as total
FROM _entries
WHERE status = 'published'
GROUP BY collection_id
"
# Export all post titles and slugs as CSV
sqlite3 -csv content.pod "
SELECT slug, json_extract(data, '$.title'), status
FROM _entries
WHERE collection_id = 'posts'
"
# List all uploaded media
sqlite3 content.pod "
SELECT filename, mime_type, ROUND(size / 1024.0, 1) || ' KB' as filesize, folder, created_at
FROM _media
ORDER BY created_at DESC
"Orbiter uses cookie-based sessions. Passwords are hashed with scrypt — Node.js built-in, no bcrypt dependency.
All /orbiter/* routes check for a valid orb_sess cookie. Unauthenticated requests redirect to /orbiter/login.
import { hashPassword, verifyPassword, generateToken } from '@a83/orbiter-core';
// Hash a password (returns a string with algorithm+params+hash+salt)
const hash = await hashPassword('my-password');
// Verify a password against a stored hash
const valid = await verifyPassword('my-password', hash); // → true / false
// Generate a random session token
const token = generateToken(); // → hex string- User submits login form (
POST /orbiter/login) - Orbiter verifies the password hash, generates a token, stores it in
_sessionswith an expiry - Token is set as an HTTP-only cookie (
orb_sess) - On each request, the cookie is checked against
_sessions. Expired sessions are pruned. - Logout deletes the session row and clears the cookie
Currently two roles are supported: admin and editor. Role is stored in _users. Role-based permissions are not yet enforced in the UI — all authenticated users have full access. This will be expanded in a future phase.
Orbiter uses better-sqlite3 — a native Node.js addon. It opens a file on disk, reads and writes synchronously, and has no network protocol. This means:
- Works: any environment that runs real Node.js with filesystem access
- Does not work: edge runtimes (Cloudflare Workers, Netlify Edge, Vercel Edge) — these are V8 isolates without native module support
| Adapter | Package | Mode | Notes |
|---|---|---|---|
| Node.js | @astrojs/node |
standalone or middleware |
Recommended for self-hosting |
| Netlify | @astrojs/netlify |
serverless functions | Use serverless, not edge |
| Vercel | @astrojs/vercel |
serverless | Admin writes won't persist on ephemeral FS — see note |
| Cloudflare Pages | — | — | ❌ Not supported — no native Node.js |
Install the adapter:
npm install @astrojs/node@^9Orbiter requires Astro 5. Use @astrojs/node@^9 — version 10+ targets Astro 6.
Configure:
import { defineConfig } from 'astro/config';
import orbiter from '@a83/orbiter-integration';
import node from '@astrojs/node';
export default defineConfig({
output: 'server',
adapter: node({ mode: 'standalone' }),
integrations: [orbiter({ pod: './content.pod' })],
});Build and run:
npx astro build
node dist/server/entry.mjsHost on any platform that runs Node.js:
| Platform | Notes |
|---|---|
| VPS (Hetzner, DigitalOcean, Linode) | Full control, persistent disk — ideal |
| Railway | Git push deploy, persistent volume for the .pod |
| Render | Web service with persistent disk |
| Fly.io | Docker-based, persistent volumes |
| Docker | Mount the .pod as a volume |
Docker example:
FROM node:20-alpine
WORKDIR /app
COPY . .
RUN npm install && npx astro build
EXPOSE 3000
CMD ["node", "dist/server/entry.mjs"]# Mount .pod from host so it persists across container restarts
docker run -p 3000:3000 -v $(pwd)/content.pod:/app/content.pod my-orbiter-siteOrbiter works on Netlify Serverless Functions. The Netlify Edge runtime is not supported.
npm install @astrojs/netlifyimport { defineConfig } from 'astro/config';
import orbiter from '@a83/orbiter-integration';
import netlify from '@astrojs/netlify';
export default defineConfig({
output: 'server',
adapter: netlify(),
integrations: [orbiter({ pod: './content.pod' })],
});Important: Netlify's filesystem is read-only in deployed functions. The
.podfile is deployed with the site at build time. This means the admin UI is read-only on Netlify — content you edit won't be saved back to disk.Recommended pattern for Netlify: run the Orbiter admin on a persistent server (VPS, Railway) and use Netlify only for the static frontend. The admin triggers a Netlify build hook when content is ready.
npm install @astrojs/vercelimport { defineConfig } from 'astro/config';
import orbiter from '@a83/orbiter-integration';
import vercel from '@astrojs/vercel';
export default defineConfig({
output: 'server',
adapter: vercel(),
integrations: [orbiter({ pod: './content.pod' })],
});Same limitation as Netlify: Vercel serverless functions have an ephemeral filesystem. Admin writes don't persist. Use Vercel for static frontend only.
┌─────────────────────────────┐ build hook ┌──────────────────────┐
│ Orbiter Admin │ ──────────────────▶ │ Netlify / Vercel │
│ (VPS or Railway) │ │ (static frontend) │
│ │ │ │
│ /orbiter ← edit content │ │ / ← visitors │
│ content.pod ← persists │ │ /blog/[slug] │
└─────────────────────────────┘ └──────────────────────┘
- Run Orbiter on a Node.js server with a persistent
.podfile - Edit content in the admin, click Trigger build
- Netlify/Vercel fetches the repo + pod, runs
astro build, deploys static HTML - Visitors hit the static CDN — fast, no server needed for public pages
Orbiter is designed for statically built Astro sites. The typical workflow is:
- Edit content in the admin (
/orbiter) - Mark entries as
published - Click Trigger build in the admin
- Your hosting platform (Netlify, Vercel, Cloudflare Pages) rebuilds the site from the pod
At build time, Astro reads all published entries from the pod via orbiter:collections and renders static HTML.
| Action | Build needed? |
|---|---|
| Publish a new entry | Yes — not visible until rebuild |
| Edit a published entry | Yes — changes won't go live until rebuild |
| Unpublish an entry | Yes — still live until rebuild |
| Upload media | Depends — if referenced by URL in content, yes |
| Change site name or locale | Yes, if used in the build |
| Change admin password | No |
1. Click Trigger build in the admin dashboard.
2. The browser sends POST /orbiter/build to the Astro server.
3. The server reads build.webhook_url from _meta in the pod. If not set, an error is returned.
4. The server sends an empty POST to the webhook URL:
POST https://api.netlify.com/build/hook/your-hook-id
Orbiter acts as a proxy — the webhook URL never appears in browser network tools.
5. On success, build.last_triggered and build.last_status are written to _meta. The UI shows the timestamp.
Orbiter does not poll for build completion. Check your platform's dashboard for build logs.
Netlify
- Netlify → your site → Site configuration → Build & deploy → Build hooks → Add build hook
- Copy the hook URL
- Orbiter admin → Settings → Build & Deploy → paste the URL
https://api.netlify.com/build/hook/abc123xyz
Vercel
- Vercel → your project → Settings → Git → Deploy Hooks → create hook
- Copy the URL
- Paste in Orbiter Settings
https://api.vercel.com/v1/integrations/deploy/prj_xxx/yyy
Cloudflare Pages
- Cloudflare → Pages → your project → Settings → Builds & deployments → Deploy hooks
- Copy the URL
- Paste in Orbiter Settings
GitHub Actions
GitHub's repository_dispatch API requires an Authorization header that Orbiter does not add. Use a small serverless proxy (Netlify Function, Vercel Edge Function) to add the header before forwarding to GitHub.
orbiter/
├── apps/
│ └── demo/ ← demo Astro site
│ ├── astro.config.mjs
│ ├── demo.pod ← generated by npm run seed
│ ├── scripts/
│ │ └── seed.js ← creates and populates demo.pod
│ └── src/
│ └── pages/ ← demo frontend pages
│
├── packages/
│ ├── core/ ← @a83/orbiter-core
│ │ └── src/
│ │ ├── index.js ← public API entry point
│ │ ├── db.js ← OrbiterDB class (SQLite wrapper)
│ │ ├── pod.js ← createPod / openPod
│ │ └── auth.js ← hashPassword / verifyPassword / generateToken
│ │
│ └── integration/ ← @a83/orbiter-integration
│ ├── src/
│ │ ├── index.js ← Astro integration, virtual modules, injectRoute
│ │ ├── admin-utils.js ← client-side JS (theme, dark mode, command palette)
│ │ └── i18n.js ← EN/DE translations
│ └── routes/
│ ├── AdminLayout.astro
│ ├── dashboard.astro
│ ├── collection.astro
│ ├── editor.astro
│ ├── media.astro
│ ├── media-serve.astro
│ ├── schema.astro
│ ├── settings.astro
│ ├── build.astro
│ ├── login.astro
│ ├── setup.astro
│ ├── search.astro
│ └── SidebarCollections.astro
│
└── package.json ← npm workspace root
| Script | What it does |
|---|---|
npm run dev |
Start Astro dev server for the demo app (port 8080) |
npm run seed |
Delete and recreate demo.pod with fresh demo data |
npm run build |
Build the demo app for production |
npm run build:all |
Build all packages |
npm run publish:core |
Publish @a83/orbiter-core to npm |
npm run publish:integration |
Publish @a83/orbiter-integration to npm |
npm run publish:all |
Publish both packages sequentially |
The admin UI ships with English and German. To add a new locale:
1. Add translations to packages/integration/src/i18n.js:
export const translations = {
en: { nav_dashboard: 'Dashboard', nav_content: 'Content', /* ... */ },
de: { nav_dashboard: 'Dashboard', nav_content: 'Inhalt', /* ... */ },
fr: { nav_dashboard: 'Tableau de bord', nav_content: 'Contenu', /* ... */ },
};2. Add the language pill to the settings page (routes/settings.astro) and the setup wizard (routes/setup.astro).
3. The useTranslations(locale) function will automatically pick it up.
npm login
npm whoami # confirm you're authenticatednpm version patch --workspace=packages/core
npm version patch --workspace=packages/integrationBoth packages follow Semantic Versioning. If you bump @a83/orbiter-core's major version, update the peer dependency in packages/integration/package.json.
Always check what will be published before pushing:
npm pack --workspace=packages/core --dry-run
npm pack --workspace=packages/integration --dry-runVerify: no secrets, no demo files, no .pod files.
# Core first (integration depends on it)
npm publish --workspace=packages/core --access=public
# Then integration
npm publish --workspace=packages/integration --access=public
# Or both at once:
npm run publish:all--access=public is required on first publish for scoped packages (@a83/...).
# Verify packages are live
npm info @a83/orbiter-core
npm info @a83/orbiter-integration
# Tag the release
git tag v0.1.0
git push origin v0.1.0[ ] Both package versions bumped
[ ] @a83/orbiter-core version in integration's package.json updated (major bumps only)
[ ] npm pack --dry-run is clean (no secrets, no demo files, no .pod files)
[ ] npm whoami confirms correct account
[ ] git status is clean
| Phase | Name | Status | Focus |
|---|---|---|---|
| 01 | Ignition | ✅ Done | Core DB, virtual modules, basic admin routes |
| 02 | Bridge | ✅ Done | Full admin UI, media library, build trigger, auth |
| 03 | Warp | ✅ Done | Block editor, version history, themes, i18n, light mode, relation fields |
| 04 | Orbit | 🔄 Active | npm publish, Astro 5, media categories, public launch |
Phase 3 delivered:
- Richtext block editor with live split-pane Markdown preview
- Version history — full JSON snapshot per save, per entry
- Admin light mode — complete light/dark support across all routes
- Two themes: Orbiter Zen + Space Enso (terminal typeset, blue/cyan)
- EN/DE i18n — cookie-based language switching, setup wizard language picker
- Relation field type — link entries across collections, resolved at build time
- Schema editor redesign — inline field management
- Command palette (
⌘ K) — search content and navigation from any page - Setup wizard with preset collection templates
- Media library — BLOB storage, folder categories, type filter, inline video preview
| Package | Version | Description |
|---|---|---|
@a83/orbiter-core |
0.1.0 | SQLite engine — OrbiterDB, createPod, openPod, hashPassword |
@a83/orbiter-integration |
0.1.0 | Astro integration — virtual modules, injected admin routes, admin UI |
ABTEILUNG83 — Less Noise. Nice Data. No Bloat.