A warm, batteries-included CMS in Rust — boards, shop, wiki, blog & an AI authoring studio, all under one roof.
Ondol (온돌) is the traditional Korean underfloor heating that keeps a home warm — the idea behind this project: a single, cozy place where your whole community lives. Inspired by Korea's Gnuboard, rebuilt in Rust.
Most Rust "CMS" projects are single-purpose (a static-site generator, a forum, a blog). Ondol fills the empty niche: a full, dynamic, WordPress/Gnuboard-class CMS in a single binary — memory-safe, fast, and self-hostable.
| Board | Admin dashboard | AI authoring studio |
|---|---|---|
![]() |
![]() |
![]() |
- 📋 Boards — categories, nested comments, per-board permission levels, points, secret posts, notices, tags, attachments, full-text search
- 👥 Members & RBAC — level system, role groups, auto-promotion, dormant/withdrawal flows, multi-admin per board
- 🛒 Shop — products & options, cart, orders, inventory, shipping fees, tracking, reviews, points (Gnuboard Youngcart-class)
- 📚 Wiki & Blog — versioned wiki with access control; blog skin reusing the board engine
- 🤖 AI authoring studio — long-form book generation (outline → draft → review → publish) + admin writing assist, with a provider abstraction (local Ollama or cloud) and encrypted API keys
- 🛡️ Security — CSRF, in-process WAF (bot/scanner blocking), RBAC, OAuth2 (PKCE, refresh, webhooks, OpenAPI), SSRF guards, AES-256-GCM secret encryption, bcrypt isolated on a blocking pool
- 🔎 SEO — OpenGraph/Twitter, JSON-LD (Article/Product/Breadcrumb), sitemap & robots, per-page noindex
- ⚡ Modern SSR — Tera server-rendering + htmx islands (list search / pagination / comments update without full reloads)
- 🧩 Skeleton for derived sites — build new services on top via
SiteHooks+ template overlay, keeping the core untouched - 🧰 Ops-ready — versioned schema migrations (auto-applied on boot), structured logging (
tracing), i18n scaffold, polls · FAQ · 1:1 inquiry · messages · notifications · mail templates · visitor stats
| Area | Choice |
|---|---|
| Language / runtime | Rust + Tokio |
| Web framework | Axum 0.8 |
| Data access | SeaORM (simple) + sqlx (joins/aggregates) |
| Database | PostgreSQL 16 (+ pgvector, optional) |
| Templates | Tera (SSR) + htmx |
| Sessions | tower-sessions (PostgreSQL store) |
# 1. Config
cp .env.example .env # edit DB, port, upload size, SMTP, APP_SECRET …
# 2. Dev database (PostgreSQL 16)
docker compose up -d
# 3. Run
cargo run # http://127.0.0.1:3000The admin account is admin; the initial password is printed once to the console on first boot
(or set ADMIN_INITIAL_PASSWORD in .env). On a fresh install the browser opens a setup wizard
(/setup) — authenticate with the console password, then choose your site name and a new admin
password. The schema is created/migrated automatically on startup.
- Architecture · Decisions (ADR) · Roadmap
- Deploy: CI/CD (Docker + GHCR) · Native (systemd) · Operations & updates
- Derived sites · Theme system
Ten in-repo starters, each shipped as track A (server-rendered Tera theme) + track B (headless Next.js)
on one backend — 10 types × 2 tracks = 20 running sites.
Details: docs/DERIVED-CATALOG.md · per-site notes in sites/<id>/docs/SITE.md.
| id | Type · benchmark | Track A (server-rendered) | Track B (headless) |
|---|---|---|---|
community |
Gallery community · DCInside | http://127.0.0.1:3001/board/gd | http://127.0.0.1:3101 |
forum |
Threaded Q&A · Discourse | http://127.0.0.1:3002/board/ask | http://127.0.0.1:3102 |
blog |
Editorial blog · Ghost/Medium | http://127.0.0.1:3003/board/log | http://127.0.0.1:3103 |
magazine |
Multi-author · NYT/Verge | http://127.0.0.1:3004/board/feature | http://127.0.0.1:3104 |
wiki |
Docs · Notion/Nextra | http://127.0.0.1:3005/wiki | http://127.0.0.1:3105 |
shop |
Commerce · Shopify/Apple | http://127.0.0.1:3006/shop | http://127.0.0.1:3106 |
jobs |
Job board · JobKorea/Wanted | http://127.0.0.1:3007/board/jobs | http://127.0.0.1:3107 |
realestate |
Listings · Zigbang/Dabang | http://127.0.0.1:3008/board/listings | http://127.0.0.1:3108 |
reserve |
Booking · Interpark/Airbnb | http://127.0.0.1:3009/reserve | http://127.0.0.1:3110 |
studio |
Book reader · Substack/Ridi | http://127.0.0.1:3010/books | http://127.0.0.1:3111 |
Backends use ports 3001–3010, fronts 3101–3111 (reserve front is 3110). Track A URLs above point at the
content screen; track B home is /. Every site has a KO/EN/JA language switcher in the header.
A track B front talks to its own track A backend, so start the backend first.
# Track A (server-rendered) — backend on 3001–3010, DB ondol_<id> (auto-created)
scripts/catalog.sh reserve
# On the very FIRST boot it prints a one-time admin password to the console (D-027):
# "최초 관리자 계정 생성: admin / <random>" — log in with that, or preset it:
# ADMIN_INITIAL_PASSWORD=admin123 scripts/catalog.sh reserve
# Track B (headless) — Next.js dev server on 3101–3111 (needs the backend above running)
cd fronts && pnpm install # first time only
pnpm --filter reserve-front dev # or: cd fronts/reserve && pnpm devscripts/portfolio-e2e.sh orchestrates every backend + front together. It uses throwaway
ondol_<id>_e2e databases (your ondol_<id> demo DBs stay untouched) and fixes the admin login to
admin / admin123.
scripts/portfolio-e2e.sh up # build + recreate DBs + start 20 servers (~1–2 min)
scripts/portfolio-e2e.sh status # 200 = ready
scripts/portfolio-e2e.sh down # stop everything
scripts/portfolio-e2e.sh up start # serve production builds instead of dev (needs: cd fronts && <id> build)Prerequisites for either path: docker compose up -d (PostgreSQL + Redis) and, for track B,
pnpm install inside fronts/. A one-page index of all 20 links lives at docs/PORTFOLIO.html.
Payments (shop): bank transfer works out of the box; card PGs (TossPayments, PortOne) are enabled per-gateway in /admin/payments after entering sandbox keys — see docs/PAYMENTS.md.
Issues and PRs welcome. Run the test suite before submitting:
docker compose up -d
cargo test --workspaceMIT © Yangseok
Bundled third-party: htmx (0BSD) · TOAST UI Editor (MIT)


