Claimpanel guide

How it works · 08

How Claimpanel is built

Technology, modules, data, security, storage and deployment for technical readers.

For the CTO, developers and anyone reviewing the system. The source lives in Claimpanel/; build history and decisions are in Claimpanel/PROGRESS.md.

Technology

LayerChoiceNotes
Language / frameworkPHP 8.x, Laravel 13The deployment target is PHP 8.5-FPM with OPcache.
InterfaceBlade + Livewire 4 (Alpine.js ships with it), Tailwind CSS 4, ViteServer-rendered; no React or separate API for the staff screens.
DatabaseMariaDB 11.8Session pinned to +00:00; the app timezone is fixed to Europe/London.
AuthLaravel Fortify (login, reset, verification, optional 2FA)Sessions in the database, 120 minute lifetime.
Rolesspatie/laravel-permissionPermissions are an enum in code, synced to the database.
PDFsdompdfBlade views in resources/views/pdf.
FilesFlysystem with SFTP (and S3-compatible) driversStorage nodes: see below.
Queue, cache, schedulerDatabase queue by default (Redis ready), Laravel schedulerOne cron entry runs schedule:run every minute; a supervised queue worker handles email and storage jobs.
Tests & qualityPest, PHPStan, PintAbout 870 tests; PHPStan at zero errors; an architecture test enforces conventions.
What it deliberately is not

No microservices, no Node backend, no native apps, no contractor portal. It is an online-only browser application for internal staff: contractors, surveyors, policyholders and insurers have no login.

Architecture: a modular monolith

One Laravel application, organised into domains under app/Modules. Each domain owns its models, actions, Livewire components, enums, policies and support classes. Business logic lives in single-purpose Action classes (CreateClaim, RaisePurchaseOrder, CloseClaim…) that the screens call; screens do not hold business rules.

ModuleOwns
IdentityUsers, roles, permissions, invitations, 2FA, the permission enum and role seeds.
PlatformOrganisations (tenants), settings, number sequences, storage nodes and areas, email/SMS connections, bank holidays, working-day arithmetic, installer.
PartiesInsurers and contacts, policyholders (customers), properties.
ClaimsThe claim, journey, status history, notes, tasks, flags, contacts, policy, reserves and recoveries, complaints, correspondence, finance journal, dashboard and operations board.
InspectionsSurveys, risk assessments, findings, survey report PDFs and emails.
EstimatingSchedule of rates, rooms, schedule sections and lines, RFQs and awards, schedule issues and decisions.
WorkOrdersWork phases, purchase orders, service orders, contractor invoices, payments and credit notes.
ContractorsNetwork members (contractors and surveyors), trades, contacts, compliance records, onboarding, eligibility, bank details.
DocumentsFile records, upload and inspection, access policy, signed downloads, removal, off-site backup.
CommunicationsEmail templates and rendering, delivery.
AuditThe activity-recording trait and the audit log.

Key design decisions

  • The journey is a read model. ClaimJourney computes each step's state from the claim's data on every load; nothing stores "step 7 done". ClaimStatus stays the audited lifecycle.
  • Instruction events are append-only. Decisions, reviews, deliveries, RFQ returns, awards and financial entries are immutable event rows with metadata; corrections are new events.
  • Money is integer pence. A Money value object and BasisPoints (markup, VAT) avoid floating-point errors.
  • Expand/contract migrations. Schema changes are additive first, so rolling back is a re-upload of the previous code, not a database restore.
  • Stored times are UK wall-clock. Changing the timezone or session offset requires a data migration.

Data model (summary)

65 migrations create about 67 tables. The tenant tables all carry organisation_id.

AreaPrincipal tables
Tenancy & usersorganisations, users, role/permission tables, sessions, number_sequences
Partiesinsurance_companies, insurance_contacts, customers, properties
Claim coreclaims, claim_status_history, claim_instruction_events, claim_notes, claim_tasks, claim_flags, claim_contacts, claim_policies, claim_triages, claim_contact_attempts, claim_pre_commencements, claim_sales_calls, claim_correspondence, claim_complaints, claim_investigation_indicators (+ the investigation_indicators definitions)
Reserves & cashclaim_reserves (one row per change, by indemnity category), claim_recoveries (optionally filed under a category), claim_cash_payments (never edited; voided with a reason), quick_scope_items
Sales ledgersales_invoices, sales_credit_notes, sales_receipts, claim_costs, claim_fee_authorities, claim_ledger_closures
Inspectionsinspections, inspection_items, inspection_risk_assessments
Estimatingrate_items, claim_rooms, schedule_sections, schedule_lines, schedule_issues
Delivery & payableswork_phases, purchase_orders, purchase_order_lines, contractor_invoices, contractor_invoice_payments, contractor_credit_notes
Networkcontractors, contractor_contacts, contractor_documents, contractor_service_areas, trades, contractor_trade
Files & platformdocuments, storage_nodes, storage_area_routes, email_templates, bank_holidays (shared reference data), audit_logs
Frameworkjobs, job_batches, failed_jobs, cache, cache_locks, password_reset_tokens
Where the sales ledger lives

From version 0.31.0 sales invoices, credit notes, insurer receipts, assessment fees, additional costs and ledger closes each have their own table, with the amounts as whole-pence columns and database rules (for example, an invoice number is unique within an organisation, and a payment reference is unique within a claim). This replaced an earlier design that kept them as JSON on the claim's instruction events. A one-off migration copied older entries across and left the events in place. ClaimFinancials reads the tables to produce every figure, so reports by insurer or period are plain database queries.

