4 Expert pathLesson 1 of 8 7 min read

Éclair's architecture

  1. 1. Éclair's architecture
  2. 2. Local development and testing
  3. 3. Customize the theme templates
  4. 4. Nginx, VPS, proxy and CDN: deploy Éclair
  5. 5. Éclair performance: staying at 100 out of 100
  6. 6. Migrate from WordPress or Wix
  7. 7. Advanced security and maintenance
  8. 8. Scaling Éclair in an agency
Contents
  1. How does a request travel through the Éclair CMS architecture?
  2. Flat-file storage: the Store class
  3. The files in the data/ folder
  4. The main classes in app/lib/
  5. Why does the cache clear on every change?
  6. Security headers and CSP
  7. Translations
  8. Key points
  9. Going further
  10. Frequently asked questions

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?

Diagram of a request's life cycle: a response from the static cache or a pass through the Éclair application
The fast path serves pages already generated; everything else goes through App::run.

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.

  1. The web server. On Apache or LiteSpeed, .htaccess sends any URL that isn't an existing file to index.php. It returns a 404 for app/, data/, cache/, hidden files (except .well-known) and any .php file other than index.php, and passes the Authorization header on to the MCP connector.
  2. The index.php fast path. For a GET or HEAD request, index.php reads cache/runtime.php, then looks for the page in cache/pages/ under the md5 of the path. If it finds it, it returns the page's saved headers, its ETag and X-Cache: HIT, or a 304 Not Modified if the browser already has this version.
  3. Bootstrap. Otherwise, app/bootstrap.php checks the PHP version (8.1 at least), loads eclair-config.php if it exists, defines ECLAIR_DATA and ECLAIR_DEBUG, registers autoloading for the Eclair\… classes, then calls Eclair\App::run().
  4. Routing. App::run() sets up error handling and hands the request to the right module (table below).
  5. 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, .md versions), then looks up the content. A page rendered with a 200 status is saved by Cache::put().
Requested pathModule that responds
/__eclair/pingrewrite test, proof of identity (Loopback)
any path, before installationInstaller::handle()
the admin URL and its subpagesAdmin::handle()
/_eclair/contactContact::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 elseSite::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:

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 folderContentsZIP backup
site.phpsettings, merged with Settings::DEFAULTSyes
users.phpaccounts, Argon2id hashes, two-factor authenticationyes
keys.phpsite secret and IndexNow key, created at installationyes
index.phpa summary of each piece of content, to list and route quicklyyes
content/<id>.phpa page, a post or a product, with its blocksyes
revisions/<id>/the last 20 versions of that contentyes
search.php, media.phpsearch text, media library registeryes
menus.php, categories.php, redirects.php, custom-css.phpmenus, categories, redirects, custom CSSyes
messages/, inbox.phpform messages, 500 at mostyes
logs/error and security logs, capped at 1 MByes
vault.phpencrypted API keysno
ai/assistant conversations, suggestions, MCP connectionsno
sessions/, limits/admin sessions, brute-force countersno

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:

ClassRole
Approuting for requests the cache doesn't serve
Req, Httpreading the request, responses and redirects
Store, Settingsflat-file storage, settings and default values
Content, Blockscontent, index, revisions; rendering of the 18 blocks
Site, Themepublic router; templates and inline CSS
Cachestatic pages and cache/runtime.php
Seo, Analyzerstructured data, sitemap, llms.txt; scores
Security, Session, Authheaders, CSRF tokens, sessions, login
Mediamedia library, re-encoding and WebP
Admin and Admin…admin screens and actions
Ai, AiChat, AiTools, McpAI assistant, tools, MCP connector and OAuth
Integrity, Log, Keys, Vault, Loopbackintegrity, logs, secrets, vault, address verification
I18nadmin 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:

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

Key points

Going further

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/.

All the lessons of this path