ParkHub is a self-hosted parking management system. This is the PHP edition, built on Laravel 13 with a React 19 / Astro 6 frontend served as static assets from Apache.
A feature-equivalent Rust edition (Axum + redb) exists in a sibling
repository. Both backends expose the same /api/v1/* REST surface and share the
identical parkhub-web frontend, making them fully interchangeable. Cross-runtime
ownership and release discipline are tracked in docs/parity-governance.md and
docs/release-checklist.md.
parkhub-php/
├── composer.json # Laravel 13, Sanctum, web-push
├── Dockerfile # Multi-stage: frontend build -> PHP 8.4 + Apache
├── docker-compose.yml # App + MySQL + worker + scheduler
├── render.yaml # Render free-tier deployment manifest
│
├── app/
│ ├── Console/Commands/ # Artisan commands
│ │ ├── AutoReleaseBookings.php # Scheduled: release expired bookings
│ │ ├── CreateAdminUser.php # Initial admin creation
│ │ ├── GenerateVapidKeys.php # VAPID key generation for Web Push
│ │ └── RefillMonthlyCredits.php # Monthly credit quota refill
│ ├── Http/
│ │ ├── Controllers/Api/ # API controllers — `BookingController` split into 5 focused controllers
│ │ │ ├── AuthController.php # Login, register, forgot/reset password (delegates to AuthenticationService)
│ │ │ ├── BookingController.php # CRUD + quick-book (1035 → 640 LOC after split)
│ │ │ ├── BookingCalendarController.php # Calendar view + iCal (split from BookingController)
│ │ │ ├── BookingCheckInController.php # Check-in / check-out (split from BookingController)
│ │ │ ├── BookingInvoiceController.php # PDF invoice generation (split from BookingController)
│ │ │ ├── BookingSwapController.php # Swap request lifecycle (split from BookingController)
│ │ │ ├── GuestBookingController.php # Guest bookings without user accounts
│ │ │ ├── MobileBookingController.php # Mobile-optimized booking endpoints
│ │ │ ├── AdminController.php # User/booking management, audit log
│ │ │ ├── AdminSettingsController.php # Settings, branding, privacy, email
│ │ │ ├── AdminReportController.php # Stats, heatmap, CSV export
│ │ │ ├── AdminCreditController.php # Credit grants, refills, transactions
│ │ │ ├── AdminAnnouncementController.php # Announcement CRUD
│ │ │ ├── AdminModulesController.php # Runtime toggle + JSON Schema config editor
│ │ │ ├── LotController.php # Parking lot CRUD + occupancy
│ │ │ ├── SlotController.php # Slot CRUD within lots
│ │ │ ├── ZoneController.php # Zone management
│ │ │ ├── UserController.php # Profile, prefs, notifications, export
│ │ │ ├── VehicleController.php # Vehicle CRUD + photo upload
│ │ │ ├── AbsenceController.php # Absence CRUD + iCal import
│ │ │ ├── RecurringBookingController.php # Recurring booking management
│ │ │ ├── WaitlistController.php # Waitlist CRUD
│ │ │ ├── TeamController.php # Team directory + today view
│ │ │ ├── MiscController.php # Push, email, QR, webhooks
│ │ │ ├── DemoController.php # Demo mode: status, vote-reset
│ │ │ ├── SetupController.php # First-run setup wizard
│ │ │ ├── HealthController.php # Health + readiness checks
│ │ │ ├── MetricsController.php # Prometheus metrics
│ │ │ ├── PublicController.php # Public occupancy display
│ │ │ └── SystemController.php # System info
│ │ ├── Middleware/
│ │ │ ├── ApiResponseWrapper.php # Wraps responses in { success, data } envelope
│ │ │ ├── ForceJsonResponse.php # Forces Accept: application/json
│ │ │ ├── RequireAdmin.php # Admin role gate
│ │ │ ├── ModuleGate.php # Returns 404 MODULE_DISABLED for runtime-disabled modules
│ │ │ └── SecurityHeaders.php # CSP, HSTS, X-Frame, etc.
│ │ └── Resources/ # API Resource transformers
│ ├── Jobs/ # Queue jobs (push notifications, etc.)
│ ├── Mail/ # Mailable classes
│ ├── Models/ # Eloquent models
│ ├── Policies/ # Laravel Policies — 11 total (up from 3): Absence, Announcement, AuditLog, Booking, Favorite, Notification, ParkingLot, Tenant, Vehicle, Webhook, Widget
│ ├── Providers/ # Service providers (ModuleServiceProvider binds runtime-toggle bus)
│ └── Services/ # Business-logic services — 12 focused services extracted across 6 passes:
│ # Authentication/AuthenticationService.php
│ # Booking/BookingCreationService.php
│ # Stripe/StripeWebhookService.php
│ # Vehicle/VehicleService.php
│ # Admin/AdminSettingsService.php
│ # Admin/AdminUserManagementService.php
│ # Audit/AuditLogQueryService.php
│ # Compliance/ComplianceService.php
│ # Modules/ModuleConfigurationService.php
│ # User/UserAccountService.php
│ # Webhook/WebhookDispatchService.php
│ # Reports/ReportExportService.php
│ # plus ModuleRegistry, PushNotificationService, CircuitBreaker
│
├── routes/
│ ├── api.php # Laravel-style API routes (108 endpoints)
│ ├── api_v1.php # Rust-compatible /api/v1/* routes (mirrored surface)
│ ├── web.php # SPA fallback route
│ └── console.php # Scheduled commands
│
├── database/
│ ├── migrations/ # 13 migration files
│ │ ├── *_create_users_table.php
│ │ ├── *_create_parkhub_tables.php # Core: lots, slots, bookings, vehicles, etc.
│ │ ├── *_create_waitlist_entries_table.php
│ │ ├── *_add_missing_indexes.php
│ │ ├── *_add_credits_system.php
│ │ ├── *_create_swap_requests_table.php
│ │ └── *_add_slot_features_*.php # EV, handicap, pricing
│ ├── factories/ # Model factories for testing
│ └── seeders/ # Database seeders
│
├── parkhub-web/ # Shared React frontend (identical to Rust repo)
│ ├── astro.config.mjs # Static output, React compiler, Tailwind, chunk splitting
│ ├── package.json # shared frontend package metadata (currently v4.13.0)
│ ├── public/ # PWA manifest, service worker, icons
│ ├── e2e/ # 14 Playwright E2E test specs
│ └── src/ # React app (see Frontend Architecture)
│
├── resources/js/ # Legacy Vite-based frontend (backup)
│ └── src/ # Older React app structure
│
├── config/ # Laravel config files
├── legal/ # German legal document templates (GDPR)
├── docs/ # Documentation + screenshots
├── tests/
│ ├── Feature/ # 130 PHPUnit feature test files, 1,320 test methods
│ └── Unit/ # 84 unit test files, 434 test methods — coverage of the extracted services, policies, and registries
└── .github/workflows/ # CI, Docker publish, mutants, schemathesis, lighthouse
Client Request
│
▼
┌──────────────────────────────────────────────────┐
│ Apache + mod_rewrite → public/index.php │
│ │
│ Laravel Middleware Pipeline │
│ ┌─────────────────────────────────────────────┐ │
│ │ ForceJsonResponse (Accept: application/json) │ │
│ │ SecurityHeaders (CSP, HSTS, X-Frame, etc.) │ │
│ │ ApiResponseWrapper ({ success, data } env.) │ │
│ │ ThrottleRequests (rate limiting) │ │
│ │ ├── auth: per-IP login/register limits │ │
│ │ ├── password-reset: 3/15min/IP │ │
│ │ └── setup: tight mutation limits │ │
│ └─────────────────────────────────────────────┘ │
│ │
│ Sanctum Token Authentication (Bearer) │
│ RequireAdmin Middleware (admin routes only) │
│ │
│ Route → Controller Method │
│ │ │
│ ▼ │
│ Request Validation ($request->validate()) │
│ │ │
│ ▼ │
│ Business Logic (Controller / Service) │
│ │ │
│ ▼ │
│ Eloquent ORM → Database (SQLite or MySQL) │
│ │ │
│ ▼ │
│ API Resource Transform → JSON Response │
└──────────────────────────────────────────────────┘
- Laravel Sanctum for API token authentication
- Bearer tokens in
Authorizationheader - Passwords hashed with bcrypt (12 rounds)
- Token expiry configurable per-session
- Personal access tokens stored in
personal_access_tokenstable
Responses wrapped by ApiResponseWrapper middleware into a consistent envelope:
Success:
{ "success": true, "data": { ... } }Error:
{ "success": false, "error": { "code": "NOT_FOUND", "message": "..." } }Laravel's exception handler maps common exceptions to appropriate HTTP codes. Validation errors return field-level detail.
| Middleware | Purpose |
|---|---|
ForceJsonResponse |
Ensures all API responses are JSON |
SecurityHeaders |
CSP, HSTS, X-Content-Type, X-Frame-Options, etc. |
ApiResponseWrapper |
Wraps responses in { success, data } envelope |
RequireAdmin |
Gates admin endpoints to admin/superadmin roles |
throttle:auth |
Rate limits auth endpoints per IP |
The parkhub-web/ directory contains the shared frontend -- identical code
powers both the Rust and PHP editions. Build scripts target each backend:
npm run build:php-- builds and copies topublic/npm run build:rust-- builds and copies to Rust's embedded assets
| Layer | Technology |
|---|---|
| Meta-framework | Astro 6 (static output mode) |
| UI framework | React 19 with React Compiler |
| Styling | Tailwind CSS v4 |
| Animations | Framer Motion |
| State | Zustand (stores) + React Context (auth/theme/features) |
| Routing | React Router v7 (lazy-loaded routes) |
| i18n | i18next + browser language detection (10 locales) |
| Icons | Phosphor Icons |
| HTTP client | Typed fetch wrapper (api/client.ts) |
| Testing | Vitest (32 unit test files) + Playwright (14 E2E specs) |
All pages are lazy-loaded via React.lazy(). Auth-required pages wrapped in
ProtectedRoute; admin pages additionally wrapped in AdminRoute:
/welcome-> Welcome/language selector/login,/register,/forgot-password-> Auth pages/-> Dashboard/book,/bookings,/calendar-> Booking management/vehicles,/absences,/credits-> User resources/profile,/team,/notifications-> User pages/admin/*-> Admin panel (settings, users, lots, reports, announcements)
- Three modes:
light,dark,system - Persisted in
localStorageasparkhub_theme - OS preference tracked via
useSyncExternalStore+matchMedia - Use-case theming via
data-usecaseattribute on<html>
Manual chunks in Astro/Vite config:
vendor-react(react, react-dom, react-router)vendor-motion(framer-motion)vendor-i18n(i18next stack)
SQLite for development and single-node deployments (Render free tier).
MySQL 8 supported for production via docker-compose.yml.
users
├── id (uuid, PK)
├── username (unique)
├── email (unique)
├── password (bcrypt hash)
├── role: user | premium | admin | superadmin
├── preferences (JSON)
├── is_active, department, phone, picture
├── credits_balance, credits_monthly_quota
└── last_login, created_at, updated_at
parking_lots
├── id (uuid, PK)
├── name, address
├── total_slots, available_slots
├── layout (JSON)
├── status: open | closed | maintenance
├── pricing_type, base_price_per_hour, currency
└── created_at, updated_at
parking_slots
├── id (uuid, PK)
├── lot_id (FK → parking_lots)
├── zone_id (FK → zones, nullable)
├── slot_number, status
├── reserved_for_department
├── is_ev_charging, is_handicap, is_covered
└── created_at, updated_at
bookings
├── id (uuid, PK)
├── user_id (FK → users)
├── lot_id (FK → parking_lots)
├── slot_id (FK → parking_slots)
├── booking_type, lot_name, slot_number
├── vehicle_plate
├── start_time, end_time
├── status: confirmed | cancelled | completed | no_show
├── notes, recurrence (JSON)
├── checked_in_at
├── base_amount, tax_amount, total_amount, payment_status
└── created_at, updated_at
INDEX(slot_id, start_time, end_time)
INDEX(user_id, status)
vehicles
├── id (uuid, PK)
├── user_id (FK → users)
├── plate, make, model, color
├── type: car | motorcycle | electric | suv
├── is_default, photo_url
└── created_at, updated_at
zones → lot_id, name, color, description
absences → user_id, type (homeoffice/vacation/sick/training), dates, source
recurring_bookings → user_id, lot_id, slot_id, days_of_week (JSON), times
guest_bookings → created_by, guest_name, guest_code (unique), dates
swap_requests → requester_id, target_booking_id, status
waitlist_entries → user_id, lot_id, preferred_date, slot_type
credit_transactions → user_id, amount, type (monthly_refill/admin_grant/booking_debit)
favorites → user_id, slot_id (unique pair)
notifications → user_id, type, title, message, read
announcements → title, message, severity, active, expires_at
push_subscriptions → user_id, endpoint, p256dh, auth (VAPID)
webhooks → url, events (JSON), secret, active
booking_notes → booking_id, user_id, note
audit_log → user_id, action, details (JSON), ip_address
settings → key-value store (setup_completed, branding, etc.)
The PHP edition maintains two route files for full compatibility:
routes/api.php-- Laravel-idiomatic routes (e.g.,POST /api/login)routes/api_v1.php-- Rust-compatible routes prefixed with/api/v1/*
Both files map to the same controllers. The v1 routes ensure the shared frontend works identically against either backend.
| Group | Prefix | Auth | Endpoints |
|---|---|---|---|
| Health | /api/v1/health/* |
None | live, ready |
| Auth | /api/v1/auth/* |
None* | login, register, forgot, reset |
| Setup | /api/v1/setup/* |
None | status, complete, change-password |
| Lots | /api/v1/lots/* |
Sanctum | CRUD + slots, zones, occupancy |
| Bookings | /api/v1/bookings/* |
Sanctum | CRUD + quick-book, guest, swap |
| Vehicles | /api/v1/vehicles/* |
Sanctum | CRUD + photo upload |
| Users | /api/v1/users/me |
Sanctum | Profile, prefs, password, export |
| Credits | /api/v1/credits/* |
Sanctum | Balance, history |
| Admin | /api/v1/admin/* |
Admin | Users, bookings, reports, settings |
| Demo | /api/v1/demo/* |
None | Status, vote-reset, heartbeat |
| Metrics | /api/v1/metrics |
None | Prometheus format |
| Public | /api/v1/public/* |
None | Occupancy display |
| Theme | /api/v1/theme |
None | Use-case CSS theme config |
Eloquent models are transformed via Laravel API Resources before serialization:
BookingResource, ParkingLotResource, ParkingSlotResource,
NotificationResource, FavoriteResource, VehicleResource, etc.
| Type | Count | Framework | Location |
|---|---|---|---|
| Feature tests | 1,320 | PHPUnit | tests/Feature/*.php (130 files) |
| Unit tests | 434 | PHPUnit | tests/Unit/*.php (84 files) |
| Mutation | — | infection-php |
Nightly (.github/workflows/mutants.yml) |
| Contract fuzz | — | schemathesis |
Nightly — runs against docs/openapi/php.json |
Feature tests cover the full HTTP surface: auth flows, booking CRUD, admin operations, credit system, edge cases, GDPR compliance, webhooks, module runtime toggle + JSON Schema config editor, tenant scope, etc. Unit tests cover the 12 extracted services, the 11 policies, and the module registry/gate.
Run with: php artisan test or composer test
| Type | Count | Framework | Location |
|---|---|---|---|
| Unit/component | 32 | Vitest + Testing Library | src/**/*.test.{ts,tsx} |
| E2E | 14 | Playwright | e2e/*.spec.ts |
10 integration test suites covering cross-module interactions:
- Auth flow end-to-end (login, refresh, session management)
- Booking lifecycle (create, modify, cancel, check-in, check-out)
- Admin CRUD operations (lots, slots, zones, announcements)
- Concurrent booking conflict detection
- Credit system integration (deduction, refill, history)
- Notification delivery pipeline
- Webhook delivery and retry logic
- GDPR data export and erasure
- Module toggle validation (enabled/disabled states)
- Security boundary enforcement (RBAC, rate limiting)
A 1-month booking cycle simulation engine with 3 configurable profiles:
| Profile | VUs | Duration | Scenario |
|---|---|---|---|
| Small office | 10 | 30 days | Single lot, standard hours |
| Campus | 50 | 30 days | Multiple lots, recurring bookings, guest passes |
| Enterprise | 200 | 30 days | Multi-tenant, dynamic pricing, high concurrency |
The simulation creates realistic booking patterns including peak hours, cancellations,
no-shows, and waitlist activity. Run via php artisan test --filter=Simulation or the
E2E full-workflow spec.
Performance testing scripts with k6 live in tests/load/:
| Script | Profile | Description |
|---|---|---|
smoke.js |
1 VU, 30s | Quick sanity check |
load.js |
50 VUs, 5min | Sustained load baseline |
stress.js |
100 VUs, 10min | All endpoints stress test |
spike.js |
1 -> 200 -> 1 VUs | Sudden surge test |
+----------+
| 29 E2E | Playwright (browser + API)
| specs |
++---------++
| 10 Integ. | Cross-module API tests
| suites |
++-----------++
| 1,320 Feature | PHPUnit HTTP surface
+----------------+
| 434 Unit | PHPUnit services + policies
+----------------+
| k6 Load Tests | smoke / load / stress / spike
+----------------+
| Mutation + | infection-php + schemathesis (nightly)
| contract fuzz |
+----------------+
Multi-stage Dockerfile:
- Frontend build -- Node 22 slim,
npm ci && npm run build - PHP runtime -- PHP 8.4 + Apache on Debian Bookworm
- Extensions: pdo_sqlite, pdo_mysql, gd, zip, bcmath
- Composer 2 with optimized autoloader
- Non-root Apache user, security headers hardened
docker-compose.yml provides four services:
| Service | Role | Memory |
|---|---|---|
app |
PHP + Apache, serves API + frontend | 512 MB |
db |
MySQL 8 (not exposed to host) | 512 MB |
worker |
queue:work for async jobs |
256 MB |
scheduler |
schedule:run loop (60s interval) |
128 MB |
Pre-built GHCR image deployed to Render free tier:
- SQLite database (ephemeral on free tier)
DEMO_MODE=truefor public demo- Auto-configured admin credentials
Activated via DEMO_MODE=true:
- Collaborative vote-to-reset (3 votes triggers data reset)
- Auto-reset on schedule
- Demo overlay in frontend showing countdown + viewer count
- Pre-seeded with realistic parking data
Defined in routes/console.php:
| Command | Schedule | Purpose |
|---|---|---|
AutoReleaseBookings |
Every 15 min | Release expired/no-show bookings |
RefillMonthlyCredits |
Monthly | Reset user credit quotas |
| Demo auto-reset | Every 6h | Reset demo data (if enabled) |
Full PWA manifest (manifest.json) with standalone display, maskable icons,
shortcuts (Quick Book, My Bookings, Calendar), and screenshots.
Custom sw.js with three caching strategies:
- Static assets -- Cache-first, version-keyed (purged on deploy)
- API reads -- Stale-while-revalidate (24h max age) for bookings, lots, etc.
- Offline mutations -- Queued in IndexedDB, replayed via Background Sync
Precached URLs: /, /favicon.svg, /offline.html, icon assets.
- Fallback to
/offline.htmlwhen network unavailable - Cached API data enables read-only browsing offline
- POST/PUT/DELETE requests queued and synced when connectivity returns
PHP (Laravel) provides rapid development, a massive hosting ecosystem, and is familiar to most web developers. Rust provides maximum performance and a single binary deployment. Both share the same API contract so the React frontend is backend-agnostic.
Zero-configuration setup: no external database server needed. Perfect for
single-node deployments and free-tier hosting (Render). MySQL is supported for
production multi-instance setups via docker-compose.yml.
Astro's static output mode generates pre-rendered HTML + JS bundles that are
served directly by Apache (PHP) or embedded in the Rust binary. No SSR server
needed. The same parkhub-web/ directory is shared between both repos with
dedicated build scripts (build:php, build:rust).
Sanctum is lighter weight for SPA token auth. No OAuth server complexity needed -- ParkHub is a self-hosted tool, not a platform with third-party API consumers.
routes/api.php follows Laravel conventions (shorter paths, grouped middleware).
routes/api_v1.php mirrors the Rust backend's exact endpoint structure so the
shared React frontend can hit /api/v1/* paths without any backend detection.
Both files route to the same controller methods.