Architecture Overview
How the container, bootstrap, modules, and request flow fit together in VelvetCMS Core 2.x.
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:
bootstrap/app.phpcreates theApplicationcontainer.- The bootstrap file binds the page index, file content driver, and
PageService. CoreServiceProviderregisters 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. SeeCoreServiceProviderfor the authoritative list.- The app boots service providers and then boots loaded modules.
- 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:
configandVelvetCMS\Core\ConfigRepositoryeventsandVelvetCMS\Core\EventDispatcherrouterandVelvetCMS\Http\Routing\RoutercacheandVelvetCMS\Contracts\CacheDrivercontent.driverandVelvetCMS\Contracts\ContentDriverpagesandVelvetCMS\Services\PageServiceviewandVelvetCMS\Services\ViewEnginesessionandVelvetCMS\Services\SessionManagermodulesandVelvetCMS\Core\ModuleManagerqueue·VelvetCMS\Queue\QueueManagerqueue.worker·VelvetCMS\Queue\Workerschedule·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 asmodule::view.routes/web.phpandroutes/api.phpare 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/pagesby default. - Existing
.mdand.vltfiles are indexed automatically. - New pages created through the CLI are written as
.vltfiles. - 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:
public/index.phpboots the app and captures the request.- The router applies global middleware from
config/http.php. - Route-specific middleware is appended.
- The matched handler runs as a closure or controller action.
- A
Responseinstance 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