Back to Home

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

LayerTechnology
BackendLaravel 13, PHP 8.4
Admin PanelFilament 5
Database (dev)PostgreSQL 18 (postgres:18.4-alpine), same as production
Database (production)PostgreSQL 18
Image Compressionintervention/image ^3.11 (GD driver, instantiated directly in AttachmentsRelationManager)
PDF GenerationDomPDF via barryvdh/laravel-dompdf
Queue / Cache / SessionRedis
AssetsVite 8, Tailwind CSS 4
ContainerisationDocker, Docker Compose (Node 26 / PHP 8.4 images — see .nvmrc and Dockerfile)
CI/CDGitHub Actions
Reverse Proxyjwilder/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-bookworm image 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.

EnumCases
JobCardStatusEnquiry, AwaitingParts, BookedIn, InProgress, OnHoldParts, VehicleReleased, ReadyForCollection, Complete, Cancelled — not Invoiced/Paid; those live exclusively on FinancialStatus now (see CLAUDE.md)
JobCardTypeServiceOnly, ServiceAndParts, PartsOnly
EmployeeRoleAdmin, Advisor, Technician, PartsManager
JobCardServiceStatusPending, InProgress, Complete, Blocked
JobCardPartStatusRequired, Ordered, Received, Allocated, Returned
NotificationTriggerJobCardAssigned, JobCardStatusChanged, PartStatusChanged, PromisedDateSoon, PromisedDateOverdue
NotificationChannelInApp, 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 on updated() when assigned_employee_id or status changes
  • JobCardPartObserver — fires on updated() when status changes

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 tomorrow
  • PromisedDateOverdueNotification — 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

  1. Create a new markdown file in resources/docs/, e.g. resources/docs/invoicing.md
  2. Add the page to the $pages array in app/Http/Controllers/DocsController.php:
   ['slug' => 'invoicing', 'title' => 'Invoicing'],

  1. Add the nav link to resources/views/docs.blade.php in the sidebar section
  2. The page is immediately accessible at /docs/invoicing — no cache clearing needed

Docker Build

The Dockerfile uses a three-stage build:

  1. composer-builder — installs PHP dependencies (optionally with dev deps)
  2. node-builder — compiles frontend assets with Vite (Node 26)
  3. final — php:8.4-apache-bookworm with GD, postgresql-client, Redis extension, and upload-size tuning (upload_max_filesize/post_max_size set 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

BranchCreated fromMerges intoAuto-deploys
feature/<n>devdev—
bugfix/<n>devdev—
release/<version>devmain + devStaging
hotfix/<n>mainmain + devProduction

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

TriggerEnvironmentURL
Push to release/*Staginghttps://sparkshop.monatemedia.com
Push v* tagProductionhttps://sparkshop.co.za
Push to mainProductionhttps://sparkshop.co.za

Pipeline Steps

  1. Build — multi-stage Docker build, push to GHCR, tagged :staging/:production/:v{version}/:{git-sha}
  2. SSH — connect to VPS via SSH agent
  3. Migrate — staging runs a dedicated sparkshop-init container (docker-compose.staging.yml) that must complete before sparkshop-web/sparkshop-queue start; production runs migrations via a one-shot docker compose run --rm in deploy-prod.sh
  4. Deploy — start/restart sparkshop-web and sparkshop-queue, fix storage permissions, gracefully restart the queue worker
  5. Health check — external HTTP check against the live URL
  6. 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