A full-featured Symfony 8 example application demonstrating web authentication, a REST API, an admin panel, database fixtures, and a scheduler — all running inside a devcontainer with swappable database backends (PostgreSQL, MySQL, SQLite).
- Docker + VS Code Dev Containers or a local PHP 8.4 + Composer environment
- mise (manages PHP, Bun, gh, and task running)
Open the project in its devcontainer, then run:
mise run setupThis single command:
- Installs Composer dependencies (
vendor/) - Installs Bun (Node) dependencies (
node_modules/) - Generates Symfony secrets keys for the
devenvironment - Creates the database and runs all migrations
To set up for production secrets instead:
mise run setup prodmise run devThis starts three processes via the Procfile:
| Process | Command | Purpose |
|---|---|---|
fpm |
php-fpm |
PHP FastCGI process manager |
web |
caddy |
Reverse proxy + static file server on http://localhost:8000 |
scheduler |
messenger:consume scheduler_default |
Symfony Scheduler worker |
Web (http://localhost:8000)
| Route | Description |
|---|---|
/ |
Public home page |
/register |
Create a new account |
/login |
Sign in with email + password |
/verify/pending |
Email verification reminder (shown after login if unverified) |
/dashboard |
Authenticated user dashboard (requires verified email) |
/profile |
View your profile |
/profile/edit |
Edit name and email |
/reset-password |
Forgot password flow |
/logout |
Sign out |
Email verification is required to access protected routes. After registration an email is sent with:
- A Verify Email Address link (expires in 1 hour)
- A cancel this registration link — clicking it immediately deletes the account (no login required)
Password reset is handled by symfonycasts/reset-password-bundle with a signed, expiring link sent to the registered email.
The API uses JWT authentication via lexik/jwt-authentication-bundle.
curl -X POST http://localhost:8000/api/login \
-H 'Content-Type: application/json' \
-d '{"email":"admin@example.com","password":"password"}'{ "token": "<jwt>" }curl http://localhost:8000/api/me \
-H 'Authorization: Bearer <jwt>'{
"id": 1,
"name": "Admin User",
"email": "admin@example.com",
"roles": ["ROLE_ADMIN", "ROLE_USER"],
"emailVerified": true,
"createdAt": "2026-01-01T00:00:00+00:00"
}The interactive API documentation (via API Platform) is available at http://localhost:8000/api.
Admin Panel (http://localhost:8000/admin)
Requires ROLE_ADMIN. Log in with the seeded admin account:
| Field | Value |
|---|---|
admin@example.com |
|
| Password | password |
The admin panel (EasyAdmin 5) provides a full CRUD interface for managing users.
PostgreSQL is the default. The connection string is set in .env:
DATABASE_URL="postgresql://symfony:secret@localhost:5432/symfony?serverVersion=17&charset=utf8"
Override locally by creating .env.local (gitignored):
DATABASE_URL="sqlite:///%kernel.project_dir%/var/data_dev.db"
mise run db:use postgres # PostgreSQL (default)
mise run db:use mysql # MySQL / MariaDB
mise run db:use sqlite # SQLite (no server required)This rewrites DATABASE_URL in .env.local, adjusts Doctrine's identity_generation_preferences, wipes existing migrations, regenerates them for the new platform, and runs them.
Pass --no-migrate to switch the URL only without touching migrations:
mise run db:use sqlite --no-migrateAll tasks are in .mise/tasks/. Run any task with mise run <task>.
| Task | Description |
|---|---|
setup [env] |
Install dependencies + generate secrets + create DB + migrate |
dev |
Start the development server (FPM + Caddy + Scheduler) |
build |
Build frontend assets for production |
clean |
Remove vendor/, node_modules/, compiled assets, and cache |
console [cmd] |
Run a Symfony console command (mise run console cache:clear) |
logs |
Tail the Symfony application log |
test |
Run the PHPUnit test suite |
cache:clear |
Clear and warm up all application caches |
db:migrate |
Run pending database migrations |
db:fresh |
Drop schema, re-run all migrations, reload fixtures (destructive) |
db:seed |
Load database fixtures |
db:use <driver> |
Switch database backend (postgres, mysql, sqlite) |
messenger:consume |
Start a Messenger queue worker |
install:compose |
Install Composer dependencies only |
install:bun |
Install Bun dependencies only |
Seed the database with demo data:
mise run db:seedThis loads:
| Account | Password | Role | |
|---|---|---|---|
| Admin | admin@example.com |
password |
ROLE_ADMIN |
| Users (×10) | user.{n}@example.com |
password |
ROLE_USER |
To wipe the database and start fresh with fixtures:
mise run db:freshmise run testTests use a separate symfony_test database. The suite covers:
- Unit tests —
Userentity logic - Functional tests — registration, login, dashboard access, JWT authentication
| Layer | Package |
|---|---|
| Framework | Symfony 8 / PHP 8.4 |
| ORM | Doctrine ORM + Migrations |
| Web auth | Symfony Security (form_login) + symfonycasts/verify-email-bundle + symfonycasts/reset-password-bundle |
| API auth | lexik/jwt-authentication-bundle |
| API docs | API Platform |
| Admin | EasyAdmin 5 |
| Scheduler | Symfony Scheduler (Messenger transport) |
Symfony Mailer (SMTP → localhost:1025) |
|
| Frontend | Webpack Encore + Bootstrap 5 + Bootstrap Icons |
| Web server | Caddy 2 + PHP-FPM (Unix socket) |
| Task runner | mise |
src/ExampleApp/
├── Controller/
│ ├── Admin/ # EasyAdmin dashboard + CRUD
│ ├── Api/ # JWT-protected API endpoints
│ ├── Auth/ # Login, registration, verify email, reset password
│ ├── DashboardController.php
│ ├── HomeController.php
│ └── ProfileController.php
├── DataFixtures/ # Demo data
├── Entity/ # Doctrine entities (User, ResetPasswordRequest)
├── EventSubscriber/ # Email verification gate
├── Form/ # Symfony Form types
└── Repository/ # Doctrine repositories
templates/
├── admin/
├── auth/ # Login, register, verify pending, reset password
├── dashboard/
├── emails/ # Transactional email templates
├── home/
└── profile/