Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: Furox <177975472+Furox-Art@users.noreply.github.com> Co-authored-by: johandenhollander <johan.den.hollander@finalist.nl> Co-authored-by: João Cruz <161638790+SrJooJ@users.noreply.github.com> Co-authored-by: Saim Hashmi <hashmisaim037@gmail.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
37 KiB
AGENTS.md
This file provides guidance to agentic coding tools when working with code in this repository.
Project Overview
Coolify is an open-source, self-hostable PaaS (alternative to Heroku/Netlify/Vercel). It manages servers, applications, databases, and services via SSH. Built with Laravel 12 (using Laravel 10 file structure), Livewire 3, and Tailwind CSS v4.
Design Reference
For UI/UX design specifications, principles, and visual standards, consult the local DESIGN.md. It is the source of truth for frontend design work in this repository.
Development Environment
Docker Compose-based dev setup with services: coolify (app, which also runs Reverb WebSockets and the terminal server), postgres, redis, vite, testing-host, mailpit, minio.
# One dev instance per git branch (containers, volumes, VMs named after the branch)
./scripts/dev start [qemu-profile] # localhost VM: KVM (Linux, /dev/kvm + root/sudo) or Lima (macOS, limactl), else testing-host
./scripts/dev stop # stop containers and VMs; data is kept for the next start
./scripts/dev run # start + follow logs, stop on exit (Jean run script)
./scripts/dev urls # all instances, URLs, ports, and checkouts
./scripts/dev exec php artisan migrate # run a command in this branch's Coolify container
./scripts/dev destroy <name> # delete containers, volumes, VMs, and the port slot
./scripts/dev teardown # destroy this worktree's instance (Jean teardown before worktree deletion)
# Compose: docker-compose.dev-multi.yml Env + slots: <main checkout>/.dev-instances/ (gitignored)
# Legacy fixed-name stack (not used by scripts/dev)
docker compose -f docker-compose.yml -f docker-compose.dev.yml up -d
The main checkout serves its branch at localhost:8000 (Reverb 6001, terminal 6002, db 5432, redis 6379, vite 5173). Worktrees get a port block at 20000 + slot*10 (app +0, Reverb +1, terminal +2, db +3, redis +4, vite +5). Each instance has its own libvirt network coolify-dev-<slot> (10.221.<slot>.0/24) and VMs coolify-dev-<branch>--<profile>; VMs are reused, use php artisan dev:qemu <profile> --fresh to rebuild one. On macOS with Lima >= 2.0 (brew install lima), the localhost VM is the Lima instance coolify-dev-<slot>-<profile> (same profiles, users, and SSH key); Coolify reaches it through Lima's forwarded SSH port on host.docker.internal, and only guest ports 80/443 are forwarded to the Mac. Rebuild it with limactl delete -f <vm>. Force it with COOLIFY_DEV_SERVER_BACKEND=lima. If APP_URL in .env is a *.ts.net host, the browser ports are published with tailscale serve. Set COOLIFY_DEV_INSTANCE=<name> to run another instance from the same checkout.
Testing the Self-Hosted Upgrade Process
Use the following workflow to test a self-hosted upgrade:
-
Install the source version with the upgrade script:
bash scripts/upgrade.sh sha-6492d081362c009519481ac70e50873e39ba1861 -
Set the current Coolify version and rebuild the cached configuration:
docker exec -e COOLIFY_VERSION=4.3.0 coolify php artisan config:cache -
In the Coolify UI, click Check for Updates.
-
Confirm that an upgrade is available, then click Upgrade and verify that the upgrade completes successfully.
Common Commands
# Tests (Pest 4)
php artisan test --compact # all tests
php artisan test --compact --filter=testName # single test
php artisan test --compact tests/Feature/SomeTest.php # specific file
# Code formatting (Pint, Laravel preset)
vendor/bin/pint --dirty --format agent # format changed files
# Frontend
npm run dev # vite dev server
npm run build # production build
Testing the Service Parser
tests/Feature/ServiceParserReferenceTest.php is a golden test for both compose parsers (serviceParser with updateCompose, and applicationParser) and the proxy label and magic-variable helpers. It parses tests/Fixtures/parser/service-reference.yaml and application-reference.yaml (one service per parser feature, with setting variations and error fixtures) and every template in templates/compose, then compares the output with tests/Fixtures/parser/expected/. Only the reference expected files are committed. Template expected files are gitignored, so CI checks only that each template parses.
./scripts/parser-snapshot-diff save # before a parser change: save the output of all cases (~90 s)
./scripts/parser-snapshot-diff baseline # after the change: one line per changed case, full diff in storage/framework/testing/parser-snapshot-diff.diff
php artisan test --compact tests/Feature/ServiceParserReferenceTest.php # before you commit
UPDATE_PARSER_SNAPSHOTS=1 php artisan test --compact tests/Feature/ServiceParserReferenceTest.php # accept an intended change, then review git diff tests/Fixtures/parser/
- A refactor must give "No differences". For an intended change, check every changed line in the diff file.
- When you add a parser feature, add a small service that uses it to
service-reference.yamlorapplication-reference.yamland regenerate the expected files. ./scripts/parser-snapshot-diff <git-ref>compares with a commit instead of a saved baseline.- It covers only the latest compose parsing version. Older parsing versions are not covered.
Browser Tests (Pest Browser Plugin)
Uses pestphp/pest-plugin-browser (Playwright). Browser tests go in tests/v4/Browser/.
# Run all browser tests
php artisan test --compact tests/v4/Browser/
# Run a specific browser test file
php artisan test --compact tests/v4/Browser/LoginTest.php
# Run a specific test by name
php artisan test --compact --filter='can login with valid credentials'
Writing Browser Tests
- Place new tests in
tests/v4/Browser/. - Use
RefreshDatabaseand seed required data (at minimumInstanceSettings::create(['id' => 0])) inbeforeEach. - Key API:
visit(),fill(field, value),click(text),assertSee(),assertDontSee(),assertPathIs(),screenshot(). - Always call
screenshot()at the end of each test for debugging. - For authenticated tests, create a helper function that logs in via the UI:
function loginAsRoot(): mixed
{
return visit('/login')
->fill('email', 'test@example.com')
->fill('password', 'password')
->click('Login');
}
- See
tests/v4/Browser/LoginTest.php,tests/v4/Browser/DashboardTest.php, andtests/v4/Browser/RegistrationTest.phpfor conventions.
How Browser Tests Actually Run (no Docker, no display needed)
visit() does NOT hit the dev app on localhost:8000. Instead the Pest Browser Plugin:
- Starts a local Playwright server (
node node_modules/.bin/playwright run-server) and launches a headless Chromium from~/.cache/ms-playwright(install once withnpm install && npx playwright install chromium). - Boots an in-process amphp HTTP server on a random port that serves the Laravel app from the test process itself.
Because the "server" and the test share one PHP process, they share the phpunit env (sqlite :memory:, array cache) — so config()->set(...), model writes, and Cache calls in the test are visible to browser-issued requests, and RefreshDatabase never touches the dev Postgres.
->screenshot(filename: '...') writes real PNGs to tests/Browser/Screenshots/ — read them to visually verify UI state (toasts, modals, stray elements).
Browser Test Gotchas
Class "Redis" not foundthrown by the HTTP server: host PHP has no phpredis, and the maintenance-mode store is hard-wired to redis (config/app.php→'maintenance' => ['store' => 'redis']). Addconfig()->set('app.maintenance.store', 'array');inbeforeEach.- Every path redirects to onboarding for a fresh user (
DecideWhatToDoWithUser+showBoarding()). Finish boarding before navigating:Team::query()->update(['show_boarding' => false]); Cache::flush();— theCache::flush()is required becauseUser::currentTeam()caches the Team for an hour and the in-process server shares that cache. ->navigate('/path')races form-submit redirects. After->click('Login'), assert something on the destination page (e.g.->assertSee('Welcome to Coolify')) before callingnavigate().- Failure messages print the initial
visit()URL, not the current URL. Read the auto-saved screenshot intests/Browser/Screenshots/to see where the browser actually ended up. - Runs hang forever: stale Playwright servers from a previously killed run. Fix:
pkill -f "playwright run-server"and rerun. Healthy runs take seconds. - Guest pages miss
DOMPurify(public/js/purify.min.jsloads only@authinlayouts/base.blade.php), so toast descriptions fail on unauthenticated pages — log in first for toast-related assertions. - Layouts that call
@livewireScriptsmanually must also call@livewireStyles, otherwise Livewire's asset auto-injection is disabled and[wire\:loading]/[x-cloak]elements render visible. - Run browser test files in their own
php artisan testinvocation — combining them with non-browser test paths in one command can hang the runner.
Architecture
Backend Structure (app/)
- Actions/ — Domain actions organized by area (Application, Database, Docker, Proxy, Server, Service, Shared, Stripe, User, CoolifyTask, Fortify). Uses
lorisleiva/laravel-actionswithAsActiontrait — actions can be called as objects, dispatched as jobs, or used as controllers. - Livewire/ — All UI components (Livewire 3). Pages organized by domain: Server, Project, Settings, Security, Notifications, Terminal, Subscription, SharedVariables. This is the primary UI layer — no traditional Blade controllers. Components listen to private team channels for real-time status updates via Laravel Reverb.
- Jobs/ — Queue jobs for deployments (
ApplicationDeploymentJob), backups, Docker cleanup, server management, proxy configuration. Uses Redis queue with Horizon for monitoring. - Models/ — Eloquent models extending
BaseModelwhich provides auto-CUID2 UUID generation. Key models:Server,Application,Service,Project,Environment,Team, plus standalone database models (StandalonePostgresql,StandaloneMysql, etc.). Common traits:HasConfiguration,HasMetrics,HasSafeStringAttribute,ClearsGlobalSearchCache. - Services/ — Business logic services (ConfigurationGenerator, DockerImageParser, ContainerStatusAggregator, HetznerService, etc.). Use Services for complex orchestration; use Actions for single-purpose domain operations.
- Helpers/ — Global helpers loaded via
bootstrap/includeHelpers.phpfrombootstrap/helpers/— organized intoshared.php,constants.php,versions.php,subscriptions.php,domains.php,docker.php,services.php,github.php,proxy.php,notifications.php. - Data/ — Spatie Laravel Data DTOs (e.g.,
ServerMetadata). - Enums/ — PHP enums (TitleCase keys). Key enums:
ProcessStatus,Role(MEMBER/ADMIN/OWNER with rank comparison),BuildPackTypes,ProxyTypes,ContainerStatusTypes. - Rules/ — Custom validation rules (
ValidGitRepositoryUrl,ValidServerIp,ValidHostname,DockerImageFormat, etc.).
API Layer
- REST API at
/api/v1/with OpenAPI 3.0 attributes (use OpenApi\Attributes as OA) for auto-generated docs - Authentication via Laravel Sanctum with custom
ApiAbilitymiddleware for token abilities (read, write, deploy) ApiSensitiveDatamiddleware masks sensitive fields (IDs, credentials) in responses- API controllers in
app/Http/Controllers/Api/use inlineValidator(not Form Request classes) - Response serialization via
serializeApiResponse()helper
Authorization
- Policy-based authorization with ~15 model-to-policy mappings in
AuthServiceProvider - Custom gates:
createAnyResource,canAccessTerminal - Role hierarchy:
Role::MEMBER(1) <Role::ADMIN(2) <Role::OWNER(3) withlt()/gt()comparison methods - Multi-tenancy via Teams — team auto-initializes notification settings on creation
- Authorize every server-side read and mutation where access can vary by user, role, team, or resource. Use policies, gates, or
$this->authorize(...); never rely on hidden Blade/Livewire controls such as@canfor security. - Scope queries to the current team before returning records. Treat route and model identifiers as untrusted, and prevent users from reading or changing resources owned by another team.
- Use the session team (
currentTeam(),Model::ownedByCurrentTeam()) only to find or open records, for "my team" lists, to create new top-level records, and on team pages. A user can switch teams in another tab while a page stays open, and every user owns a personal team. - Once a component, action, or controller works on a loaded resource, use that resource's team for everything after it:
- Roles and redaction: use the policy (
$this->authorize('update', $resource)) orauth()->user()->isAdminOfTeam($resource->team()->id). Never useisMember(),isAdmin(), orrole()for resource permissions; they read the role in the session team. - Related records the user can pick (S3 storages, keys, servers, sources, tags, tokens): query with
->where('team_id', $resource->team()->id)and validate submitted IDs against that team. - New rows that belong to the resource: set
team_idfrom the resource's team, notcurrentTeam()->id.
- Roles and redaction: use the policy (
- Test this with a user who is a member of the resource's team and the owner of another team, with the session switched to the other team after mount.
- Apply authorization consistently across Livewire actions, API and web controllers, actions, downloads, exports, search, event listeners, and any other path that exposes or changes protected data.
- Default to denying access when a policy or ownership relationship is missing or ambiguous. Members must not gain access to administrative, credential, security, billing, or instance-wide data merely because they belong to the team.
- Add authorization regression tests for protected changes. Cover permitted access, member restrictions where applicable, and cross-team access; verify unauthorized reads and writes return
403or otherwise reveal no protected data. - Do not add a
TRUSTED_PROXIESsetting or changeTrustProxiesto use one. This caused problems in supported Coolify deployments. Fix IP-based rate limits and allow-lists at their call sites instead of changing proxy trust as a shortcut.
Event Broadcasting
- Laravel Reverb WebSocket server for real-time updates (port 6001) and a Node terminal WebSocket server (port 6002), both run inside the
coolifycontainer as s6 services - Server-side broadcasts use
PUSHER_BACKEND_HOST/PUSHER_BACKEND_PORT(defaults127.0.0.1:6001);PUSHER_HOST/PUSHER_PORTare browser-facing only - Status change events:
ApplicationStatusChanged,ServiceStatusChanged,DatabaseStatusChanged,ProxyStatusChanged - Livewire components subscribe to private team channels via
getListeners()
Key Domain Concepts
- Server — A managed host connected via SSH. Has settings, proxy config, and destinations.
- Application — A deployed app (from Git or Docker image) with environment variables, previews, deployment queue.
- Service — A pre-configured service stack from templates (
templates/service-templates-latest.json). - Standalone Databases — Individual database instances (Postgres, MySQL, MariaDB, MongoDB, Redis, Clickhouse, KeyDB, Dragonfly, SQLite).
- Project/Environment — Organizational hierarchy: Team → Project → Environment → Resources.
- Proxy — Traefik reverse proxy managed per server.
Server transfer (keep it complete)
A server transfer moves a server and every record that belongs to it to another Coolify instance: ServerTransferExporter writes the bundle, ServerTransferImporter reads it (app/Services/ServerTransfer/). ServerTransferCoverage records the transfer decision for every model and every column.
- When you add a model, a column, or a relation that belongs to a server or to a resource on a server (application, preview, database, service, volume, environment variable, backup, scheduled task, tag, or setting), decide whether a transfer moves it:
- Moved: export and import it, and add a round-trip assertion to
tests/Unit/ServerTransfer/ServerTransferExporterImporterTest.php. For a new model, also add it toServerTransferCoverage::TRANSFERRED. - Not moved: add the model to
ServerTransferCoverage::NOT_TRANSFERRED, or the column toNOT_EXPORTED_COLUMNS, with the reason. If the data is lost for the user, also add an export warning.
- Moved: export and import it, and add a round-trip assertion to
tests/Unit/ServerTransfer/ServerTransferCoverageTest.phpexports a server with one record of every transferred model. It fails when a model or a column has no decision. When you add a transferred model, add a record of it to the fixture of that test.docs/server-transfer-full-coverage.mdlists what to set up on a source server for a manual transfer test that covers every transferred model and setting. Add new transferred models and settings to it.scripts/transfer-test/has live end-to-end transfer tests between two dev instances (see its README). Run them after a change to the transfer flow. When you add transferred data that the unit tests cannot prove (remote commands, provider calls, queue behavior), add a check toextras-e2e.sh.- Application, Service, and the standalone databases export their
$fillablecolumns, plus hand-written lists for hidden columns. A column that is not fillable is lost unless you add it to those lists. - Do not copy a foreign key as a raw ID. IDs are different on the target. Link the record by UUID, or leave the column out with a reason.
- Team credentials that a moved record needs (integration tokens, S3 storages, git sources, private keys, cloud provider tokens) move with it.
- When the user must do a step after the transfer (for example, change a webhook URL at GitHub), add an item type to
TransferFinishChecklist: the "Finish the transfer" list on the server page of the instance that manages the server now. Give it a check where Coolify can verify the step, and a fix where it can do the step. Also add it to thepost_transfer_checklistof the bundle ({target_url}stands for the URL of the target), which the source shows as text. - On the source, a transferred server (
Server::isTransferredAway()) is only an archive, because another instance manages the host. Do not run remote commands, cloud provider calls, or DNS provider calls for it, also not when the archive is deleted.
Instance sentinels (id = 0)
Coolify seeds instance-owned rows at primary key 0. That value is a sentinel meaning “this is the Coolify instance itself”, not a normal autoincrement id. Do not migrate, resequence, or “fix” these to a positive id.
| Record | Model / lookup | Meaning |
|---|---|---|
| Root team | Team::find(0), team_id === 0 |
Instance / root team. Cloud billing and many skip-checks exempt team_id === 0. |
| Localhost server | Server::find(0) / findOrFail(0) |
The machine running Coolify. Upgrades, instance backups, and docker inspect target this server. |
| Instance settings | InstanceSettings with id = 0 |
Singleton settings row. Tests must seed InstanceSettings::create(['id' => 0]) (or forceCreate). |
| Instance Postgres | StandalonePostgresql id = 0, name coolify-db |
Coolify’s own database. UI treats database_id === 0 as the instance DB (e.g. hide delete on backup screens). |
| Local docker dest | StandaloneDocker id = 0 |
Destination on the localhost server (destination_id = 0). |
| Root user / default GitHub App | seeders | First-install defaults. |
Do not assign id = 0 to new or non-instance rows. In particular, ScheduledDatabaseBackup and ScheduledTask are ordinary schedules. Legacy installs may still have a coolify-db backup at id = 0; resolve that backup via the coolify-db relation / uuid, not ScheduledDatabaseBackup::find(0).
0 is a PHP/Eloquent landmine (empty(0) is true; keyset pagination where('id', '>', $cursor) starting at 0 skips the row). Queries that page by id must include id = 0 on the first page (no lower bound, or cursor < 0). Prefer chunkById() over a hand-rolled id > 0 cursor.
Frontend
- Livewire 3 components with Alpine.js for client-side interactivity
- Blade templates in
resources/views/livewire/ - Tailwind CSS v4 with
@tailwindcss/formsand@tailwindcss/typography - Vite for asset bundling
Laravel 10 Structure (NOT Laravel 11+ slim structure)
- Middleware in
app/Http/Middleware/— custom middleware includesCheckForcePasswordReset,DecideWhatToDoWithUser,ApiAbility,ApiSensitiveData - Kernels:
app/Http/Kernel.php,app/Console/Kernel.php - Exception handler:
app/Exceptions/Handler.php - Service providers in
app/Providers/
Livewire conventions
Dynamic lists and snapshot errors
When an add, delete, or conversion leaves controls unresponsive and the browser reports Snapshot missing on Livewire component, inspect both component keys and refresh events. Stable keys alone may not fix it.
- Give every Livewire component rendered in a loop a stable key based on the record ID, UUID, filename, or another immutable identity. Never include a collection count,
$loop->index, or a reindexed array position in the key. - Pass the same stable identity to edit/delete actions. A keyed row can survive reordering while a
wire:ignoreor teleported Alpine modal keeps its originalsubmitAction; an action such asremoveItem($index)then targets a stale position after the first deletion. Resolve the current row server-side from an ID, UUID, or stable row hash instead. - Do not broadcast one refresh event to both a parent list component and children that the parent may insert, remove, or hide during the same operation. This can queue a child update after its snapshot has been removed from the DOM.
- Split refresh responsibilities into targeted events. Refresh the parent for counts and tab visibility, and refresh an existing child list with a separate event. Use
$this->dispatch('event')->to(Component::class)instead of a page-wide event when possible. - Before targeting a child list, confirm that it existed before the mutation, still exists afterward, and is on the active tab. A newly inserted child loads current data during
mount()and does not need an immediate refresh. A removed or hidden child must not receive one. - A child that deletes itself should finish its own update, then target only the parent to refresh counts. The parent should not send a refresh back to that child when the list became empty.
- Apply the same pattern to file, directory, conversion, and external reload paths such as Compose edits. One remaining broad event can reproduce the race.
- Add regression tests that assert the scoped event names, assert the old broad event is not dispatched, and verify that keys do not depend on counts or positions. Manually repeat add/delete operations while watching the browser console.
The persistent-storage implementation is the reference pattern: Project\Service\Storage handles storageCountsChanged, while Project\Shared\Storages\All handles refreshVolumeList.
Key Conventions
- Use
php artisan make:*commands with--no-interactionto create files - Use Eloquent relationships, avoid
DB::facade — preferModel::query() - PHP 8.5: constructor property promotion, explicit return types, type hints
- Validation uses inline
Validatorfacade in controllers/Livewire components and custom rules inapp/Rules/— not Form Request classes - Run
vendor/bin/pint --dirty --format agentbefore finalizing changes - Every change must have tests — write or update tests, then run them. For bug fixes, follow TDD: write a failing test first, then fix the bug (see Test Enforcement below)
- Check sibling files for conventions before creating new files
- When adding remote shell commands, account for servers using non-root SSH users: commands pass through
parseCommandsByLineForSudo(), so test pipelines, redirects, substitutions, andsh -c/bash -cscripts with the non-root sudo parser.
Git Workflow
- Production branch:
main - Development branch:
release/v4.x, wherexis the next minor release (for example,release/v4.5while production is4.4.x). Do not usenext. - Fix PRs should target the current production branch; feature PRs should target the current
release/v4.xbranch
Laravel Boost Guidelines
The Laravel Boost guidelines are specifically curated by Laravel maintainers for this application. These guidelines should be followed closely to ensure the best experience when building Laravel applications.
Foundational Context
This application is a Laravel application running on PHP 8.5. You are an expert with the Laravel ecosystem. Always use the APIs that match the installed major version of each package — do not assume a version.
Before relying on a package's API, confirm its installed version:
- PHP packages: run
composer show --directto list direct dependencies with versions, orcomposer show <vendor/package>for a single package. - JS packages: check
package.jsonfor the installed versions.
Skills Activation
This project has domain-specific skills available in **/skills/**. You MUST activate the relevant skill whenever you work in that domain—don't wait until you're stuck.
Conventions
- You must follow all existing code conventions used in this application. When creating or editing a file, check sibling files for the correct structure, approach, and naming.
- Use descriptive names for variables and methods. For example,
isRegisteredForDiscounts, notdiscount(). - Check for existing components to reuse before writing a new one.
Verification Scripts
- Do not create verification scripts or tinker when tests cover that functionality and prove they work. Unit and feature tests are more important.
Application Structure & Architecture
- Stick to existing directory structure; don't create new base folders without approval.
- Do not change the application's dependencies without approval.
Frontend Bundling
- If the user doesn't see a frontend change reflected in the UI, it could mean they need to run
npm run build,npm run dev, orcomposer run dev. Ask them.
Documentation Files
- You must only create documentation files if explicitly requested by the user.
Replies
- Be concise in your explanations - focus on what's important rather than explaining obvious details.
=== boost rules ===
Laravel Boost
Tools
- Laravel Boost is an MCP server with tools designed specifically for this application. Prefer Boost tools over manual alternatives like shell commands or file reads.
- Use
database-queryto run read-only queries against the database instead of writing raw SQL in tinker. - Use
database-schemato inspect table structure before writing migrations or models. - Use
get-absolute-urlto resolve the correct scheme, domain, and port for project URLs. Always use this before sharing a URL with the user. - Use
browser-logsto read browser logs, errors, and exceptions. Only recent logs are useful, ignore old entries.
Searching Documentation (IMPORTANT)
- Use
search-docsbefore changes that depend on Laravel ecosystem APIs, behavior, configuration, or version-specific syntax. Skip it for copy-only edits and other changes where package documentation is irrelevant. Reuse sufficient results already in context instead of searching again. - Pass a
packagesarray to scope results when you know which packages are relevant. - Use multiple broad, topic-based queries:
['rate limiting', 'routing rate limiting', 'routing']. Expect the most relevant results first. - Do not add package names to queries because package info is already shared. Use
test resource table, notfilament 4 test resource table.
Search Syntax
- Use words for auto-stemmed AND logic:
rate limitmatches both "rate" AND "limit". - Use
"quoted phrases"for exact position matching:"infinite scroll"requires adjacent words in order. - Combine words and phrases for mixed queries:
middleware "rate limit". - Use multiple queries for OR logic:
queries=["authentication", "middleware"].
Project Rules
- This project contains committed, area-grouped rules in
.ai/ruleswhen that directory exists (settled decisions, non-obvious traps, standing constraints). Framework and package guidelines that only apply to specific paths (testing, frontend, components) also live there, under.ai/rules/boost— this is not just recorded decisions, it is load-bearing guidance you have not seen inline. Before you enter plan mode or create/edit any file, you MUST first: open @.ai/rules/index.md (it maps file globs to rule files), read every rule file whose globs cover the path(s) in scope, and rungrep -rin 'keyword' .ai/rulesto catch what a path match alone misses. Do not write code until you have read and are following every matching rule. If.ai/rulesdoes not exist, continue without it. - Record a rule with
record-ruleonly when the user explicitly asks for one. Instructions for the work at hand are not rules, no matter how emphatic: "remove this typo", "use X here" are work to do, not rules to record. Never record a rule on your own initiative, as a byproduct of a change, or to summarize what you just did. When the user does ask, pass aglob(e.g.app/Http/Controllers/**), a shorttitle, and a few-linenote. Userecord-rulerather than your native memory or notes tool, because native memory is personal and session-scoped, while only.ai/rulesis shared with the team and persists in the repo.
Artisan
- Run Artisan commands directly via the command line (e.g.,
php artisan route:list). Usephp artisan listto discover available commands andphp artisan [command] --helpto check parameters. - Inspect routes with
php artisan route:list. Filter with:--method=GET,--name=users,--path=api,--except-vendor,--only-vendor. - Read configuration values using dot notation:
php artisan config:show app.name,php artisan config:show database.default. Or read config files directly from theconfig/directory.
Tinker
- Execute PHP in app context for debugging and testing code. Do not create models without user approval, prefer tests with factories instead. Prefer existing Artisan commands over custom tinker code.
- Always use single quotes to prevent shell expansion:
php artisan tinker --execute 'Your::code();'- Double quotes for PHP strings inside:
php artisan tinker --execute 'User::where("active", true)->count();'
- Double quotes for PHP strings inside:
=== php rules ===
PHP
- Always use curly braces for control structures, even for single-line bodies.
- Use PHP 8 constructor property promotion:
public function __construct(public GitHub $github) { }. Do not leave empty zero-parameter__construct()methods unless the constructor is private. - Use explicit return type declarations and type hints for all method parameters:
function isAccessible(User $user, ?string $path = null): bool - Follow existing application Enum naming conventions.
- Prefer PHPDoc blocks over inline comments. Only add inline comments for exceptionally complex logic.
- Use array shape type definitions in PHPDoc blocks.
=== deployments rules ===
Deployment
- Laravel can be deployed using Laravel Cloud, which is the fastest way to deploy and scale production Laravel applications.
- Activate the
deploying-to-cloudskill whenever deploying to Laravel Cloud, configuring Cloud environments or resources, using the Cloud CLI, or troubleshooting Cloud deployments.
=== tests rules ===
Test Enforcement
- Add or update tests for behavior and logic changes when a test provides meaningful regression coverage.
- Pure copy, styling, and layout-only changes do not require new or updated tests.
- When test coverage applies, run the affected tests and ensure they pass.
- Test the changed behavior and its important failure modes, but do not add tests beyond them.
- Read the
testing-best-practicesskill before writing tests.
=== laravel/core rules ===
Do Things the Laravel Way
- Use
php artisan make:commands to create new files (i.e. migrations, controllers, models, etc.). You can list available Artisan commands usingphp artisan listand check their parameters withphp artisan [command] --help. - If you're creating a generic PHP class, use
php artisan make:class. - Pass
--no-interactionto all Artisan commands to ensure they work without user input. You should also pass the correct--optionsto ensure correct behavior.
Model Creation
- When creating new models, create useful factories and seeders for them too. Ask the user if they need any other things, using
php artisan make:model --helpto check the available options.
APIs & Eloquent Resources
- For APIs, default to using Eloquent API Resources and API versioning unless existing API routes do not, then you should follow existing application convention.
URL Generation
- When generating links to other pages, prefer named routes and the
route()function.
Testing
- When creating models for tests, use the factories for the models. Check if the factory has custom states that can be used before manually setting up the model.
- Faker: Use methods such as
$this->faker->word()orfake()->randomDigit(). Follow existing conventions whether to use$this->fakerorfake(). - When creating tests, make use of
php artisan make:test [options] {name}to create a feature test, and pass--unitto create a unit test. Most tests should be feature tests.
Vite Error
- If you receive an "Illuminate\Foundation\ViteException: Unable to locate file in Vite manifest" error, you can run
npm run buildor ask the user to runnpm run devorcomposer run dev.
=== laravel/v12 rules ===
Laravel 12
- CRITICAL: ALWAYS use
search-docstool for version-specific Laravel documentation and updated code examples. - This project upgraded from Laravel 10 without migrating to the new streamlined Laravel file structure.
- This is perfectly fine and recommended by Laravel. Follow the existing structure from Laravel 10. We do not need to migrate to the new Laravel structure unless the user explicitly requests it.
Laravel 10 Structure
- Middleware typically lives in
app/Http/Middleware/and service providers inapp/Providers/. - There is no
bootstrap/app.phpapplication configuration in a Laravel 10 structure:- Middleware registration happens in
app/Http/Kernel.php - Exception handling is in
app/Exceptions/Handler.php - Console commands and schedule register in
app/Console/Kernel.php - Rate limits likely exist in
RouteServiceProviderorapp/Http/Kernel.php
- Middleware registration happens in
Database
-
When modifying a column, the migration must include all of the attributes that were previously defined on the column. Otherwise, they will be dropped and lost.
-
Laravel 12 allows limiting eagerly loaded records natively, without external packages:
$query->latest()->limit(10);.
Models
- Casts can and likely should be set in a
casts()method on a model rather than the$castsproperty. Follow existing conventions from other models.
=== livewire/core rules ===
Livewire
- Livewire allows you to build dynamic, reactive interfaces in PHP without writing JavaScript.
- You can use Alpine.js for client-side interactions instead of JavaScript frameworks.
- Keep state server-side so the UI reflects it. Validate and authorize in actions as you would in HTTP requests.
=== pint/core rules ===
Laravel Pint Code Formatter
- If you have modified any PHP files, you must run
vendor/bin/pint --dirty --format agentbefore finalizing changes to ensure your code matches the project's expected style. - Do not run
vendor/bin/pint --test --format agent, simply runvendor/bin/pint --format agentto fix any formatting issues.
=== pest/core rules ===
Pest
- This project uses Pest. Create tests with
php artisan make:test --pest {name}. - Do not include the test suite directory in
{name}. UseSomeFeatureTest, notFeature/SomeFeatureTest. - Read the
testing-best-practicesskill for guidance on coverage, naming, structure, dependency isolation, and review. - Do not delete tests or test files without approval. They are part of the application.
Running Tests
- Run the narrowest set of tests that covers the change. Pass a file path or
--filter=testNametophp artisan test --compact. - Rerun a test after each change to it.
- Run
vendor/bin/pestto call the test runner directly. It accepts the same file path and--filter=testNamearguments. - After the feature tests pass, ask the user to run the complete suite with
php artisan test --compact.