rajtik76 / nette-tailwind-starter-kit
Nette starter kit with Tailwind CSS: authentication, dashboard and settings
Package info
github.com/rajtik76/nette-tailwind-starter-kit
Type:project
pkg:composer/rajtik76/nette-tailwind-starter-kit
Requires
- php: >= 8.4
- latte/latte: ^3.1
- nette/application: ^3.2.3
- nette/assets: ^1.0.0
- nette/bootstrap: ^3.2.6
- nette/caching: ^3.2
- nette/database: ^3.2
- nette/di: ^3.2
- nette/forms: ^3.2
- nette/http: ^3.3
- nette/mail: ^4.0
- nette/robot-loader: ^4.0
- nette/security: ^3.2
- nette/utils: ^4.0
- psr/clock: ^1.0
- rajtik76/nette-console: ^0.2
- rajtik76/nette-middleware: ^0.3
- rajtik76/nette-migrations: ^0.2
- tracy/tracy: ^2.10
Requires (Dev)
- fakerphp/faker: ^1.24
- nette/phpstan-rules: ^1.0
- nette/tester: ^2.5
- phpstan/extension-installer: ^1.4
- phpstan/phpstan: ^2.1
Suggests
None
Provides
None
Conflicts
None
Replaces
None
README
A Nette counterpart of the Laravel Livewire Starter Kit: authentication, dashboard and account settings on Latte templates, Tailwind CSS 4 and Naja for AJAX. Built on the rajtik76/nette-tailwind skeleton.
- PHP 8.4+, Nette 3.2, Latte 3
- Tailwind CSS 4 via
@tailwindcss/vite, dark mode via the.darkclass - Vite 8 with
@nette/vite-plugin, assets served bynette/assets - Naja for AJAX forms (the Livewire counterpart), Alpine.js for small UI behaviour
- Route middleware from rajtik76/nette-middleware
- Nette Database Explorer, SQLite by default
- Migrations, seeders and factories from rajtik76/nette-migrations
- Mail through
nette/mailwith Latte templates, written tolog/mailuntil SMTP is set
Features
- Log in with remember me, registration, logout
- Forgotten and reset password, email verification, password confirmation
- Login and mail rate limiting, signed verification links
- Dashboard in an application layout with a sidebar, mobile menu and user menu
- Settings: profile (a new email is verified again), password, appearance (light, dark, system), account deletion
- Every auth feature can be switched off in
config/common.neon
Two-factor authentication and API tokens are not part of the kit yet.
Requirements
- PHP 8.4 or newer
- Composer
- Node.js 22.12+ and npm (
.nvmrcpins the version used in CI)
Installation
composer create-project rajtik76/nette-tailwind-starter-kit my-app
cd my-app
npm install
npm run build
php nette migrate --seed
create-project runs php nette key:generate, which writes appKey into config/local.neon.
From a clone of the repository, run it yourself:
git clone https://github.com/rajtik76/nette-tailwind-starter-kit.git my-app
cd my-app
composer install
php nette key:generate
npm install
npm run build
php nette migrate --seed
Log in as test@example.com with the password password.
Make sure the temp/ and log/ directories are writable.
npm run build is required. Vite is enabled in config/common.neon, so the page
fails with "Failed to read Vite manifest" until the assets are built.
Database
The default connection is SQLite in database/database.sqlite (ignored by git). The file
is created on the first connection, so php nette migrate is all it takes. To use MySQL,
MariaDB or PostgreSQL, override the connection in config/local.neon:
database:
dsn: 'mysql:host=127.0.0.1;dbname=app'
user: root
password: secret
php nette migrate --seed creates the user test@example.com with the password password.
php nette migrate:fresh --seed # drop all tables, migrate and seed again
php nette make:migration create_posts_table
php nette make:factory Post
php nette make:seeder Post
Migrations live in database/migrations, factories and seeders in database/factories
and database/seeders. See rajtik76/nette-migrations.
Authentication
The auth core in app/Auth is the counterpart of Laravel Fortify.
It knows nothing about presenters or forms, so a Latte UI and a JSON API can share it.
It stays self-contained to be extracted into a package later.
Actions (App\Auth\Actions) take plain input arrays and throw
App\Auth\Validation\ValidationException, whose errors hold field => message:
| Action | Input |
|---|---|
RegisterUser |
name, email, password, password_confirmation; logs in, sends the verification link |
AuthenticateUser |
email, password, remember; 5 attempts per minute per email + IP |
LogoutUser |
|
SendPasswordResetLink |
email; one link per minute |
ResetPassword |
token, email, password, password_confirmation; tokens expire in 60 minutes |
SendEmailVerification |
user; 6 per minute, throws TooManyAttemptsException |
VerifyEmail |
user + id, hash, expires, signature from the signed link (60 minutes) |
ConfirmPassword |
user + password; valid for 3 hours (PasswordConfirmation::isRecent()) |
UpdateProfile |
user + name, email; a new email must be verified again |
UpdatePassword |
user + current_password, password, password_confirmation |
DeleteUser |
user + password; logs out |
Emails are lowercased, like Fortify's lowercase_usernames. The session stores only the user id;
Authenticator reloads the identity on every request, so a deleted user is logged out.
Customise validation and persistence in app/Actions/Auth, the counterpart of
app/Actions/Fortify. These classes implement the contracts in App\Auth\Contracts
(CreatesNewUsers, ResetsUserPasswords, UpdatesUserPasswords,
UpdatesUserProfileInformation, DeletesUsers) and are registered in services.neon.
Shared rules live in ValidationRules. App\Mail\AuthMailer sends the auth mails.
Configuration in config/common.neon, the counterpart of config/fortify.php:
auth: secret: %appKey% # signs email verification links strictPasswords: %productionMode% # min 12 chars, mixed case, numbers, symbols features: registration: true resetPasswords: true emailVerification: true links: # destinations used in mails verifyEmail: Auth:verifyEmail resetPassword: Auth:resetPassword rememberFor: 30 days # login expiration with "remember me" expireAfter: 2 hours # inactivity expiration without it passwordTimeout: 10800 # seconds a password confirmation lasts
php nette key:generate writes a random appKey into config/local.neon (it runs after
composer create-project). Rewriting an existing local.neon drops its comments.
Pages and routes
Routes follow the Laravel starter kit and are listed one by one in app/Core/RouterFactory.php.
There is no catch-all <presenter>/<action> route, so route middleware cannot be bypassed.
| Route | Presenter | Middleware |
|---|---|---|
/login, /register, /forgot-password, /reset-password/<token> |
Auth |
guest |
/verify-email, /email/verify/<id>/<hash> |
Verification |
auth |
/confirm-password, /logout (POST) |
Auth |
auth |
/dashboard |
Dashboard |
auth, verified |
/settings/profile |
Settings |
auth |
/settings/appearance |
Settings |
auth, verified |
/settings/security |
Settings |
auth, verified, password.confirm |
Routes of disabled auth features are not registered. Middleware lives in app/Http/Middleware:
Authenticate, RedirectIfAuthenticated, EnsureEmailIsVerified and RequirePassword.
Authenticate and RequirePassword store the request and pass its key as backlink, so
the user returns to the original page after logging in or confirming the password.
Auth pages are plain POST forms, like the Fortify views. Settings forms have the ajax class
and go through Naja: validation errors and toasts appear without a reload. A redirect is always
a full page load, because it usually leads to another layout.
Templates use Tailwind utilities directly. Shared pieces are Latte blocks in
app/Presentation/@ui.latte (form field, checkbox, auth header, logo) and Heroicons in
@icons.latte. The button() Latte function returns the classes of a button variant
(primary, danger, filled, ghost). Dropdowns, the mobile sidebar, the delete-account
dialog, password visibility and the appearance switch use Alpine.js.
Out of the box, mails are written to log/mail as .eml files (Nette\Mail\FileMailer),
like Laravel's MAIL_MAILER=log, so nothing breaks without an SMTP server.
To send them, override the mailer in config/local.neon, e.g. for
Mailpit (UI on http://localhost:8025):
services:
mail.mailer: Nette\Mail\SmtpMailer(host: 127.0.0.1, port: 1025, username: '', password: '')
docker run -d --name mailpit -p 1025:1025 -p 8025:8025 axllent/mailpit
In production, set your SMTP server and credentials the same way and change mailFrom
and appName. A failing SMTP server is an error (HTTP 500), as in Laravel with synchronous
notifications.
App\Mail\TemplateMailer renders app/Mail/templates/<name>.latte inside the shared
@layout.latte; the <title> block becomes the subject.
Development
Run the Vite dev server and a PHP server side by side:
npm run dev
php -S localhost:8000 -t www
Then open http://localhost:8000. In debug mode, {asset 'main.js'} loads assets
from the Vite dev server, so changes to templates and CSS show up immediately.
Without the dev server running, the last npm run build output is used.
Laravel Herd / Valet need a custom driver that serves www/ as the document root.
wwwDir is set explicitly in app/Bootstrap.php, because these servers boot
index.php through their own router script.
Console
php nette is the Nette counterpart of php artisan, provided by
rajtik76/nette-console:
php nette list # all commands
php nette make:command SendReport # app/Console/SendReportCommand.php, runs as app:send-report
php nette cache:clear # delete temp/cache
php nette package:discover # extensions registered by installed packages
Packages declare their DI extensions in composer.json (extra.nette.extensions) and
Bootstrap registers them automatically, so their commands work right after composer require.
Classes ending with Command in app/ are registered by search in services.neon.
The console runs in debug mode, so config changes apply without clearing the cache.
Set NETTE_DEBUG=0 in production.
Checks
composer run lint:check # Latte templates (own engine, warnings fail) + NEON config
composer run type:check # PHPStan level 8
composer run tester # Nette Tester
composer run ci:check # all of the above, same as GitHub CI
Tests render the layout, so run npm run build first.
CI runs on PHP 8.4 and 8.5, Node version comes from .nvmrc.
Testing
tests/config.neon switches the container to an in-memory SQLite database (migrated by
tests/bootstrap.php), an ArrayMailer that keeps sent mails, a FrozenClock and a memory cache.
Helpers in tests/Support/functions.php:
$container = require __DIR__ . '/../../bootstrap.php'; actingAs($container, createUser($container, ['email_verified_at' => null])); $response = request($container, 'POST', '/settings/profile?do=profileForm-submit', $post, ajax: true); redirectPath($response); // '/verify-email', without the flash id html($response); // rendered page mailLink(mailer($container)->last());
request() goes through the router and the route middleware, so tests cover access control too.
tests/Auth tests the auth actions, tests/Presentation the pages.
How Tailwind is wired in
assets/main.cssimports Tailwind and adds@source "../app". Vite's root isassets/, so without it Tailwind would not scan the Latte templates inapp/.assets/main.jsis the Vite entry point and importsmain.css.vite.config.tsregisters thetailwindcss()plugin next to the Nette plugin.app/Presentation/@layout.latterenders{asset? 'main.js'}, which outputs both the<script>and the<link rel="stylesheet">for the bundled CSS.- The
dark:variant follows the.darkclass on<html>. An inline script in@layout.lattesets it before the first paint fromlocalStorage(appearanceislight,darkor missing for system);assets/appearance.jschanges it at runtime and the Appearance settings page switches it through theappearanceAlpine component.
Use Tailwind classes directly in any .latte file under app/.
Welcome and error pages
- Colors and the font are defined in
@themeinassets/main.css. The font (Instrument Sans) is loaded from Bunny Fonts by the welcome page and the auth and app layouts, through theheadblock of@layout.latte. Error pages make no external requests and fall back to the system font. - The cube illustrations are inline SVG blocks in
app/Presentation/@cubes.latte. Error4xx/@layout.lattewraps every 4xx template in a shared frame, so403.latte,404.latteetc. stay plain markup.500.phtmland503.phtmluse neither Tailwind nor the layout, because they must render even when the app or the asset build is broken. Their CSS is inline, so when you change colors inmain.css, update them there by hand.
Production
npm run build
php nette key:generate
NETTE_DEBUG=0 php nette migrate --force
Set the database, SMTP, mailFrom and appName in config/local.neon.
strictPasswords follows production mode, so new passwords need 12 characters
with mixed case, numbers and symbols.
Point the web server's document root to www/. Built assets go to www/assets/
(ignored by git, so build them on deploy).
Important: app/, config/, database/, log/ and temp/ must never be web-accessible.
See the security warning.