- Delete `.claude/rules/livewire.md` (Livewire is vestigial – only starter-kit auth screens remain) - Add `.claude/rules/frontend.md` documenting Vue 3 SPA stack, router, composables, search-URL mirroring, and Leaflet integration - Add `docs/ops/nginx-log-masking.md` runbook for redacting lat/lng from Nginx access/error logs and PHP-FPM on IONOS VPS - Update `CLAUDE.md` and `architecture.md` to reflect Vue SPA as primary frontend, REST API as backend contract, and Livewire's reduced role - Note stale service names in legacy domain docs (`scoring.md`, `prediction.md`, `notifications.md`) now refactored into `Services/Forecasting/`, `Services/StationSearch/`, and `PlanFeatures`
6.1 KiB
Architecture
Shape: Vue SPA ⇄ REST API ⇄ fat Laravel Services
The frontend is a Vue 3 single-page app (resources/js/). It talks to the
backend exclusively over a REST API (routes/api.php →
app/Http/Controllers/Api/). Controllers stay thin; all business logic lives in
Service classes (app/Services/). Blade is reduced to a one-line SPA shell,
server-rendered legal pages, transactional emails, and the Filament admin panel.
Browser
└── Vue SPA (resources/js, mounted on #app via app.blade.php)
└── axios (resources/js/axios.js — baseURL '/api', cookie + XSRF auth)
└── REST API (routes/api.php → Http/Controllers/Api/*)
└── Services (app/Services/*) ← all business logic
└── Models / Jobs / Events / Notifications
Core principle: fat Services, thin everything else
All business logic lives in Service classes. API controllers, console commands, jobs, listeners, and Filament resources are thin orchestrators — they call Services and return results. Never put domain logic in a controller or a Vue component.
Directory structure (actual)
app/
├── Console/Commands/ # Scheduler: PollFuelPrices, FetchOilPrices, BackfillOilPrices,
│ # RunLlmOverlay, EvaluateVolatilityRegime, ResolveForecastOutcomes,
│ # ArchiveOldPricesCommand, ImportBeisFuelPrices, ImportPostcodes
├── Http/
│ ├── Controllers/Api/ # AuthController, StationController, StatsController, UserController
│ ├── Controllers/ # BillingController (Stripe Checkout/Portal redirects)
│ ├── Middleware/ # VerifyApiKey (API-key gate), RequiresFeature (tier gate)
│ ├── Requests/ # Form requests
│ └── Resources/Api/ # StationResource (API output shaping)
├── Actions/Fortify/ # Fortify auth actions (CreateNewUser, …)
├── Services/ # ALL business logic — see below
├── Models/ # Eloquent models
├── Notifications/ # FuelPriceAlert + custom Channels (OneSignal, Vonage WA/SMS)
├── Jobs/ # DispatchUserNotificationJob, PollFuelPricesJob,
│ # SendScheduledWhatsAppJob, SendPaymentFailedReminderJob
├── Events/ # PricesUpdatedEvent
├── Listeners/ # HandleStripeWebhook (Cashier WebhookReceived)
├── Filament/ # Admin panel (Resources, Widgets) + Providers/Filament
├── Enums/ # FuelType, PlanTier, …
└── Livewire/Actions/ # Logout only — Livewire is otherwise vestigial
app/Services/
├── FuelPriceService.php # Fuel Finder API polling + storage
├── StationTaggingService.php # Supermarket brand detection
├── PostcodeService.php # postcode/outcode/place → lat/lng (+ LocationResult DTO)
├── PlanFeatures.php # Single source of tier-entitlement truth (see tiers.md)
├── HaversineQuery.php # Distance SQL helper
├── ApiLogger.php # Wraps outbound HTTP, logs to api_logs
├── StationSearch/ # StationSearchService (+ SearchCriteria, SearchResult DTOs)
├── BrentPriceSources/ # FRED + EIA Brent sources (+ BrentPriceFetcher)
└── Forecasting/ # Weekly forecast subsystem — WeeklyForecastService,
# LlmOverlayService, BacktestRunner, OutcomeResolver,
# VolatilityRegimeService, feature/leak tooling, …
resources/
├── js/ # Vue 3 SPA — see .claude/rules/frontend.md
└── views/
├── app.blade.php # SPA shell (mounts #app)
├── legal/ # Server-rendered legal pages
├── emails/ # Mailable templates
└── livewire/auth/ # Starter-kit Volt auth screens (left untouched)
routes/
├── web.php # SPA shell + catch-all, legal pages, billing redirects, server logout
└── api.php # REST API consumed by the SPA (see below)
Stale service names elsewhere. Domain rule files (
scoring.md,prediction.md,notifications.md) still name services that have since been refactored away — e.g.AlertScoringService,OilPriceService,NationalFuelPredictionService,NotificationDispatchService,SubscriptionService. Those concerns now live underServices/Forecasting/,Services/StationSearch/, andPlanFeatures. Always verify a service name againstapp/Services/before trusting it from those files.
Service class conventions
- Constructor injection only — no facade usage inside Services
- Each Service has one responsibility — do not merge concerns
- Return typed DTOs or Eloquent collections — never raw arrays from Services
- Services never dispatch jobs directly — that's the controller/command/listener's job
The REST API
routes/api.php is the SPA's backend. Three access tiers:
- Public (no auth):
POST /api/auth/register,POST /api/auth/login,GET /api/auth/me,GET /api/fuel-types,GET /api/stats/live - API-key (
VerifyApiKey+throttle:60,1):GET /api/stations,GET /api/stats/searches - Sanctum (
auth:sanctum):POST /api/auth/logout,/api/user/*(preferences, saved stations, profile, password, delete account)
Conventions:
- Shape responses with API Resources (
app/Http/Resources/Api/), never raw arrays. - The fuel prediction is embedded in the
/api/stationsresponse under thepredictionkey — there is no standalone prediction endpoint (seeprediction.md). - Tier-gate routes with the
featuremiddleware (RequiresFeature); never inline entitlement checks (seetiers.md). - Auth is cookie/session via Sanctum SPA mode (axios sends XSRF + credentials); the API-key gate protects the expensive station search from scraping.
Frontend
The Vue SPA is documented in .claude/rules/frontend.md. Do not build new
Livewire components — Livewire/Flux remain only for the starter-kit auth screens
and the Logout action.