Appearance
Architecture
Rangefront Waste is a modular monolith with separate web and mobile clients. It's sold as a one-off licence with source code, self-hosted or hosted by Rangefront Labs, and white-labelled per business.
Parts
| Folder | What it is |
|---|---|
apps/backend-laravel | Laravel 13 API (PHP 8.3 or newer). Tenancy, queues, reports, PDFs, billing, council reporting, sync endpoints |
apps/web-aurelia | Aurelia 2 and Vite operations console, plus the Admin area and the public customer pages (pay, quote, tracking, portal) |
apps/mobile-expo | Expo SDK 56 offline-first field app. Read its AGENTS.md first |
shared-contracts | The API envelope schema, the generated OpenAPI file and generated TypeScript types |
infra/local-dev | Docker for Postgres/PostGIS, Redis, S3 (SeaweedFS) and Mailpit |
npm workspaces cover the web and mobile apps. The Laravel app is driven with Composer, through root npm scripts.
Runtime
text
Aurelia console -> Laravel API -> PostgreSQL/PostGIS (MySQL 8 also works)
Expo field app -> Laravel API -> same database
Laravel queues -> Redis, run by Horizon
Laravel broadcast -> Reverb (websockets)
Files, PDFs, photos -> S3-compatible object storage
Field data on the phone -> Expo SQLite and an outboxThe console and the API are served from the same address. APP_URL has to be that address, because pay links, tracking links, quote links and portal links are built from it and open pages of the console.
Tenancy
Tenancy is shared-schema:
- Every operational table carries
organization_id. - Requests under
/api/v1need anX-Organization-Idheader. TheResolveOrganizationmiddleware checks the signed-in user has an active membership. - That middleware is context, not the security model. Policies, the
permission:middleware and service actions still check permissions, and every query is scoped by organization. - A user can belong to several organizations, each with its own role.
- Public links (pay, quote, tracking, portal) use hashed random tokens, not ids.
Backend layout
text
app/Http/Controllers/Api/V1 one controller per resource or action
app/Http/Requests/Api/V1 Form Requests, one per mutation
app/Http/Middleware ResolveOrganization, RequirePermission, DemoReadOnly
app/Policies TenantResourcePolicy maps resources to permissions
app/Services the business logic: workflows, billing, messaging, PDFs
app/Services/Ai, PhoneAgent Claude gateway and the phone agent
app/Services/Messaging email and SMS provider catalogue and senders
app/Services/Routing route optimisation providers
app/Jobs queued work
app/Events broadcast events
app/Extensibility hooks, plugins and themes
app/Support/PermissionCatalog the permission keys
routes/api.php the API (plus api-team.php and extensibility.php)
routes/console.php artisan commands and the scheduleMost simple resources go through TenantTableController, which scopes reads and writes by organization, paginates, soft-deletes and writes audit records. Workflow actions (job status, report issue, invoice send, payment) go through services so rules are in one place and every path audits.
Workflows to know:
- Jobs move through
JobWorkflowService. Invalid moves are refused. After a move the service fires customer messages, webhooks, time entries and report issue. - Compliance reports move draft, generated, issued, submitted (or superseded). PDFs are immutable once issued. Generic
PATCHrefuses workflow-owned fields. - Invoices are edited only while draft. Sending, paying, PDFs and accounting sync are workflow services.
- Messages are dispatched from rules by
CustomerMessageService, with a dedupe key so each goes once.
Queues and the scheduler
Jobs run on named queues: critical, reports, routing, uploads, notifications, council, imports, default, plus documents (PDFs), integrations and webhooks. See install.
The scheduler runs every minute and starts the commands listed in install.
Realtime
Reverb broadcasts organization-scoped events (job changes, notifications, team messages, monitoring) on private channels. The console listens through Laravel Echo. Channel access is checked against organization membership, and the team channel also needs team.read.
Files
Uploads go directly to object storage with a signed address. Object keys are scoped under organizations/{organization_id}/.... Each upload is hashed and scanned (a built-in check, or ClamAV) before it's marked ready. Generated PDFs are stored the same way with their hashes.
PDFs
PDFs come from HTML templates rendered through headless Chromium in production (PDF_RENDERER, PDF_CHROMIUM_BINARY), queued as render jobs with retries. Themes and tenant templates are Twig; see themes and document templates.
Field app
The Expo app keeps routes, jobs, forms, photos and time in SQLite and queues changes in an outbox. It pulls and pushes through /mobile/sync/pull and /mobile/sync/push, with conflict review on the device. Devices are enrolled and can be revoked from the console.
Extensibility
Hooks, plugins and themes let a developer change behaviour without editing core, so upgrades don't overwrite the work. See extending, themes and plugin fields.
Contracts
The OpenAPI file is generated from the Laravel routes (npm run contracts:openapi) and TypeScript types from it (npm run contracts:types, or both with npm run contracts:generate). Commit the result.
UI direction
The console follows the office app built for a single operator, without its branding: a deep petrol frame around one white work surface, compact page headers, flat ruled directory rows, right-hand record sheets, and a schedule where work stream is colour and dispatch state is shape. It stays white-label: the frame shows the business's trading name beside the Rangefront mark. Avoid generic SaaS hero layouts, nested cards, decorative shadows and blue-purple gradients. AGENTS.md lists the style vocabulary.