Where everything lives, why it lives there, and how to change it without breaking your app.
There are two ways to start: from a blueprint template, or by installing the Kernel into a fresh folder yourself. Both end up with the same shape — the blueprints just come pre-filled with a sensible starting set of files and capabilities.
Either way, the Kernel itself lives in kernel/ at your project root. It's self-contained, so you can read its source, upgrade it, or fork it. Everything else in your root is your project — your app folder, your capabilities and modules, your config, and your storage.
Blueprint ≠ magic. A blueprint is just a pre-made project skeleton. Once it's on disk it's a normal app — you keep all the same folders and controls described on this page.
A handful of files sit at the very top of your project. They're the front doors.
Your project manifest — what the Kernel and which capabilities to install, and the version of the Kernel you're on. The installer reads and updates this file.
The CLI entry point. This is the php forge.php you run on the command line. It boots a CLI container and hands off to the command application.
The one-time installer. Downloads the Kernel as a ZIP from the registry, verifies its integrity, and unpacks it into kernel/. You rarely run this more than once, if at all.
Environment configuration — app name, environment, debug flag, app key, and a list of disabled modules. Loaded at boot. Never commit secrets here.
A safety guard. It returns 403 Forbidden so nobody can serve your project root directly. Real requests go through public/index.php instead.
Your customization file, if you've generated one. It's optional; without it the Kernel uses its built-in defaults. See Customizing Folder Structure.
One source of truth: forge.json describes what you have. forge-lock.json records the exact versions and integrity of everything that's installed. See forge-lock.json below.
Simple PHP files that return arrays. The Kernel reads any file in here by name — config('registry') maps to config/registry.php, and so on.
Which config files you have depends on which capabilities you've installed. Each capability contributes its own config file. A fresh app typically starts with things like:
config/
├── forge_router.php # routes / middleware groups (ForgeRouter)
├── middleware.php # registered middleware
└── registry.php # package / source registryConfig here is your app's opinion. The Kernel just loads whatever array the file returns and hands it to you through the config() helper.
These are the three folders where your code lives. They differ by what they hold and by namespace, not by how the Kernel treats them. The folders and their namespaces below are the defaults — every one of them can be changed (see Customizing Folder Structure). If you customize, keep this section in mind: your layout follows whatever you configure.
| Folder | Default Namespace | Holds |
|---|---|---|
app/ | App\ | Your application — controllers, services, models, views, migrations. |
modules/ | Modules\ | App-specific feature modules (like Blog, Shop) and most legacy primitives during migration. |
capabilities/ | Capability\ | Reusable building-block primitives (like ForgeHtmx), the recommended new home for those. |
All of the above are configurable: app_root/app_namespace rename app/ and App\; modules_root/modules_namespace rename modules/, capabilities/, and their Modules\/Capability\ namespaces. The Kernel resolves folders and namespaces from this configuration, so routing, discovery, and generation all follow what you set.
Your choice — not enforced. modules/ and capabilities/ are scanned by the same loader. The split is a recommended convention, not a requirement. You can put everything in modules/, everything in capabilities/, or mix them — all valid. Primitives like ForgeHtmx are the reference for capabilities/; most older primitives still live in modules/ until moved.
Inside app/, folders follow your forge_structure.php defaults — Controllers, Http, Services, Listeners, Models, Events, Database/Migrations, UI/views, and so on.
One of these paths deserves its own note: the injectable folders. This is where classes are automatically discovered and registered by the container so they can be injected into your code. The default names — Services and Listeners in your app, src/Services, src/Listeners and src/Providers in a module — are really just the default folder names for that injectable path. Services was the older name for this concept; the kernel now calls it injectable because that's what the folder actually does, but the folder can still be called Services (the default) if you prefer. It's a naming and semantics thing, not a hard rule.
Discovery doesn't care what the folder is called — it reads the injectable paths from your forge_structure.php and scans whatever folders those point to. So you can rename them, split them, or point the path somewhere else entirely, and the container will find your classes there instead. See Customizing Folder Structure.
Each module or capability holds its own src/ folder with the same internal layout, plus an entry file like ForgeHtmxModule.php in src/ that announces the module.
capabilities/ForgeHtmx/
└── src/
├── ForgeHtmxModule.php # the module's entry file
├── Middlewares/
├── Traits/
└── UI/assets/The Kernel looks for an entry file matching *Module.php (or {Name}.php) inside each module's src/ folder. That's what tells it a folder is a module at all.
Generated content that should never be committed to version control. The Kernel makes sure these folders exist at every boot:
storage/
├── sessions/ # session data
├── logs/ # application logs
├── database/ # sqlite databases (cache, app, security…)
├── framework/ # cache, compiled hooks, cron, trusted sources
├── app/ # user uploads (images, media)
├── bin/ # runtime binaries
└── queues/ # queue workers & outputInside storage/framework/ you'll find the Autoloader's class map cache, compiled lifecycle hooks, cron job state, and trusted_sources.json — the list of package sources the package manager trusts.
You generally never touch storage/ by hand. If a folder is missing, the Kernel recreates it the next time it boots.
The only folder your web server should serve directly. This is your app's web root.
public/
├── index.php # the web entry point
├── storage/ # symlinked public assets
├── assets/ # compiled css / js
└── favicon.icopublic/index.php is the real front door for web requests. It defines the base path, registers the Autoloader, checks for a maintenance page, and then hands off to the Kernel — Kernel::init(). The details of that path are on the Lifecycle page.
The index.php at your project root is a deliberate dead-end that returns 403, so directory browsing or direct hits on the root can't serve anything sensitive.
Every default on this page — where app/ lives, what root folders hold modules, and even the namespaces — comes from one place: the Kernel's internal structure file. You can override any of it without touching the Kernel.
You don't have to write that file by hand. Use the wizard:
php forge.php structure:init # generates forge_structure.php at your project root
php forge.php structure:info # interactively view your current configThe wizard asks what you want to customize and offers you a few choices:
modules/ and capabilities/ live and what their namespaces are.If a forge_structure.php already exists the wizard will ask whether to overwrite it or merge your new choices into it. If you'd rather write it by hand, create forge_structure.php at your project root — it's just a PHP file that returns an array. The wizard generates the same file, so the two paths are equivalent.
<?php
return [
'app_root' => 'app',
'app_namespace' => 'App',
'modules_root' => ['modules', 'capabilities'],
'modules_namespace' => ['Modules', 'Capability'],
'app' => [
'controllers' => 'Controllers',
'injectable' => ['Services', 'Listeners'],
'models' => 'Models',
'views' => 'UI/views',
],
'modules' => [
'controllers' => 'src/Controllers',
'injectable' => ['src/Services', 'src/Listeners', 'src/Providers'],
'models' => 'src/Models',
'views' => 'src/UI/views',
],
];Any key you set is merged over the Kernel's defaults — you only need to list what you're changing. The full list of keys mirrors the defaults shown elsewhere on this page: app_root, app_namespace, modules_root, modules_namespace, module_entry_files, plus per-type paths for app and modules (controllers, views, models, migrations, injectable, and more).
Read this before you run it. Changing paths does not move your existing files, and deleting forge_structure.php does not move them back. The Kernel trusts your configuration without validating that your filesystem matches it. Back up your project first, and make sure your folders actually line up with what you configure.
Two more things worth knowing:
#[Structure(structure: [...])] attribute on its module class. Those are sovereign — your project-level forge_structure.php doesn't touch them.When in doubt, run structure:info — it shows your app structure, your modules, and whether a user-defined file is active, all interactively.
Where forge.json says what you want, forge-lock.json records exactly what the package manager installed. For every module it stores the pinned version, the source (registry, git, or URL), and an integrity hash.
That integrity hash is what keeps installs reproducible and tamper-evident — the same module version resolves to the same verified content. Commit this file. It's the record that makes your build stable.
The package manager uses this file to know what's actually on disk, and the lock can be regenerated from your forge.json requirements.