Bolt CMS Docs
Sign in

Platform

Data Store

BOLT_DB is a dependency-free, connectionless flat-file data store for read-heavy JSON snapshots and secured records — without reaching for MySQL.

What BOLT_DB is

includes/BOLT_DB.php is a dependency-free, connectionless flat-file data store. It is not a MySQL replacement — relational data still belongs in the database. BOLT_DB exists for two specific jobs, each with its own store:

  • Public store — read-heavy JSON snapshots served to many polling clients (for example, a live metrics dashboard that a single writer updates). Files live inside the web root (data/public/) so Apache can serve them as static bytes — no PHP boot, no database round-trip per read.
  • Private store — secured records stored as PHP files that return an array. Files live outside the web root and are reachable only through BOLT_DB::read() / get_data(). Two optional, co-located hooks shape each read: rules.php (authorize and redact) and init.php (observe and audit).

Load it with require_once before use:

require_once ROOT_DIR . '/includes/BOLT_DB.php';

The public store

The public store keeps small JSON records at data/public/{collection}/{key}.json. Collection and key names are restricted to [A-Za-z0-9_-]. Because the files sit inside the web root, the fastest possible read is a browser fetching the static file directly — no PHP involved.

Method Returns Description
put($collection, $key, array $data) bool Atomic write (temp file + rename)
get($collection, $key) ?array Decoded record, or null if missing or unparseable
has($collection, $key) bool Whether the record exists
keys($collection) string[] Keys in the collection (sorted)
all($collection) array key => data for the whole collection
update($collection, $key, $fn) array Locked read-modify-write; $fn receives the current record (or []) and returns the new one
delete($collection, $key) bool Delete; false if it did not exist
serve($collection, $key, $opts) void Emit JSON with ETag/Last-Modified; answers a conditional GET with 304. $opts: cors, cache_control

One writer updates; many clients poll:

// Written by the app, polled by many dashboards:
BOLT_DB::put('metrics', 'pageviews', ['total' => 1840, 'updated' => time()]);

// Safe concurrent increment under a lock:
BOLT_DB::update('metrics', 'pageviews', function ($m) { $m['total']++; return $m; });

// Cheap polling endpoint (ETag + 304):
BOLT_DB::serve('metrics', 'pageviews', ['cors' => true]);

Tip: for maximum efficiency, point pollers straight at the static file (/data/public/metrics/pageviews.json) and let Apache handle the conditional GET with zero PHP. serve() exists to add CORS and a stable URL shape.

The entire public store is runtime cache, not source. data/.gitignore ignores everything but itself, so the store is never committed — it is regenerated from MySQL or from live submissions.

The private store

Private records are <?php return [...]; files stored outside the web root, so the web server can never hand them out directly. Every read runs a fixed pipeline:

  1. Validate the path (reject traversal or escaping).
  2. Fire the init.php cascade (root → leaf).
  3. Load the record.
  4. Apply rules.php to produce the returned view.
Method Returns Description
read($path, array $context = []) array Validate + init + rules, then return the (possibly redacted) record
get_data($path, array $context = []) array Convenience wrapper for read()
write($path, array $data) bool Atomic <?php return ...; write
forget($path) bool Delete; false if missing or escaping

rules.php — authorize and redact

A rules.php file in the record's leaf directory (optional) returns a function (array $data, array $ctx) that returns a transformed or redacted view, or throws BoltDBAccessDenied. If it is absent, data is returned as-is. If it is present but not callable, the read is denied. Prefer an allow-list (not unset()) so a secret field added later cannot leak by accident:

// rules.php for private user/* records
return function (array $data, array $ctx) {
    if (!empty($ctx['internal'])) return $data;        // trusted server-side caller
    return [                                            // public allow-list view
        'id'           => $data['id'],
        'display_name' => $data['display_name'],
    ];
};

init.php — observe and audit

An optional init.php returns a function (array $event) and fires on every read, cascading root → leaf — every init.php from the private root down to the record's directory runs. It is for side effects only: the return value is ignored and it must not echo. Use it for audit and observability.

Trust model

read() is a trusted internal helper. A private directory with no rules.php returns its data as-is, so the security perimeter is whoever calls read() — not the store itself.

BOLT_DB::read('user/1-jack-davis', $ctx);   // SAFE   - literal path in PHP
BOLT_DB::read($_GET['path'], $ctx);          // DANGER - client-controlled path

Never wire a client-controlled path into read() on a directory that lacks a rules.php; map specific routes to specific records instead. BOLT_DB stores no notion of "the current user" — rules.php and init.php only see the $context the caller passes (typically ['user' => [...]]), which is only as trustworthy as the caller supplying it. Until Bolt has an authenticated session, treat read() as callable by trusted server-side code only.

Configuration

The public store defaults to data/public. The private store is opt-in and must point outside the web root:

define('BOLT_DB_PRIVATE_DIR', '/absolute/path/outside/webroot');
// or
BOLT_DB::configure([
    'private_dir' => '/abs/path/outside/webroot',
    'public_dir'  => ROOT_DIR . '/data/public',            // optional override
    'auditor'     => function (array $event) { /* sink */ } // optional security-event sink
]);

BOLT_DB fails closed: if the private store is used while unconfigured, missing, or resolving to a path inside the web-served Bolt directory, it throws BoltDBConfigError rather than operate unsafely.

Exceptions

Exception Meaning
BoltDBNotFound Bad or escaping path, or missing record
BoltDBAccessDenied A rule rejected the read, or a present rule is broken
BoltDBConfigError Private store misconfigured (unset or web-reachable)
BoltDBException Base class; encode/write failure or malformed record

At an HTTP boundary, return the same response for BoltDBNotFound and BoltDBAccessDenied so the endpoint cannot be used to enumerate which records exist.

Blocks SQL data layer