Install and work with Laravel Essentials Kit generated code in Laravel applications.
php artisan boost:add-skill gts-meghni/laravel-essentials-kit
Save this content to: .claude/skills/laravel-essentials-kit-development/SKILL.md
# Laravel Essentials Kit
Use this skill when a Laravel application installs `gts-meghni/laravel-essentials-kit`, or when working with the code that kit has already generated.
## Primary Goal
- run `essentials:install` with the narrowest selection that satisfies the request, and edit the generated code directly afterwards
## Key Fact
The kit is a generator, not a runtime dependency. It writes code into the application and then does nothing. There is no package configuration file, no service provider applying behavior from inside the vendor directory, and no upgrade path. Everything the kit installs lives in `app/` and is owned by the application.
Never reach for package configuration to change behavior. Edit the generated file.
## Workflow
### 1. Determine what already exists
Check for generated files before installing anything:
- `app/Providers/EssentialsServiceProvider.php`: framework defaults
- `app/Support/ApiResponse.php`: response envelope
- `app/Exceptions/ApiExceptionRenderer.php`: global exception rendering
- `app/Http/Middleware/ForceJsonResponse.php`, `app/Http/Middleware/RequestId.php`
- `app/Support/PhoneNumber.php`, `app/Rules/AlgerianPhoneNumber.php`
- `app/Support/Otp/`: one time password generator and store, configured by `config/otp.php`
- `app/Models/Province.php`, `app/Models/Commune.php`: Algerian administrative division, seeded from `database/data/algeria.json`
- `routes/api/client.php`, `routes/api/backoffice.php`: route groups mounted from `routes/api.php`
- `app/OpenApi/`: OpenAPI attribute classes, present when `darkaonline/l5-swagger` was installed
- `config/reverb.php`, `routes/channels.php`: realtime broadcasting, present when `laravel/reverb` was installed
- `docker/production/`: the production deployment, present when the docker step has run
If the relevant file exists, edit it. Do not re-run the installer to change behavior; re-running requires `--force` and overwrites local edits.
### 2. Install only what is missing
```bash
php artisan essentials:install --features=api --no-interaction
```
Features: `database`, `models`, `dates`, `security`, `observability`, `api`, `route-groups`, `health`, `otp`, `phone`, `geo`, `names`.
Pass `--no-interaction` in any scripted context. Without explicit flags a non-interactive run does nothing at all, which is intended. Other steps are flags, not features: `--packages=*`, `--redis=`, `--redis-cache`, `--timezone`, `--tooling`, `--docker`, `--cleanup`.
`--redis=` takes `none`, `phpredis`, or `predis`. It sets `REDIS_CLIENT`, requires `predis/predis` for the predis client, and adds `REDIS_HOST`, `REDIS_PORT`, and `REDIS_PASSWORD` only when they are absent. `--redis-cache` also sets `CACHE_STORE=redis`. Nothing sets `QUEUE_CONNECTION` or `SESSION_DRIVER`; change those yourself when a worker is actually supervised. `phpredis` is a PECL extension, so the installer never tries to install it.
Do not run `php artisan install:api` first. The installer creates `routes/api.php` and adds `api:` to `withRouting()` itself, without pulling in Sanctum.
### 3. Use the generated API surface
Return responses through the envelope:
```php
return ApiResponse::success(data: UserResource::collection($users));
return ApiResponse::paginated($users);
return ApiResponse::error(message: 'Not found.', status: 404);
return ApiResponse::noContent();
```
Do not add per-controller try/catch blocks. `ApiExceptionRenderer` already converts every uncaught throwable into the same envelope, including validation, authentication, authorization, and HTTP exceptions.
To vary error messages by route group, edit the `messages()` method in `ApiExceptionRenderer`.
The API middleware and exception rendering are wired in `bootstrap/app.php`, not in a service provider. That patch also adds `$middleware->trustProxies(at: '*')`, which is what makes `https://` URLs, signed URLs, and real client IPs work behind a reverse proxy. Do not replace it with `URL::forceScheme('https')`: that corrects the URLs while `$request->ip()` still returns the proxy, so the rate limiter keys every client the same and the logs show one address. Narrow the `'*'` only if the application becomes reachable without going through the proxy.
### 4. Document endpoints
When `app/OpenApi/` exists, every endpoint is documented by its own attribute-only class under `app/OpenApi/Client/Endpoints/` or `app/OpenApi/Backoffice/Endpoints/`. Copy the generated `HealthEndpoint` as the template.
The two definitions are separate documents: `config/l5-swagger.php` scans `app/OpenApi/Client` for one and `app/OpenApi/Backoffice` for the other, with `app/OpenApi/Schemas` and `app/OpenApi/Parameters` shared by both. Put a schema used by one side only in that side's directory.
Reference the shared schemas rather than restating the envelope:
```php
content: new OA\JsonContent(ref: '#/components/schemas/ErrorResponse'),
new OA\Property(property: 'meta', ref: '#/components/schemas/PaginationMeta'),
```
Access is guarded by `app/Http/Middleware/ProtectApiDocs`: `L5_SWAGGER_ENABLED=false` returns 404, and `L5_SWAGGER_USERNAME` plus `L5_SWAGGER_PASSWORD` demand HTTP basic credentials. Set both in production.
### 5. Use the geographic data
When `app/Models/Province.php` exists, the Algerian administrative division is already modelled. Do not add another provinces or communes table, and do not re-derive the data from an external source.
```php
Province::where('code', '16')->first(); // codes are zero padded strings, "01" not 1
$province->communes;
$commune->translatedName(); // current locale, falling back to French
```
Names live in a `name` JSON column cast to an array, keyed `ar`, `fr`, `en`. Query a locale with `where('name->fr', ...)`. Sorting a full commune list by localized name has no index behind it, so paginate or filter by province first.
Re-seed with `php artisan db:seed --class=AlgeriaGeoSeeder`, which updates in place rather than duplicating.
When `database/factories/Faker/AlgerianPerson.php` exists, seed people with its formatters, not with Faker's `name()` or `firstName()`:
```php
fake()->algerianName(); // random gender
fake()->algerianName('female'); // 'male' or 'female', anything else throws
fake()->algerianLastName(); // surnames are shared across genders
fake()->algerianMobile(); // passes AlgerianPhoneNumber
```
When a model stores gender, pick it once and pass it to `algerianName($gender)` so the name matches the column. Add names by editing the lists in `AlgerianPerson`, not by adding a second provider.
### 6. Keep Telescope out of production
The installer registers Telescope from `AppServiceProvider::register()`, behind `$this->app->environment('local')` and a `class_exists()` check, instead of listing `App\Providers\TelescopeServiceProvider` in `bootstrap/providers.php`. Telescope is a dev dependency, so naming it in the manifest fatals every request after `composer install --no-dev`. Leave that conditional in place, and never move the provider back into the manifest.
### 7. Read the timezone from the environment
When `config/app.php` reads `env('APP_TIMEZONE', 'Africa/Algiers')`, the timezone step has run. Change the zone through `APP_TIMEZONE` in the environment, not by editing the config default, so deployments can differ.
### 8. Use one health endpoint
`GET /api/health` is the health check. The framework's `health: '/up'` is removed by the cleanup step once that route exists, so do not reference `/up` and do not add it back. `/api/health` does not answer during `php artisan down`; if a probe must survive maintenance mode, say so rather than reinstating `/up` silently.
### 9. Issue one time passwords through the store
When `app/Support/Otp/` exists, resolve `OtpStore` and use it. Do not generate codes inline, and do not add a second cache key holding a code.
```php
$code = app(OtpStore::class)->issue($phone); // then send it yourself
app(OtpStore::class)->verify($phone, $code); // true once, then consumed
```
`OtpGenerator` only produces digits. Attempt limits, expiry, and comparison live in `OtpStore`, so put new lifecycle behavior there rather than in a generator subclass.
Delivery is not part of the feature. Send the code from application code, whether a notification, a job, or an SMS client, and keep that out of `app/Support/Otp/`.
Codes are `111111` under `local` and `testing`, and random everywhere else, including staging. There is no configuration flag for this and none should be added: a flag copied into production would make every code guessable. Tests can rely on `111111` without arranging anything.
### 10. Treat the production deployment as owned config
`docker/production/` is written by `--docker` and read by nothing in the package. Edit the files; never re-run the installer to change them, and note that a second run keeps every existing file unless `--force` is passed.
`docker/production/README.md` is the reference for environment keys, sizing tables, the timeout ladder, and post-deploy checks. Read it before changing a value there.
Four constraints hold across those files:
- `pm.max_children` in `php/php-fpm.conf` and `APP_MEM_LIMIT` in `docker-compose.yml` move together: `max_children x 48MB + 128MB <= APP_MEM_LIMIT`. Raising the pool alone converts a slow request into an OOM kill.
- The published port stays on `127.0.0.1`. Docker's DNAT rules run before UFW's filter rules, so `HOST_IP=0.0.0.0` exposes the stack to the internet and no firewall rule takes it back.
- Healthchecks probe `GET /api/health` and assert `"database":"ok"` in the body, never Laravel's `/up`. The one exception is the `reverb` service, which probes Reverb's own `/up` on port 8080, served by the WebSocket server rather than the router.
- Deploy with `docker compose up -d`, never `down`, or the queue worker loses the job in flight and every service is recreated.
`env()` outside `config/` returns `null` in this deployment, because the entrypoint runs `config:cache` and the framework then never reads the environment file again. It cannot reproduce locally. Use `config('services.foo.key')` in application code, and add the `config/` entry in the same commit as a new environment key.
When the app uses Reverb, the stack has a `reverb` service and both nginx layers proxy `/app/` (WebSocket connections) and `/apps/` (Reverb's signed HTTP API) to it:
- Clients connect to the API's own host on 443 with `REVERB_APP_KEY`. Never publish the reverb port to the host.
- `REVERB_HOST` in the deploy environment is the reverb container name with `REVERB_PORT=8080` and `REVERB_SCHEME=http`, because it is where the app and queue containers send broadcasts, not the public host.
- `ShouldBroadcast` events go through the queue. If clients receive nothing, read the queue container log before the reverb one. Use `ShouldBroadcastNow` only for events that must not wait behind other jobs.
- Do not add Laravel routes under `/app/` or `/apps/`; nginx sends both prefixes to reverb. `/broadcasting/auth` stays a Laravel route.
No CI pipeline ships with the step. Whatever builds the image must also write the environment file the compose file and containers read.
## Rules, References, and Templates
Read before executing:
- `app/Providers/EssentialsServiceProvider.php` in the target application, to see which defaults are active
## Examples
- The app needs consistent JSON errors: run `essentials:install --features=api`, then delete any pre-existing exception handling that duplicates the renderer.
- The app should stop allowing `migrate:fresh` in production: run `essentials:install --features=database`, which generates `bootDatabase()`.
- The app needs realtime updates: run `essentials:install --packages=reverb --no-interaction`, define the channel authorization in `routes/channels.php`, and broadcast with an event implementing `ShouldBroadcast`. Run `--docker --force` afterwards only if the deployment files have no local edits, since the reverb sections are written at docker step time.
- Validation messages need to differ per module: edit `messages()` in the generated `ApiExceptionRenderer`, not any package configuration.
## Anti-patterns
- do not look for `config/laravel-essentials-kit.php`; it does not exist
- do not re-run the installer with `--force` to tweak behavior, since it discards local edits
- do not wrap controller bodies in try/catch when the API feature is installed
- do not use the cleanup step on a repository with uncommitted changes
- do not call `env()` outside `config/` in an application deployed with the docker step
- do not point a healthcheck, probe, or uptime monitor at `/up`; that route is removed
Use this skill when a Laravel application installs gts-meghni/laravel-essentials-kit, or when working with the code that kit has already generated.
essentials:install with the narrowest selection that satisfies the request, and edit the generated code directly afterwardsThe kit is a generator, not a runtime dependency. It writes code into the application and then does nothing. There is no package configuration file, no service provider applying behavior from inside the vendor directory, and no upgrade path. Everything the kit installs lives in app/ and is owned by the application.
Never reach for package configuration to change behavior. Edit the generated file.
Check for generated files before installing anything:
app/Providers/EssentialsServiceProvider.php: framework defaultsapp/Support/ApiResponse.php: response envelopeapp/Exceptions/ApiExceptionRenderer.php: global exception renderingapp/Http/Middleware/ForceJsonResponse.php, app/Http/Middleware/RequestId.phpapp/Support/PhoneNumber.php, app/Rules/AlgerianPhoneNumber.phpapp/Support/Otp/: one time password generator and store, configured by config/otp.phpapp/Models/Province.php, app/Models/Commune.php: Algerian administrative division, seeded from database/data/algeria.jsonroutes/api/client.php, routes/api/backoffice.php: route groups mounted from routes/api.phpapp/OpenApi/: OpenAPI attribute classes, present when darkaonline/l5-swagger was installedconfig/reverb.php, routes/channels.php: realtime broadcasting, present when laravel/reverb was installeddocker/production/: the production deployment, present when the docker step has runIf the relevant file exists, edit it. Do not re-run the installer to change behavior; re-running requires --force and overwrites local edits.
php artisan essentials:install --features=api --no-interaction
Features: database, models, dates, security, observability, api, route-groups, health, otp, phone, geo, names.
Pass --no-interaction in any scripted context. Without explicit flags a non-interactive run does nothing at all, which is intended. Other steps are flags, not features: --packages=*, --redis=, --redis-cache, --timezone, --tooling, --docker, --cleanup.
--redis= takes none, phpredis, or predis. It sets REDIS_CLIENT, requires predis/predis for the predis client, and adds REDIS_HOST, REDIS_PORT, and REDIS_PASSWORD only when they are absent. --redis-cache also sets CACHE_STORE=redis. Nothing sets QUEUE_CONNECTION or SESSION_DRIVER; change those yourself when a worker is actually supervised. phpredis is a PECL extension, so the installer never tries to install it.
Do not run php artisan install:api first. The installer creates routes/api.php and adds api: to withRouting() itself, without pulling in Sanctum.
Return responses through the envelope:
return ApiResponse::success(data: UserResource::collection($users));
return ApiResponse::paginated($users);
return ApiResponse::error(message: 'Not found.', status: 404);
return ApiResponse::noContent();
Do not add per-controller try/catch blocks. ApiExceptionRenderer already converts every uncaught throwable into the same envelope, including validation, authentication, authorization, and HTTP exceptions.
To vary error messages by route group, edit the messages() method in ApiExceptionRenderer.
The API middleware and exception rendering are wired in bootstrap/app.php, not in a service provider. That patch also adds $middleware->trustProxies(at: '*'), which is what makes https:// URLs, signed URLs, and real client IPs work behind a reverse proxy. Do not replace it with URL::forceScheme('https'): that corrects the URLs while $request->ip() still returns the proxy, so the rate limiter keys every client the same and the logs show one address. Narrow the '*' only if the application becomes reachable without going through the proxy.
When app/OpenApi/ exists, every endpoint is documented by its own attribute-only class under app/OpenApi/Client/Endpoints/ or app/OpenApi/Backoffice/Endpoints/. Copy the generated HealthEndpoint as the template.
The two definitions are separate documents: config/l5-swagger.php scans app/OpenApi/Client for one and app/OpenApi/Backoffice for the other, with app/OpenApi/Schemas and app/OpenApi/Parameters shared by both. Put a schema used by one side only in that side's directory.
Reference the shared schemas rather than restating the envelope:
content: new OA\JsonContent(ref: '#/components/schemas/ErrorResponse'),
new OA\Property(property: 'meta', ref: '#/components/schemas/PaginationMeta'),
Access is guarded by app/Http/Middleware/ProtectApiDocs: L5_SWAGGER_ENABLED=false returns 404, and L5_SWAGGER_USERNAME plus L5_SWAGGER_PASSWORD demand HTTP basic credentials. Set both in production.
When app/Models/Province.php exists, the Algerian administrative division is already modelled. Do not add another provinces or communes table, and do not re-derive the data from an external source.
Province::where('code', '16')->first(); // codes are zero padded strings, "01" not 1
$province->communes;
$commune->translatedName(); // current locale, falling back to French
Names live in a name JSON column cast to an array, keyed ar, fr, en. Query a locale with where('name->fr', ...). Sorting a full commune list by localized name has no index behind it, so paginate or filter by province first.
Re-seed with php artisan db:seed --class=AlgeriaGeoSeeder, which updates in place rather than duplicating.
When database/factories/Faker/AlgerianPerson.php exists, seed people with its formatters, not with Faker's name() or firstName():
fake()->algerianName(); // random gender
fake()->algerianName('female'); // 'male' or 'female', anything else throws
fake()->algerianLastName(); // surnames are shared across genders
fake()->algerianMobile(); // passes AlgerianPhoneNumber
When a model stores gender, pick it once and pass it to algerianName($gender) so the name matches the column. Add names by editing the lists in AlgerianPerson, not by adding a second provider.
The installer registers Telescope from AppServiceProvider::register(), behind $this->app->environment('local') and a class_exists() check, instead of listing App\Providers\TelescopeServiceProvider in bootstrap/providers.php. Telescope is a dev dependency, so naming it in the manifest fatals every request after composer install --no-dev. Leave that conditional in place, and never move the provider back into the manifest.
When config/app.php reads env('APP_TIMEZONE', 'Africa/Algiers'), the timezone step has run. Change the zone through APP_TIMEZONE in the environment, not by editing the config default, so deployments can differ.
GET /api/health is the health check. The framework's health: '/up' is removed by the cleanup step once that route exists, so do not reference /up and do not add it back. /api/health does not answer during php artisan down; if a probe must survive maintenance mode, say so rather than reinstating /up silently.
When app/Support/Otp/ exists, resolve OtpStore and use it. Do not generate codes inline, and do not add a second cache key holding a code.
$code = app(OtpStore::class)->issue($phone); // then send it yourself
app(OtpStore::class)->verify($phone, $code); // true once, then consumed
OtpGenerator only produces digits. Attempt limits, expiry, and comparison live in OtpStore, so put new lifecycle behavior there rather than in a generator subclass.
Delivery is not part of the feature. Send the code from application code, whether a notification, a job, or an SMS client, and keep that out of app/Support/Otp/.
Codes are 111111 under local and testing, and random everywhere else, including staging. There is no configuration flag for this and none should be added: a flag copied into production would make every code guessable. Tests can rely on 111111 without arranging anything.
docker/production/ is written by --docker and read by nothing in the package. Edit the files; never re-run the installer to change them, and note that a second run keeps every existing file unless --force is passed.
docker/production/README.md is the reference for environment keys, sizing tables, the timeout ladder, and post-deploy checks. Read it before changing a value there.
Four constraints hold across those files:
pm.max_children in php/php-fpm.conf and APP_MEM_LIMIT in docker-compose.yml move together: max_children x 48MB + 128MB <= APP_MEM_LIMIT. Raising the pool alone converts a slow request into an OOM kill.127.0.0.1. Docker's DNAT rules run before UFW's filter rules, so HOST_IP=0.0.0.0 exposes the stack to the internet and no firewall rule takes it back.GET /api/health and assert "database":"ok" in the body, never Laravel's /up. The one exception is the reverb service, which probes Reverb's own /up on port 8080, served by the WebSocket server rather than the router.docker compose up -d, never down, or the queue worker loses the job in flight and every service is recreated.env() outside config/ returns null in this deployment, because the entrypoint runs config:cache and the framework then never reads the environment file again. It cannot reproduce locally. Use config('services.foo.key') in application code, and add the config/ entry in the same commit as a new environment key.
When the app uses Reverb, the stack has a reverb service and both nginx layers proxy /app/ (WebSocket connections) and /apps/ (Reverb's signed HTTP API) to it:
REVERB_APP_KEY. Never publish the reverb port to the host.REVERB_HOST in the deploy environment is the reverb container name with REVERB_PORT=8080 and REVERB_SCHEME=http, because it is where the app and queue containers send broadcasts, not the public host.ShouldBroadcast events go through the queue. If clients receive nothing, read the queue container log before the reverb one. Use ShouldBroadcastNow only for events that must not wait behind other jobs./app/ or /apps/; nginx sends both prefixes to reverb. /broadcasting/auth stays a Laravel route.No CI pipeline ships with the step. Whatever builds the image must also write the environment file the compose file and containers read.
Read before executing:
app/Providers/EssentialsServiceProvider.php in the target application, to see which defaults are activeessentials:install --features=api, then delete any pre-existing exception handling that duplicates the renderer.migrate:fresh in production: run essentials:install --features=database, which generates bootDatabase().essentials:install --packages=reverb --no-interaction, define the channel authorization in routes/channels.php, and broadcast with an event implementing ShouldBroadcast. Run --docker --force afterwards only if the deployment files have no local edits, since the reverb sections are written at docker step time.messages() in the generated ApiExceptionRenderer, not any package configuration.config/laravel-essentials-kit.php; it does not exist--force to tweak behavior, since it discards local editsenv() outside config/ in an application deployed with the docker step/up; that route is removedHow can I help you explore Laravel packages today?