Simplicity
Chinaski is built to be small to today and still understandable tomorrow: a couple of purpose-built Perl programs, an SQLite database file, and a web server that hands out files. No service mesh, no plugin runtime, no framework churn. This page walks through the parts, and the deliberate omissions, that make the system simple.
Two programs, not a fleet
The whole product is:
- The CMS: one Perl application that manages content and shows you a browser-based admin interface. Everything it knows lives in one SQLite database file.
- The builder: a separate Perl process that reads the same database, renders Template Toolkit output, and writes static HTML to disk. It runs when a build is asked for (dashboard button, webhook, or by hand), never as a background side effect, and every run leaves a log.
- The web server: reverse proxy (Caddy in the project’s examples) serves static files straight from disk; the CMS process is never in the public request path.
So “one monolith” in spirit, with one exception the README makes explicit: builds happen in their own process, on purpose, because they are long operations with their own logs. The builder performs its swap atomically (rename into place), so a proxy never serves half-written output.
One database, one schema file
All state (content, users, settings, logs) is tables in a single chinaski.db. There is no ORM, no database server, no credentials to manage. The schema lives in one schema.sql file, and a keeper script compares the schema file against the applied migrations on demand, exiting visibly nonzero when the two drift apart so drift can’t hide.
Readable over clever
The code is plain by design:
- Implementation lives in a small number of files, layered with comments that include issue-tracker references, so a change’s history is as legible as its current shape.
- A short module library (
DB.pm,I18n.pm,Media.pm,Migrations.pm,Util.pm,Webhook.pmand a handful of plugin modules) carries just the shared machinery; there’s no framework-within-a-framework. - Where copies of logic once drifted apart, the project consolidated into one canonical helper (the database-open helper exists precisely so scripts stop hand-rolling their own copies) instead of adding an abstraction layer to paper over the difference.
- Admin command-line helpers are small scripts all following the same environment conventions: same variable names, same defaults.
Visibility into complexity is also part of readability: the builder itself records per-phase timings, and the build guard measures page counts and broken links before promoting anything, not because the code is clever, but because the system tells you what it did.
Few configuration points, one deployment shape
- The standard systemd units use an instance template: one definition serves any number of instances (
chinaski@1,chinaski@prod, …). Everything else about a deployment is two env vars plus a reverse proxy line or two. - Where transitional complexity does exist, it’s named, bounded, and scheduled for deletion; the admin skin migration is the worked example: one target class system, a backward-compat layer explicitly not to be extended, and per-file tracking until the legacy names are gone.
- Flat directory layout: the app file, the schema, the dependency file, the builder, admin templates, output trees per language, public assets. What each part does is visible from its filename.
What this simplicity buys, and costs
- Operations: two systemd services and a timer, transparent output trees you can
rsync, backups that are a file copy, no package manager of its own. - Changes stay legible: the codebase is comment-heavy with issue references, structured so a fix doesn’t require archaeology.
- The cost is stated honestly by the project: no plugin architecture, no plugin registry, no theme marketplace; anything you would get from those needs to be either a code change or not done.
The rule of thumb
Chinaski’s default answer to “should we add this?” is “will it still make sense to someone reading it cold at 2am?”: one monolith per process, few moving parts, and readable code beats clever abstractions.