AppKit Skeleton is the recommended way to start a new AppKit project. It gives you a working app with authentication, a User entity, Doctrine ORM, and a Tailwind CSS build pipeline — all wired up and ready to extend.
Everything on this page — the commands, the files, the directories — comes from the skeleton (modufolio/appkit-skeleton), not from the framework. Once created, it is application code and yours to change.
Create a project from the skeleton. It is a template — create-project copies it once and from then on the code is yours, like the rest of your app.
composer create-project modufolio/appkit-skeleton my-app
cd my-app
npm installOr clone the repository directly (useful for trying an unreleased state):
git clone https://github.com/modufolio/appkit-skeleton my-app
cd my-app
composer install
npm installCopy the example file to .env. The .env file is gitignored — never commit it.
cp .env.example .env.env.example lists four variables:
| Variable | Description |
|---|---|
APP_ENV |
dev, test, or prod. Controls caching, error verbosity, and debug mode. |
COOKIE_SECURE |
Set to true when running behind HTTPS to add the Secure flag to session cookies. Use false in development. |
REMEMBER_ME_SECRET |
Signing secret for remember-me cookies — a 64-character hex string (php -r "echo bin2hex(random_bytes(32));"). The skeleton's config/services.php reads it with env()->getRequired(), which throws when it is missing; remove the remember_me authenticator from config/security.php if you do not want it. |
APP_URL |
Optional. The skeleton's own variable, available to your code as env('APP_URL'); nothing in the framework or in the skeleton's shipped code reads it. The framework derives base URLs from the request ($this->url() in templates). |
Use the env() helper to read environment variables anywhere in your config files:
env('APP_ENV', 'prod') // returns string
env('COOKIE_SECURE', false) // returns bool when value is "true" or "false"
env()->getBool('COOKIE_SECURE') // always a bool, or throws if unset
env()->getRequired('JWT_SECRET') // throws when the secret is missingSee Configuration for the full set of typed accessors.
env() checks $_ENV, then $_SERVER, then whatever .env file was loaded into the reader — in that order, so a real environment variable always wins. Loading the file is one line in bootstrap.php: (new Env())->fromFile(BASE_DIR . '/.env')->freeze();. The skeleton's bootstrap.php does exactly that, as do the framework's own and the RoadRunner reference's. The file is read once at boot and the reader is then frozen. In production, set variables in your web server config or container environment and skip the .env file entirely; a missing file is not an error.
Limitations. The built-in env() helper reads a single .env file using parse_ini_file(). It does not support multiple layered files (.env.local, .env.test), variable interpolation, or multiline values. If you need any of those, replace it with Symfony Dotenv — see Configuration for the upgrade path.
Compile Tailwind CSS and your JavaScript bundle into public/assets/.
npm run buildCompiled files are gitignored — rebuild them in every deployment.
For a new project, create the tables directly from your entity definitions.
php bin/console orm:schema-tool:createFor an existing project with data, run migrations instead.
php bin/console migrations:migrateNever run
orm:schema-tool:createagainst a database that already has data. It will attempt to create tables that already exist.
Two directories must exist and be writable before the app can run:
| Directory | What goes in it |
|---|---|
storage/logs/ |
Application logs (app.log, error.log), written by the skeleton's FileLogger |
var/ |
Generated files: Doctrine cache and proxies (var/cache/, var/proxies/), sessions (var/sessions/), brute-force state (var/brute-force/), and a private copy of all of it per test worker (var/test/) |
Both are already present in the skeleton with a .gitkeep. Their contents are gitignored — never commit files from these directories.
storage/ is for app data that must persist across deploys — logs, uploaded files.
var/ is for generated, re-buildable files. You can safely delete its contents — the app recreates them on the next request. It must be writable by the CLI user too: bin/console boots the same container.
composer start
# → http://localhost:8000This runs the PHP built-in server with router.php as the router script. It serves files from public/ and forwards everything else to public/index.php. It is for development only.
The app ships with a login page at /login but no users. Create one with the console command.
php bin/console app:add-userFollow the prompts, or pass arguments directly.
php bin/console app:add-user you@example.com yourpassword --adminUsers with --admin get ROLE_ADMIN, which includes ROLE_USER via the role hierarchy.
Open http://localhost:8000. The welcome page says whether you are signed in; it does not link to the login form, so go to /login yourself. After authenticating, your email address and a sign-out button appear on the welcome page.
- Project structure — learn where everything lives
- Routing — add new pages
- Controllers — handle requests and render templates
- Security — protect routes and manage roles