Developing Nibble
Architecture
How a request becomes a page, where the code lives, and why it is arranged this way.
Rails 8 serves both the public site and the control panel, through Inertia and Vue 3 rendered on the server. SQLite holds the content, the cache and the job queue. Uploads go to S3 behind a CDN in production.
That is a deliberately short list of moving parts. A Nibble site needs one machine and one file, and a developer needs to understand one application rather than a front end and a back end that disagree.
Where things live
Everything under lib/nibble/ is Nibble’s. Everything else is the site’s.
| Path | Holds |
|---|---|
lib/nibble/ |
the engine, autoloaded as Nibble:: |
lib/nibble/app/ |
controllers, models, jobs, mailers, services and views — laid out as Rails lays out app/ |
lib/nibble/frontend/ |
nibble-admin/ (the control panel), nibble/ (the theme runtime), entrypoints/, ssr/ |
lib/nibble/core_schema/ |
the baseline schema, first of the three layers |
themes/crumbs/ |
the theme Nibble ships, and the starter a site’s own is copied from |
app/, schema/, site/, content/, config/nibble.yml, db/migrate |
the site’s, and never written by an upgrade |
The layout does not change any names: Admin::EntriesController is exactly that, in the place Rails expects to
find it, just rooted somewhere a site’s own app/ can sit beside rather than inside.
A site’s views are looked in first, which is what makes overriding one a matter of putting a file in the right place.
How a public request becomes a page
NibbleRedirectsMiddleware— redirects are checked before anything else, from a table, cached. A moved page costs one lookup, not a full render.- Fixed routes —
/up,/assets/:uuid/…,POST /forms/:handle,/robots.txt,/sitemap.xml. - The catch-all,
SiteController#show, asksNibble::Routing.resolvewhat this path is. It answers with an entry, a term, a redirect, or nothing. Nibble::PagePropsruns the view’s query sidecar throughNibble::QueryandNibble::Presenter, gathering exactly what the page asked for.- Inertia renders
theme/<view>from the active theme, on the server. Nibble::PageCachestores the response against the tagsNibble::Dependenciescollected along the way, echoed to a CDN asSurrogate-Key.
Step 6 is why queries live in a file rather than in the view: because Nibble ran them, it knows what the page depended on, and publishing any of those clears exactly the pages affected and no others.
The content model
Entries, terms, globals and navigation are rows. Their fields live in a data JSON column, validated against
a blueprint built from the schema. A new field is not a migration.
Nibble::Lifecycleowns everything that happens to a record: drafts, revisions, scheduling, workflow, trash and publishing. Nothing writes content around it.Nibble::Uriskeeps URIs current, and records a 301 when a live record’s URI changes.Nibble::Eventspublishes through an outbox. The subscribers registered inNibble.boot!update relations, URIs, the audit log, notifications, the page cache, the search index and webhooks.
The outbox matters: a publish does not do eight things inline and fail halfway. It records what happened, and the subscribers follow.
The modules
Schema, Field/Fields/Fieldtype, Records::*, Lifecycle, Query, Presenter, Routing, PageProps,
PageCache, Search, Seo, Sitemaps, Assets, Forms, Outbound, Webhooks, Access, Packages,
ContentMigrations, Release, Releases, Eject, Install, Upgrade, Check.
Each is one idea. If you are looking for where something happens, the name is usually the answer.
The control panel
The control panel is the same application: Rails controllers under Admin::, rendering Vue pages through
Inertia. It is not a separate front end talking to an API, which is why a new fieldtype is one Ruby class and one
Vue component rather than a contract negotiated across a network.
A site can replace any control panel screen by putting its own file at the same path under site/pages/. See
Extending Nibble.
Things deliberately not here
No Redis, no Elasticsearch, no separate front-end application, no GraphQL layer. Search is SQLite’s FTS5, the cache and queue are SQLite tables, and the front end is the same application. Each of those is a thing you do not have to run, monitor or pay for — and each is a choice that would be defensible the other way, on a much larger site.