YAFFA (Yet Another Free Financial Application) is a self-hosted personal finance web application. It enables multi-account/currency tracking, transaction categorization, investment monitoring, and long-term financial planning. Conscious manual tracking is a core product value — it is not a bank-sync tool.
- PHP 8.4 / Laravel 13 — MVC + Services architecture
- Vue 3 (Options API) + Bootstrap 5.3 + CoreUI — multi-page app, NOT a SPA
- MySQL 8 — primary database
- Redis — queue backend for background jobs
- Vite — asset bundling
- Laravel Sail — Docker-based local development
All PHP/Artisan/Composer/Node commands must be prefixed with vendor/bin/sail:
vendor/bin/sail up -d # start services
vendor/bin/sail artisan migrate # run migrations
vendor/bin/sail npm run dev # build assets (dev)
vendor/bin/sail npm run build # build assets (production)
vendor/bin/sail artisan test --compact # run tests
vendor/bin/sail bin pint --dirty # fix PHP code style- NEVER modify
.env— ask the user instead; it is off-limits at all times - Never comment on a GitHub issue or PR (
gh issue comment,gh pr comment,gh pr review, or the raw API) without asking first. Draft the comment and ask; post only after explicit approval. Creating PRs/issues, pushing commits, and other git operations already covered by an established collaborative workflow are unaffected — this is scoped to posting comments specifically. - Always rebuild assets after JS/Vue/SCSS changes before testing UI
- Always run Pint and PHPStan before finalizing PHP changes
- Run only the minimum affected tests, then ask if the full suite should follow
- Do not add dependencies or restructure directories without user approval
- QIF/CSV import — system profiles are code-only:
FileImportProfilerows oftype = system(with executablematching_rules) are defined solely inSystemFileImportProfileRegistryand applied viaartisan app:import:sync-system-profilesat deploy time. Never add an API/UI path that lets a user create or mutate asystem-typed profile or setoptions_json.matching_rules/actionson auser-typed one — see.ai/docs/features/qif-csv-import/permissions.mdandarchitecture.md(ReDoS risk note). - API personal access token
abilitiesare enforced on everyAPIcontroller, not just token-management and 2FA-management. Each controller'smiddleware()(viaIlluminate\Routing\Controllers\HasMiddleware) declaresabilities:read/abilities:write/abilities:settingsper action, scoped withIlluminate\Routing\Controllers\Middleware'sonly:parameter — a no-op for session requests (TransientToken::can()is alwaystrue), a real gate for bearer tokens. The per-controller mapping, rationale, and the five config/credential controllers that aresettings-gated on every action (including reads) are documented in.ai/docs/features/api-access-and-2fa/permissions.md. Coverage is pinned bytests/Feature/API/ApiAbilityEnforcementTest.php(one deny + one allow case per controller) — keep that data provider in sync whenever a newAPIcontroller or action is added. - A stray
storage/app/duskapiconf_tmp.txtsilently overrides liveconfig()values project-wide, for anyartisancommand, not just Dusk runs.alebatistella/duskapiconf(used bytests/DuskTestCase.phpviasetConfig()/getConfig()) persists overrides to this file, and its service provider re-applies the whole file toconfig()on every non-production boot until the file is deleted.DuskTestCase::tearDown()deletes it after every Dusk test, but a killed test run (Ctrl+C, OOM) can still leak it. If aconfig('yaffa.*')value looks wrong in this dev environment for no reason you can find in code (e.g.sandbox_modestucktrue), check this file before chasing anything else —cat storage/app/duskapiconf_tmp.txtthenrmit. Seetests/CLAUDE.md"Dusk-Specific" for the full mechanism. - Money/quantity model attributes are cast via
App\Casts\MoneyCast/App\Casts\DecimalCast(brick/math/brick/money), never a blanket'float'cast.MoneyCast(an actual currency amount — resolves itsCurrencyvia a per-modelresolve*Currency()method) yields aBrick\Money\Money;DecimalCast(a quantity/ratio that isn't itself a currency amount — a share count, an exchange rate) yields aBrick\Math\BigDecimal. Both implementSerializesCastableAttributes, sotoArray()/toJson()— and therefore every/api/v1/*response — emit these fields as decimal strings, not JSON numbers; a new API consumer must parse them as such, not assume a JSON number (seeUPGRADE.md's "API Response Precision" entry for the exact field list). Never callMoney::formatTo()from backend code — it would introduce an undocumentedext-intlruntime dependency; display formatting is centralized inresources/js/shared/lib/i18n/format.json the frontend instead. Any new non-Eloquent value object that stands in for a Transaction (e.g.App\Support\ScheduleInstance) must unwrapMoney/BigDecimalthe same way in its owntoArray(), or it'll silently serialize toMoney's own{"amount":...,"currency":...}JSON shape instead of matching the cast's decimal-string wire format. See.ai/docs/specifications/precision-improvements/for the full rationale. - AI document retention irreversibly deletes user data — keep it opt-in, per-user, finalized-only, and path-safe.
ai_user_settings.document_retention_days(NULL= keep forever; there is deliberately no env var or global default) drivesApp\Jobs\CleanupOldAiDocuments, dispatched per user byai-documents:cleanup-old-files(daily 03:30 + the maintenance button). Onlyfinalizeddocuments are deleted; old documents in any other status only trigger a reminder email. Any code that deletes files fromai_document_files.file_pathmust go throughCleanupOldAiDocuments::isOwnedPath()(insideai_documents/{userId}/, no traversal, no symlink escape) — the manual delete action does not yet, don't copy it.transactions.ai_document_idisON DELETE SET NULL: deleting a document must never delete its transaction. Drive file names are reduced to their last path segment before becoming part offile_path(ProcessGoogleDriveConfigJob::safeFileName()); keep any new import path doing the same, and still don't treat a storedfile_pathas trusted. See.ai/docs/features/ai-document-retention/. - All recurrence-pattern evaluation (budgets and transaction schedules) must go through
RecurrenceRuleService, never a hand-builtRecurr\Rule; recurrence shape is stored only in therrulecolumn.budgets/transaction_schedulespersist one RFC 5545 RRULE string (rrule—#[Hidden], not fillable, never client-writable).frequency/interval/count/end_date/by_day/by_month/days_before_month_end/last_business_day_of_monthare virtual attributes composed/decomposed byApp\Models\Concerns\HasRecurrenceRule(effectiveRrule()); those DB columns no longer exist, so neverwhere()/orderBy()/raw-SQL on them, and add any new RFC 5545 feature inHasRecurrenceRule+ValidatesRecurrenceRule, not as a new column orRecurrenceRuleServiceparameter. Occurrence methods take(Carbon $startDate, string $rrule, ...);buildRule()(publicsoTransaction::scheduleInstances()can reuse it) is the only runtime place a stored RRULE is parsed into aRecurr\Rule. Hand-built API payloads (getScheduledItems()budget rows,budgetChart()breakdowns) get no#[Appends]— list every virtual recurrence field in them explicitly.TransactionSchedule::isActive()/getNextInstance()/occursOn(),Budget, andTransaction::scheduleInstances()(forecast/budget-chart projection) all route through the service. See.ai/docs/features/budget-schedule-redesign/architecture.md.
Read .ai/docs/ before implementing a feature — it describes the domain model and product intent:
| Path | Contents |
|---|---|
.ai/docs/product-context.md |
Philosophy, goals, non-goals |
.ai/docs/assets/ |
Entity definitions (account, transaction, category, payee, investment, etc.) |
.ai/docs/features/ |
As-built feature docs, extracted from code post-implementation (architecture, flows, permissions, tests, variables) |
.ai/docs/specifications/ |
Pre-implementation design specs (background/rationale, spec, future directions) |
Code is always the source of truth if docs and code conflict. Notify the user if you find discrepancies, and suggest doc updates.
Role-specific implementation guidelines live in .ai/agents/:
| File | Purpose |
|---|---|
planning.agent.md |
Feature scoping and requirement structuring |
laravel-backend.agent.md |
Laravel backend implementation rules |
frontend.agent.md |
Vue/Blade frontend implementation rules |
testing.agent.md |
Test design and coverage rules |
documentation.agent.md |
Feature documentation extraction |
- Services over controllers: business logic lives in
app/Services/ - Form Requests: all validation via dedicated
app/Http/Requests/classes - No SPA state: Blade pages are independent; Vue components are self-contained islands
- New tests are written in Pest 5 (any level); existing PHPUnit tests are not converted without owner approval
- Feature tests preferred; browser tests only for critical E2E flows, as Pest browser tests in
tests/PEST/(excluded from the default run:pest --testsuite=Browser). Dusk is legacy — no new Dusk tests - Build output (
public/js/,public/css/) is Git-ignored — do not commit built assets
app/Http/Controllers/ thin controllers
app/Services/ business logic
app/Models/ Eloquent models
app/Policies/ authorization
app/Jobs/ queue jobs
resources/views/ Blade templates
resources/js/ Vue components + JS
tests/Unit/ pure logic tests
tests/Feature/ HTTP/API tests
tests/Browser/ Dusk E2E tests
.ai/docs/ domain documentation
.ai/agents/ agent role instructions
Run linters before committing code to catch style and quality issues.
# PHP linting (PSR-12 code style)
./vendor/bin/pint # Auto-fixes style issues
# PHP static analysis (PHPStan Level 5)
./vendor/bin/phpstan analyse # Finds type errors and bugs
# JavaScript/Vue linting
npx eslint resources/js --ext .js,.vueNote: Pint excludes vendor/, public/, storage/, bootstrap/ directories. PHPStan analyzes app/ directory only.