"A little bird carrying hope, one letter at a time."
Kobato is a self-hosted blog CMS built by Yufan Sheng
— the engine behind 且听书吟. It runs on React Router 8 (SSR), Hono, and oRPC,
with a built-in /admin console for everything.
Content is stored as PortableText and authored through a Tiptap editor that round-trips losslessly to the wire format.
This repository is the complete product: public site, admin SPA, API, SSR renderer, install gate, and database migrations. The whole deployment is one process — content lives in an embedded SQLite database, analytics in an embedded DuckDB sidecar. There is no database server to run, back up, or upgrade.
Contributors: start at AGENTS.md — it documents the import boundaries, the four-layer
src/server/graph, the install contract, and the API permission matrix.
- Posts, pages, categories, tags, and comments — all managed in a built-in
/adminconsole - PortableText content model with a Tiptap editor
- Per-section settings (general, SEO, assets, comments, navigation, and more)
- First-party analytics with optional GeoIP enrichment
- Optional S3-compatible object storage for media
- Node.js 26+ for development (the SEA binary deployment needs no runtime);
pnpm run sea:buildrequires Node.js 26
No database server: SQLite and DuckDB are embedded.
Install dependencies and start the dev server:
pnpm install
pnpm run devOn first boot a kobato.config.json is created in the repo root
(gitignored) — fill in the two secrets (see Configuration
for the file shape). The database files create themselves under
storage.data (./data by default):
Open /admin/setup and enter the setup token printed in the console to
create the admin account. Settings are seeded automatically.
Most settings are managed in the admin dashboard. Infrastructure
configuration (database, secrets, data paths) lives in
kobato.config.json — always present, auto-created with defaults when
missing:
--config <path>/-c <path><binary dir>/kobato.config.json(SEA binary only)./kobato.config.json~/.config/kobato.config.json
The first existing file wins. Environment variables override file values and are written back into the file — env is the injection mechanism, the file converges to the effective configuration. Variable names are the nested config path joined with a double underscore:
| Variable | Config path | Description |
|---|---|---|
storage__data |
storage.data |
Root data directory for the databases, fonts, uploads, and MaxMind DB |
storage__database |
storage.database |
SQLite content database file, default <data>/kobato.db |
storage__analyticsDatabase |
storage.analyticsDatabase |
DuckDB analytics sidecar file, default <data>/analytics.duckdb |
security__sessionSecret |
security.sessionSecret |
HMAC secret for cookies. Generate with openssl rand -hex 32 |
security__encryptionKey |
security.encryptionKey |
AES-256-GCM key for encrypting secrets in the database |
server__host |
server.host |
Listen address, default 0.0.0.0 |
server__port |
server.port |
Listen port, default 4321 |
storage__defaultFont |
storage.defaultFont |
Optional fallback font file copied into <data>/fonts |
server__loggingLevel |
server.loggingLevel |
debug / info / warn / error / silent |
See kobato.config.example.json for the file shape.
The suite is fully self-contained — integration tests run against a shared in-memory SQLite database (temp files only for file-backed flows) and temp-file DuckDB sidecars, so no Docker or services are needed:
pnpm run testFor fast local feedback, run unit tests and snapshot tests only:
pnpm run test:unit
pnpm run test:snapsThe root docker-compose.yml runs the app as a single container — the
SQLite content database and the DuckDB analytics sidecar are embedded
files on the kobato_data volume, so there is no database service.
Launch with randomly generated secrets:
SESSION_SECRET=$(openssl rand -hex 32) \
ENCRYPTION_KEY=$(openssl rand -hex 32) \
docker compose up -dOptional overrides (compose forwards them to the app env; they are written
into /etc/kobato/config.json on first boot and persist in the
kobato_config volume):
HOST— default0.0.0.0PORT— default4321LOG_LEVEL— defaultinfo
Migrations run automatically at boot. Backups copy the content database
file (/data/kobato.db) — the admin console's backup job packs both
database files (content + the analytics sidecar) into a single
.tar.gz archive. Legacy content-only backups stay restorable.
Use the included Dockerfile to build locally:
docker build -t kobato .
docker run -p 4321:4321 \
-e security__sessionSecret=... \
-e security__encryptionKey=... \
-v kobato_data:/data \
-v kobato_config:/etc/kobato \
kobatoEvery release also ships a self-contained single executable — no Node.js
runtime, no node_modules, no database server. The server bundle, client
assets, and database migrations are embedded in the binary; the native
packages (sharp, canvas, DuckDB) are extracted to a cache directory on
first run. Targets: linux-x64, linux-arm64 (glibc), darwin-arm64,
win32-x64, and win32-arm64.
Download the archive for your platform (e.g. kobato-linux-x64.tar.gz)
and its .sha256 sidecar from the latest release, verify,
extract, and install:
sha256sum -c kobato-linux-x64.tar.gz.sha256
tar -xzf kobato-linux-x64.tar.gz
install -m 0755 kobato-linux-x64 /usr/local/bin/kobato
kobato --version # prints the baked-in versionConfigure it through kobato.config.json (see Configuration):
on first boot the binary creates it next to itself with defaults — edit the
file, or pass __-style environment variables once and let them be written
into the file. storage.data defaults to ./data relative to the working
directory, so set it explicitly for a system install. The natives cache
lands in $XDG_CACHE_HOME/kobato (override with KOBATO_CACHE_DIR).
Database migrations run automatically at boot; on first boot, open
/admin/setup.
A minimal systemd unit:
[Unit]
Description=Kobato blog CMS
After=network-online.target
[Service]
Type=simple
# Generate real secrets with e.g. `openssl rand -hex 32` — the schema
# requires both ≥ 32 chars and encryptionKey ≥ 10 distinct characters.
Environment=security__sessionSecret=change-me
Environment=security__encryptionKey=change-me
Environment=storage__data=/var/lib/kobato
ExecStart=/usr/local/bin/kobato
Restart=always
RestartSec=3
[Install]
WantedBy=multi-user.targetThe binary can update itself: in the admin console, open the version
dialog → 检查更新 → 立即更新. It downloads the release asset for the current
platform, verifies the sha256, swaps the executable in place (the previous
one is kept as kobato.bak for manual rollback), and restarts. Self-update
is intentionally unavailable inside Docker — upgrade containers by pulling
a new image instead.
pnpm run dev # development server
pnpm run build # production build
pnpm run test # run tests
pnpm run test:unit # run unit tests only
pnpm run test:snaps # run snapshot tests only
pnpm run fmt # formatting
pnpm run lint # lint
pnpm run type # TypeScript check
pnpm run db:gen # generate Drizzle migrationskobato.sketch is a Sketch template for the kobato favicon, logo, and branding assets.
{ "security": { "sessionSecret": "", // openssl rand -hex 32 "encryptionKey": "", // openssl rand -hex 32 }, }