Developer Guide
This guide covers everything you need to contribute to SparkShop or run it in a development environment — local setup, Docker, branching workflow, and deployment pipeline.
Tech Stack
| Layer | Technology |
|---|---|
| Backend | Laravel 13, PHP 8.4 |
| Admin Panel | Filament 5 |
| Database (dev) | PostgreSQL 18 (postgres:18.4-alpine), same as production |
| Database (production) | PostgreSQL 18 |
| Image Compression | intervention/image ^3.11 (GD driver, instantiated directly in AttachmentsRelationManager) |
| PDF Generation | DomPDF via barryvdh/laravel-dompdf |
| Queue / Cache / Session | Redis |
| Assets | Vite 8, Tailwind CSS 4 |
| Containerisation | Docker, Docker Compose (Node 26 / PHP 8.4 images — see .nvmrc and Dockerfile) |
| CI/CD | GitHub Actions |
| Reverse Proxy | jwilder/nginx-proxy + acme-companion (Let's Encrypt) |
Local Development Setup
SparkShop's local environment is Docker-first — see the main [README](../../README.md#getting-started) for the full Docker Desktop setup (one docker compose up, no local PHP/Node/Postgres/Redis install required). That's the supported path and what CLAUDE.md assumes.
The sections below cover running natively on the host instead, for cases where Docker isn't available. This path is not part of the primary workflow — expect to keep it in sync with .env yourself.
Prerequisites
- PHP >= 8.4 with the GD extension enabled (matches the
php:8.4-apache-bookwormimage used in Docker) - Composer >= 2.7
- Node 26 (see
.nvmrc) - A local PostgreSQL 18 instance — SQLite is not supported; the schema (migrations) targets Postgres in both dev and production
#### Enable GD
XAMPP (Windows): Open php.ini and uncomment:
extension=gd
Linux:
sudo apt install -y php-gd jpegoptim optipng pngquant gifsicle webp
macOS:
brew install php jpegoptim optipng pngquant gifsicle webp
Installation
git clone https://github.com/monatemedia/sparkshop.git
cd sparkshop
composer install
npm install
cp .env.local .env # then point DB_HOST/DB_PORT/REDIS_HOST/REDIS_PORT at your local Postgres/Redis
php artisan key:generate
php artisan migrate:fresh --seed
php artisan storage:link
Running All Processes
You need four terminals running simultaneously:
# Terminal 1 — Web server
php artisan serve
Terminal 2 — Vite (hot reload)
npm run dev
Terminal 3 — Queue worker (image compression, notifications)
php artisan queue:work
Terminal 4 — Scheduler (promised date notifications)
php artisan schedule:work
Visit http://localhost:8000/admin and log in with the superuser credentials from your .env.
> [!NOTE]
> php artisan schedule:work is required here because — unlike some Docker setups — nothing in this repo's Dockerfile/docker-entrypoint.sh currently invokes schedule:run on a timer either (no cron entry, no dedicated scheduler container/service). In Docker, SendPromisedDateNotifications and the daily livewire-tmp purge (routes/console.php) are registered but not currently firing anywhere — production and staging have the same gap. Worth a follow-up: either add a cron entry to the image, or run schedule:work as its own long-lived service in docker-compose.yml.
Project Structure
app/
Console/Commands/ # Artisan commands (e.g. SendPromisedDateNotifications)
Enums/ # PHP-backed enums (JobCardStatus, EmployeeRole, etc.)
Filament/
Pages/ # Custom Filament pages (Dashboard, NotificationPreferences)
Resources/ # Filament resources (JobCardResource, CustomerResource, etc.)
Widgets/ # Dashboard widgets
Http/Controllers/ # Standard Laravel controllers (DocsController, JobCardPdfController)
Models/ # Eloquent models
Notifications/ # Laravel notification classes (5 notification types)
Observers/ # Model observers (JobCardObserver, JobCardPartObserver)
Providers/
AppServiceProvider.php # Registers observers
Filament/
AdminPanelProvider.php # Filament panel configuration
database/
migrations/ # All database migrations
seeders/ # DatabaseSeeder, NotificationPreferenceSeeder
resources/
docs/ # Public documentation markdown files
views/
welcome.blade.php # Public landing page
docs.blade.php # Public documentation page
routes/
web.php # Public routes (/, /docs, /job-cards/{id}/pdf)
Enums Reference
All enums are in app/Enums/ and implement HasLabel and HasColor for Filament integration.
| Enum | Cases |
|---|---|
JobCardStatus | Enquiry, AwaitingParts, BookedIn, InProgress, OnHoldParts, VehicleReleased, ReadyForCollection, Complete, Cancelled — not Invoiced/Paid; those live exclusively on FinancialStatus now (see CLAUDE.md) |
JobCardType | ServiceOnly, ServiceAndParts, PartsOnly |
EmployeeRole | Admin, Advisor, Technician, PartsManager |
JobCardServiceStatus | Pending, InProgress, Complete, Blocked |
JobCardPartStatus | Required, Ordered, Received, Allocated, Returned |
NotificationTrigger | JobCardAssigned, JobCardStatusChanged, PartStatusChanged, PromisedDateSoon, PromisedDateOverdue |
NotificationChannel | InApp, Email |
Since Phase 9 (Work Orders) and the financial/document workflows were added, app/Enums/ also has FinancialStatus, WorkOrderStatus, CommunicationChannel, PaymentReversalReason, VoidReason, CreditNoteReason, and Permission — not catalogued here case-by-case to avoid this table drifting out of sync again. FinancialStatus's state machine is documented in CLAUDE.md; for the rest, the enum files themselves are the source of truth.
Notifications Architecture
Notifications are triggered by two model observers and one scheduled command.
Observers
JobCardObserver— fires onupdated()whenassigned_employee_idorstatuschangesJobCardPartObserver— fires onupdated()whenstatuschanges
Both observers use wasChanged() (not isDirty()) and both are registered in AppServiceProvider.
Scheduled Command
SendPromisedDateNotifications runs daily at 08:00 and dispatches:
PromisedDateSoonNotification— for job cards with a promised date tomorrowPromisedDateOverdueNotification— for overdue job cards not yet complete
Scheduled in routes/console.php:
Schedule::command('sparkshop:promised-date-notifications')->dailyAt('08:00');
Notification Classes
All five notification classes store 'duration' => 'persistent' in their toDatabase() array. This is required — without it, Filament's inline notification renderer auto-dismisses the notification after 6 seconds and deletes the database record.
Image Compression
File attachments are handled by the AttachmentsRelationManager on JobCardResource. Images are compressed server-side using intervention/image with the GD driver (not Imagick).
There's no published config/image.php — the driver is instantiated directly where it's used:
// app/Filament/Resources/JobCards/RelationManagers/AttachmentsRelationManager.php
$manager = new ImageManager(new Driver());
Compression runs in a queued job so uploads don't block the UI. The queue worker must be running for compression to process.
Adding a Documentation Page
- Create a new markdown file in
resources/docs/, e.g.resources/docs/invoicing.md - Add the page to the
$pagesarray inapp/Http/Controllers/DocsController.php:
['slug' => 'invoicing', 'title' => 'Invoicing'],
- Add the nav link to
resources/views/docs.blade.phpin the sidebar section - The page is immediately accessible at
/docs/invoicing— no cache clearing needed
Docker Build
The Dockerfile uses a three-stage build:
- composer-builder — installs PHP dependencies (optionally with dev deps)
- node-builder — compiles frontend assets with Vite (Node 26)
- final —
php:8.4-apache-bookwormwith GD,postgresql-client, Redis extension, and upload-size tuning (upload_max_filesize/post_max_sizeset to 100M)
The ARG INSTALL_DEV_DEPENDENCIES=false build argument controls whether dev dependencies (e.g. Faker) are included:
# Production build (no dev deps)
docker compose build
Local/staging build (with dev deps for seeding)
docker compose build --build-arg INSTALL_DEV_DEPENDENCIES=true
> [!WARNING]
> There is no cron entry, no MySQL client, and no scheduler service in this image — see the note in Local Development Setup above. docker-entrypoint.sh runs migrations, cache rebuilds, and the superuser seeder on boot; it does not run schedule:run on any cadence.
GitFlow Branching Model
Branch Types
| Branch | Created from | Merges into | Auto-deploys |
|---|---|---|---|
feature/<n> | dev | dev | — |
bugfix/<n> | dev | dev | — |
release/<version> | dev | main + dev | Staging |
hotfix/<n> | main | main + dev | Production |
Feature Branch Workflow
# Start
git checkout dev && git pull origin dev
git checkout -b feature/my-feature
Finish
git checkout dev && git pull origin dev
git merge feature/my-feature
git push origin dev
git branch -d feature/my-feature
git push origin --delete feature/my-feature
Release Workflow
# Create and push (triggers staging deploy)
git checkout dev && git pull origin dev
git checkout -b release/1.0.0
git push origin release/1.0.0
Promote to production
git checkout main && git merge release/1.0.0
git push origin main
git tag -a v1.0.0 -m "Release 1.0.0"
git push origin v1.0.0
Clean up
git checkout dev && git merge release/1.0.0
git push origin dev
git branch -d release/1.0.0
git push origin --delete release/1.0.0
CI/CD Pipeline
The GitHub Actions workflow at .github/workflows/docker-publish.yml handles the full build and deploy cycle.
Triggers
| Trigger | Environment | URL |
|---|---|---|
Push to release/* | Staging | https://sparkshop.monatemedia.com |
Push v* tag | Production | https://sparkshop.co.za |
Push to main | Production | https://sparkshop.co.za |
Pipeline Steps
- Build — multi-stage Docker build, push to GHCR, tagged
:staging/:production/:v{version}/:{git-sha} - SSH — connect to VPS via SSH agent
- Migrate — staging runs a dedicated
sparkshop-initcontainer (docker-compose.staging.yml) that must complete beforesparkshop-web/sparkshop-queuestart; production runs migrations via a one-shotdocker compose run --rmindeploy-prod.sh - Deploy — start/restart
sparkshop-webandsparkshop-queue, fix storage permissions, gracefully restart the queue worker - Health check — external HTTP check against the live URL
- Release — GitHub Release created for version tags
There is no blue/green traffic swap — deploys restart the containers in place. COMPOSE_PROJECT_NAME (sparkshop-staging / sparkshop-production) namespaces containers, networks, and volumes so both environments can run on the same VPS without colliding.
Required GitHub Secrets
See the [README's Required GitHub Secrets section](../../README.md#required-github-secrets) for the full, current list (staging and production, each with SSH access, app key, superuser credentials, Postgres credentials, mail, and port secrets) — kept there as the single source of truth so this guide doesn't drift out of sync with it again.
Useful Artisan Commands
# Run migrations
php artisan migrate
Fresh migration with seed
php artisan migrate:fresh --seed
Seed notification preferences only
php artisan db:seed --class=NotificationPreferenceSeeder
Manually trigger promised date notifications
php artisan sparkshop:promised-date-notifications
Clear all caches
php artisan config:clear && php artisan route:clear && php artisan view:clear
Generate IDE helper files (if installed)
php artisan ide-helper:generate