- Home
- Learning paths
- Expert path
- Éclair's architecture
Éclair's architecture
Contents
Contents
The Éclair CMS architecture comes down to one rule: only make PHP work when it's needed. This lesson follows a request from end to end, from the web server to the static cache, shows how data is stored without a database and which class does what. You'll then be able to debug, customize or host Éclair knowing how it works inside.
What you'll be able to do
Follow a request
From the web server to index.php, then to the cache or the application.
Read the data/ folder
What each file contains and how it's protected.
Find the right class
Know where to act in app/lib/ in a few seconds.
How does a request travel through the Éclair CMS architecture?

It all starts from one observation: most visits ask for a page that hasn't changed. So the Éclair CMS architecture sends it back as is, without reading a single piece of content. Here is the path a request takes.
- The web server. On Apache or LiteSpeed,
.htaccesssends any URL that isn't an existing file toindex.php. It returns a 404 forapp/,data/,cache/, hidden files (except.well-known) and any.phpfile other thanindex.php, and passes theAuthorizationheader on to the MCP connector. - The index.php fast path. For a GET or HEAD request,
index.phpreadscache/runtime.php, then looks for the page incache/pages/under themd5of the path. If it finds it, it returns the page's saved headers, its ETag andX-Cache: HIT, or a304 Not Modifiedif the browser already has this version. - Bootstrap. Otherwise,
app/bootstrap.phpchecks the PHP version (8.1 at least), loadseclair-config.phpif it exists, definesECLAIR_DATAandECLAIR_DEBUG, registers autoloading for theEclair\…classes, then callsEclair\App::run(). - Routing.
App::run()sets up error handling and hands the request to the right module (table below). - Rendering.
Site::handle()only accepts GET and HEAD, enforces a single URL per piece of content (trailing slash, uppercase,index.php: 301 redirect), serves the generated files (robots.txt,sitemap.xml,llms.txt, feeds,.mdversions), then looks up the content. A page rendered with a 200 status is saved byCache::put().
| Requested path | Module that responds |
|---|---|
/__eclair/ping | rewrite test, proof of identity (Loopback) |
| any path, before installation | Installer::handle() |
| the admin URL and its subpages | Admin::handle() |
/_eclair/contact | Contact::handle(), the contact form |
/_eclair/edit/<id> | redirect to the editor, if you're logged in |
/_eclair/… and /.well-known/… | Mcp::handle(), silent while the connector is disabled |
| everything else | Site::handle() |
Search, the 404 page, the appearance preview, Visual mode and the admin are never cached. To see the fast path at work, request the same page twice:
# 1st call: the application generates the page, then saves it
curl -sI https://exemple.fr/contact | grep -i -E 'etag|x-cache'
# ETag: "0745734655e7d523"
# 2nd call: served by index.php from cache/pages/, without loading the application
curl -sI https://exemple.fr/contact | grep -i -E 'etag|x-cache'
# ETag: "0745734655e7d523"
# X-Cache: HIT
# The cache file is named after the md5 of the requested path
php -r 'echo md5("/contact"), PHP_EOL;' # 90609cfd31a32bdbd3d81843c57d356b
Flat-file storage: the Store class
The Éclair CMS architecture does without a database: each piece of data is a data/<name>.php file that starts with the line <?php exit; ?>, followed by readable JSON. Requested from the web, it returns nothing, even on a server that ignores .htaccess files. Here is the beginning of a real menus file:
<?php exit; ?>
{
"main": [
{
"type": "content",
"id": "af59e5ba0a",
"label": "",
"url": ""
}
]
}
Four mechanisms keep this storage safe, even on a busy shared server:
- Atomic writes:
Store::atomic()writes a temporary file, then renames it, so a reader never sees a half-written file. Files get 0600 permissions, and the folders indata/andcache/get 0700. - No injectable code:
Store::write()encodes the JSON withJSON_HEX_TAGandJSON_HEX_AMP. A<becomes<, so the<?phpsequence can't appear in the data. - Lock:
Store::locked()takes an exclusive lock (flock) ondata/.lockduring every read-modify-write operation. - Controlled names:
Store::file()only accepts lowercase letters, numbers, hyphens and slashes. There's no way out ofdata/.
Every class that writes data follows this pattern, Redirects::add() for example:
use Eclair\Store;
$menus = Store::read('menus', []); // read, with a default value
Store::locked(function (): void { // read, modify, write with no race
$list = Store::read('redirects', []);
// … modify $list …
Store::write('redirects', $list); // temporary file, 0600, rename
});
The files in the data/ folder
The last column shows what the ZIP backup on the Security page keeps: secrets and short-lived data are left out.
| File or folder | Contents | ZIP backup |
|---|---|---|
site.php | settings, merged with Settings::DEFAULTS | yes |
users.php | accounts, Argon2id hashes, two-factor authentication | yes |
keys.php | site secret and IndexNow key, created at installation | yes |
index.php | a summary of each piece of content, to list and route quickly | yes |
content/<id>.php | a page, a post or a product, with its blocks | yes |
revisions/<id>/ | the last 20 versions of that content | yes |
search.php, media.php | search text, media library register | yes |
menus.php, categories.php, redirects.php, custom-css.php | menus, categories, redirects, custom CSS | yes |
messages/, inbox.php | form messages, 500 at most | yes |
logs/ | error and security logs, capped at 1 MB | yes |
vault.php | encrypted API keys | no |
ai/ | assistant conversations, suggestions, MCP connections | no |
sessions/, limits/ | admin sessions, brute-force counters | no |
Updates never touch data/, and the ECLAIR_DATA constant can even move it outside the public folder.
The main classes in app/lib/
All classes live in the Eclair\ namespace, one file per class, with no framework and no Composer:
| Class | Role |
|---|---|
App | routing for requests the cache doesn't serve |
Req, Http | reading the request, responses and redirects |
Store, Settings | flat-file storage, settings and default values |
Content, Blocks | content, index, revisions; rendering of the 18 blocks |
Site, Theme | public router; templates and inline CSS |
Cache | static pages and cache/runtime.php |
Seo, Analyzer | structured data, sitemap, llms.txt; scores |
Security, Session, Auth | headers, CSRF tokens, sessions, login |
Media | media library, re-encoding and WebP |
Admin and Admin… | admin screens and actions |
Ai, AiChat, AiTools, Mcp | AI assistant, tools, MCP connector and OAuth |
Integrity, Log, Keys, Vault, Loopback | integrity, logs, secrets, vault, address verification |
I18n | admin translations |
Why does the cache clear on every change?
A changed title also appears in a menu, a post card and the breadcrumbs. Rather than guess these dependencies, the Éclair CMS architecture clears everything: Cache::purge() runs after every save of content, settings, menus, categories, media, a profile or the custom CSS. It deletes the files in cache/pages/ and rewrites cache/runtime.php; each page is regenerated on its first visit.
<?php exit; ?>
{"on":true,"expires":0,"host":"exemple.fr","https":true}
index.php reads this small file on every request:
on: cache active (Static page cache checked, debug mode off);expires: the time of the next scheduled post, after whichindex.phpignores the cache andCache::checkExpiry()clears it;hostandhttps: the official host and protocol, once the address is verified, so that redirects are never bypassed.
Security headers and CSP
Security::headers() produces three sets of headers: site, admin and text (sitemap, llms.txt, feeds). They depend on the settings, never on the request, so nobody can poison the cache with a forged header. Here are the headers of a public page served over HTTP, recorded on Éclair 1.8:
Content-Security-Policy: default-src 'self'; script-src 'self'; style-src 'self' 'sha256-…'; img-src 'self' data: https:; media-src 'self' https:; frame-src https://www.youtube-nocookie.com https://player.vimeo.com; connect-src 'self'; form-action 'self'; base-uri 'self'; object-src 'none'; frame-ancestors 'self'
X-Frame-Options: SAMEORIGIN
X-Content-Type-Options: nosniff
Referrer-Policy: strict-origin-when-cross-origin
Permissions-Policy: camera=(), microphone=(), geolocation=(), payment=(), usb=(), browsing-topics=()
Cross-Origin-Opener-Policy: same-origin
Theme::hash() computes the sha256 hash of the CSS inlined in the page: only that <style> tag is allowed, no inline script is permitted, and the only public script is assets/site/site.js. Over HTTPS, upgrade-insecure-requests is added, along with Strict-Transport-Security if the HSTS option is checked. The admin is stricter still: no inline styles, frame-ancestors 'none', noindex and Cache-Control: no-store, private.
Translations
- Admin: every string is written in French in the code, with
t().I18n::t()looks up its translation inapp/lang/<lang>.json.gz, compiled bytools/i18n.phpand indexed by the CRC32 hash of the French text. Without the zlib extension, the admin stays in French. - Public site:
Theme::t('key', $lang)reads theTheme::STRINGSarray (“Contents”, “Key takeaways”, “Read in”…) in six languages, andTheme::words()gives the words used in URLs (page, author, search, shop) in the site's language.
Key points
- In the Éclair CMS architecture, a page already generated is served by
index.phpfromcache/pages/without loading the application:X-Cache: HITproves it. - Everything else goes through
app/bootstrap.php, thenApp::run(), which routes to the installer, the admin, the contact form, MCP or the site. - Data is stored as JSON files behind
<?php exit; ?>, written atomically, with 0600 permissions and under a lock. - The cache clears completely on every change and rebuilds itself on the next visit.
- Security headers depend only on the settings: the cache can't be poisoned.
Going further
- Local development and testing: run Éclair on your machine and replay its 550 checks.
- Customize the theme templates: change the rendering without breaking the CSP.
- Nginx, VPS, proxy and CDN: reproduce the
.htaccessrules outside Apache. - Advanced security and maintenance: integrity, backups and updates.
- PHP documentation: flock() and rename(), at the heart of how
Storewrites. - OWASP: Content Security Policy Cheat Sheet.
Frequently asked questions
Can Éclair run without its static cache?
Yes: uncheck Static page cache in Settings › Advanced, or turn on debug mode. Every visit then generates the page, which costs more server time.
Why store JSON in .php files?
So that the <?php exit; ?> line protects each file even if the server ignores .htaccess. Requested from the web, the file displays nothing, while Éclair reads the JSON that follows this first line.
What happens if two people save at the same time?
Writes go through Store::locked(), which takes an exclusive lock on data/.lock. The two saves therefore run one after the other, and the previous version joins the revision history.
Can I put the data/ folder outside the public folder?
Yes. First move the folder, then set its new location with define('ECLAIR_DATA', '/path/to/eclair-data'); in eclair-config.php, next to index.php. The fast path doesn't depend on it: it only reads cache/.