Insurer decisions

The insurer's decision is still recorded as an event, but every question about it (does it authorise repairs, reject the claim, end the claim without repairs, allow cash payments) is answered in one place, the ClaimDecision list of fifteen outcomes.

Authentication, authorisation and tenancy

  • Tenancy. Models using the BelongsToOrganisation trait get a global scope (queries cannot see another organisation) and the organisation id is stamped on create, never mass-assigned. Another organisation's record answers 404. The structure is multi-organisation ready; the first install serves one business.
  • Roles. CTO, CEO, Staff, Read Only. Each role lists its permissions explicitly (nothing inherits). The CTO and CEO hold every permission; Gate::before grants them within their own organisation so a newly added permission never locks them out. Three pages (email, SMS, storage) check the CTO role rather than a permission. Only a CTO can manage a CTO.
  • Policies. Document access, claim access and tab visibility are policy-checked on the server; hidden menu items are a convenience, not the control.
  • Rate limiting. Failed sign-ins are counted per email and IP address together, a lockout is written to the audit log, and the two-factor challenge allows 5 attempts a minute; downloads (30/min), document uploads (20/min), exports (10/min), contact CSV (30/min) and template previews are limited.
  • Security headers. Content-Security-Policy with a nonce, X-Frame-Options: DENY, nosniff, a strict referrer policy. HSTS is sent by the web server.
  • CSRF, validation, mass-assignment. Laravel defaults; every action validates its own input and re-checks permission and organisation inside the transaction.
  • Secrets. SMTP and SMS credentials, storage private keys and bank numbers are encrypted with APP_KEY. Bank numbers are excluded from audit logging entirely.

Files and storage nodes

  • Documents are rows in documents with the claim, uploader, category, access level, original filename, MIME type, size, SHA-256, storage key and timestamps.
  • On upload the real file is inspected: size, extension against contents, MIME, and the PDF signature. Uploads are idempotent via a request token.
  • Bytes live on storage nodes: user-managed Ubuntu VPS machines reached over fingerprint-pinned SFTP, with a dedicated key-only claimfiles account. A CTO provisions a fresh VPS from Settings → Storage nodes: a queued job pins the SSH fingerprint, creates the account and verifies a write/read/delete probe. The root password is erased after the attempt.
  • Six storage areas route to nodes and folders: claim documents, photographs and evidence, inspection reports, generated documents, exports, backups. The first online node receives the defaults.
  • Downloads: policy check → 60-second signed URL → stream through the app. There are no public file URLs.
  • claimpanel:backup-files runs daily at 02:30 and copies claim files to the node routed for backups, verifying copied size.

Queues, scheduler and commands

WhatDetail
Queued jobsEmail delivery (surveys, reports, schedules, RFQs, purchase orders), storage-node provisioning. Jobs are tenant-aware: they carry the organisation and restore it when run.
Schedulerclaimpanel:backup-files daily 02:30; claimpanel:refresh-bank-holidays Mondays 03:15.
Artisan commandsclaimpanel:install, claimpanel:sync-permissions (run after a release that adds permissions), claimpanel:refresh-bank-holidays, claimpanel:backup-files, plus sample-data, rate-import, trade-seed and reset commands for rehearsal use.
Restart workers after a release

Queue workers keep the old code in memory. Restart them after every deploy that changes jobs or mail.

PWA and offline use

The project direction is an installable web app for internal staff, but it is deliberately online-only: no service-worker caching of claim data and no offline queue, because claim data is sensitive and there is no field-user login. At this version no web-app manifest or service worker is shipped, so the panel is used as an ordinary website; adding the manifest is a small future task.

Environments and deployment

EnvironmentPurposeNotes
Local PCDevelopment, testsLaravel Herd (PHP, Composer), MariaDB, Redis-compatible store; Claude Code and Git.
RehearsalTrial deployments with sample data onlySeparate server running HestiaCP. Never holds real claims.
ProductionReal claimsA separate, fresh install on a stronger VPS running WebDeck, nginx and PHP-FPM, MariaDB, Redis, supervised workers, scheduler cron.

Release procedure (summary)

  1. Verify locally

    Pint, PHPStan and the full Pest suite must pass.

  2. Build assets

    npm ci and npm run build produce public/build. Node does not run in production.

  3. Package

    tools/package-release.ps1 builds the release folder (production Composer install, built assets) and never touches a server. VERSION is the SemVer source of truth.

  4. Back up

    Source and database backups are taken before any upload.

  5. Upload

    By FTP/SFTP. Never upload .env, .git or node_modules. The production .env is created on the server.

  6. Migrate and cache

    php artisan migrate --force, claimpanel:sync-permissions, then rebuild config, route and view caches (php artisan optimize).

  7. Reload

    Reload PHP-FPM (OPcache otherwise serves old code) and restart queue workers.

  8. Check

    Sign in; open the dashboard and a claim; confirm worker and scheduler are running. tools/verify-release.php assists.

Never

Never restore a rehearsal database into production, never put real claim data on rehearsal, and never deploy without a fresh backup. Full detail and the server-specific commands are in Claimpanel/DEPLOY.md and LOCAL-SETUP.md.

Quality controls

  • An accessibility pass to WCAG 2.2 AA: skip link, keyboard-safe menus and dialogs, announced validation messages, measured colour contrast, reduced-motion respect. Tests lock these in.
  • Tests cover permission and organisation boundaries on each feature, refusal paths, and (for finance) the arithmetic of credits, payments and margin.
  • An architecture test enforces module conventions.