Docs LATEST

Architecture Overview

How the container, bootstrap, modules, and request flow fit together in VelvetCMS Core 2.x.

Architecture

VelvetCMS Core 2.x stays deliberately small. The runtime is built from a few explicit pieces: the application container, a core service provider, a file-native page layer, optional queue + worker services, and a module system that favors conventions over framework magic.

Application Lifecycle #

Both HTTP and CLI entrypoints follow the same broad sequence:

  1. bootstrap/app.php creates the Application container.
  2. The bootstrap file binds the page index, file content driver, and PageService.
  3. CoreServiceProvider registers framework services like config, events, router, cache, views, sessions, database, queues, modules, scheduling, validation, tenancy helpers, HTTP client wiring, storage, migrations, cron integration, asset server routes, module artefact tooling, HTTP rate limiting, and more. See CoreServiceProvider for the authoritative list.
  4. The app boots service providers and then boots loaded modules.
  5. The entrypoint dispatches either an HTTP request or a CLI command.
Bootstrap -> Container -> Services -> Modules -> HTTP or CLI

That flow is intentionally easy to trace. Most framework behavior is defined in bootstrap code or explicit service registration, not hidden behind decorators or facades.

Service Container #

VelvetCMS\Core\Application is the container and lifecycle coordinator. It supports named services, singletons, aliases, and pragmatic autowiring for concrete classes.

Binding Services #

// Singleton service bound under a string key.
$app->singleton('search.index', fn () => new SearchIndex(storage_path('search')));

// Resolve by class alias or concrete class.
$app->alias('search.index', SearchIndex::class);

// Pre-built instance.
$app->instance('build.id', '2026.03.25');

Resolving Services #

$pages = $app->make(VelvetCMS\Services\PageService::class);
$router = $app->make('router');

$pages = app(VelvetCMS\Services\PageService::class);
$router = app('router');

Autowiring #

If a class is concrete and its constructor types are resolvable, the container can instantiate it without a manual binding.

class MyController
{
    public function __construct(
        private readonly VelvetCMS\Services\PageService $pages,
        private readonly Psr\Log\LoggerInterface $logger,
    ) {}
}

$controller = $app->make(MyController::class);

Autowiring stops at interfaces and ambiguous scalar arguments. That is deliberate. Core tries to reduce boilerplate without hiding dependency edges.

Core Services #

The default bootstrap and core provider wire the framework around a few central services:

  • config and VelvetCMS\Core\ConfigRepository
  • events and VelvetCMS\Core\EventDispatcher
  • router and VelvetCMS\Http\Routing\Router
  • cache and VelvetCMS\Contracts\CacheDriver
  • content.driver and VelvetCMS\Contracts\ContentDriver
  • pages and VelvetCMS\Services\PageService
  • view and VelvetCMS\Services\ViewEngine
  • session and VelvetCMS\Services\SessionManager
  • modules and VelvetCMS\Core\ModuleManager
  • queue · VelvetCMS\Queue\QueueManager
  • queue.worker · VelvetCMS\Queue\Worker
  • schedule · VelvetCMS\Scheduling\Schedule

Events #

Events are simple string-based notifications. Core uses them to surface lifecycle points without requiring inheritance or deep coupling.

$events = app('events');

$events->listen('page.saved', function (VelvetCMS\Models\Page $page) {
    app('cache.tags')->flush('pages:list');
});

$events->dispatch('page.saved', $page);

Core emits events such as app.booting, app.booted, router.matching, router.matched, page.loading, page.saved, queue.job.processing, and the exception reporting hooks. See Standard Events for the authoritative list.

Modules #

Modules extend the application through a module.json manifest and an entry class. Current Core keeps the conventions introduced in 2.1, with incremental refinements since:

  • config/ is automatically registered as a module config namespace.
  • resources/views/ is automatically exposed as module::view.
  • routes/web.php and routes/api.php are loaded automatically unless disabled.
  • Commands can be declared directly in module.json.

That lets small modules stay lean. A module can still do extra work in register() and boot(), but it no longer needs to manually wire the obvious pieces.

See the Modules page for manifest format and discovery details.

File-Native Content #

Core 2.x treats pages as files first. The bootstrap binds a single FileDriver implementation behind the ContentDriver contract.

  • Pages live under user/content/pages by default.
  • Existing .md and .vlt files are indexed automatically.
  • New pages created through the CLI are written as .vlt files.
  • Listing and filtering pages run against a lightweight page index instead of scanning and reparsing every file on every request.

That index can use JSON or SQLite storage. See Page Indexing for the tradeoffs.

HTTP Request Flow #

For web requests, the router is the center of execution:

  1. public/index.php boots the app and captures the request.
  2. The router applies global middleware from config/http.php.
  3. Route-specific middleware is appended.
  4. The matched handler runs as a closure or controller action.
  5. A Response instance is returned to the client.
Request -> Router -> Middleware pipeline -> Handler -> Response

CLI Flow #

The velvet entrypoint bootstraps the app, registers built-in commands, fires the commands.registering event, and then lets modules add their own commands.

That design matters because module manifests can declare CLI commands declaratively. Additional commands may still subscribe to commands.registering.

For background processing, queue + queue:work behave like other long-lived infrastructure. See Queue.

What's Next #

  • Lifecycle - the detailed boot and request sequence
  • Directory Structure - where Core, user space, and storage live
  • Modules - manifests, conventions, and tenant-aware module artifacts