Appearance
Install
This page is for the developer or IT person who installs and runs Rangefront Waste. It covers requirements, every setting in .env.example, the queue worker, the scheduler, storage and mail. For shipping to a server, see deploy.
Requirements
| Need | Notes |
|---|---|
| PHP 8.3 or newer | Laravel 13. Needs the usual Laravel extensions plus a database driver (pdo_pgsql or pdo_mysql) and sodium (used to verify Telnyx reply signatures). The demo runs PHP 8.4 |
| Composer | For apps/backend-laravel |
| Database | PostgreSQL (the dev stack uses the PostGIS image) or MySQL 8. Tests run on SQLite |
| Redis | Queues, cache and Horizon. The predis client is bundled. Use REDIS_CLIENT=predis if phpredis isn't installed |
| S3-compatible storage | AWS S3, MinIO or similar, for photos, signatures and PDFs |
| Headless Chromium | Renders PDFs in production. Set PDF_CHROMIUM_BINARY |
| A process manager | systemd or Supervisor, to keep Horizon and Reverb running |
| Cron | To run the scheduler every minute |
| Node 20.19 or newer, on a build machine | Builds the console and the field app (Vite 7). The server does not need Node |
For local development, docker compose -f infra/local-dev/docker-compose.yml up -d starts Postgres with PostGIS (port 54329), Redis (port 63799), SeaweedFS as S3 (port 9000, bucket rangefront-waste, keys rangefront / rangefront-secret, created by the s3-bucket one-shot) and Mailpit (SMTP 1025, web 8025). Postgres 18 stores its cluster under /var/lib/postgresql, so the volume mounts there. The Postgres image is amd64-only and runs under emulation on Apple Silicon. If another local service already holds 127.0.0.1:9000 (a PHP-FPM, for example), use AWS_ENDPOINT=http://localhost:9000, which Docker still serves.
Install steps
bash
cd apps/backend-laravel
composer install --no-dev --optimize-autoloader # drop --no-dev for development
cp .env.example .env
php artisan key:generate
# edit .env (see below)
php artisan migrate --force
php artisan db:seed --class=CompliancePackSeeder # state and territory rule packsKeep the generated APP_KEY safe. Credentials stored for payments, SMS, email and integrations are encrypted with it, and can't be recovered without it.
Create the first business
bash
php artisan rangefront:tenant:create "Acme Pumping" acme-pumping owner@acme.example --timezone=Australia/BrisbaneThis creates the business, a Main depot, the four default roles (Owner, Dispatcher, Service Agent, Accounts, with the Owner holding every permission), the commissioning certificate form and the standard customer messages (rules off), then emails the owner an invitation. The owner opens the link, chooses a password and signs in. If mail isn't set up yet the command prints the invitation link so you can pass it on. Set CONSOLE_URL (or APP_URL) first, because the link is built from it.
Other commands:
| Command | Does |
|---|---|
rangefront:tenant:list | Lists businesses with status and how many people can sign in |
rangefront:tenant:suspend <org> [--reason=] | Switches a business off. Its people get a clear "business is suspended" message in the console and the field app; nothing is deleted |
rangefront:tenant:resume <org> | Switches it back on |
<org> is the id or the slug. Each is written to the audit log. rangefront:install-defaults <org> [--enable-messages] is still there for a business made another way, and is safe to run again.
php artisan db:seed creates two demo businesses with fake data, for demos and development only.
Build the clients
bash
npm install
npm run web:build # apps/web-aurelia/distServe dist as the site root, with every unknown path falling back to index.html, and send /api, /broadcasting, /sanctum, /storage, /up and /book to Laravel's public/index.php. The websocket path /app/ goes to Reverb. Web build variables:
| Variable | Meaning |
|---|---|
VITE_RANGEFRONT_API_BASE_URL | API base. Defaults to /api/v1/, which suits a single-host setup |
VITE_MAP_TILE_URL | Map tile address. Defaults to OpenStreetMap |
VITE_MAP_TILE_ATTRIBUTION | Attribution text for the tiles |
VITE_FIELD_APP_URL | Adds a "Field app" link to the console |
VITE_FORCE_DEMO_LOGIN, VITE_ENABLE_DEMO_LOGIN, VITE_DEMO_EMAIL, VITE_DEMO_PASSWORD | Auto sign-in with a fixed account, for the public demo only. Without it the console shows its sign-in page. npm run web also auto signs in; set VITE_ENABLE_DEMO_LOGIN=false to see the sign-in page in development. Never bake a real password into a public build |
The field app is built with Expo. Its public variables are EXPO_PUBLIC_RANGEFRONT_API_URL (the server email sign-in goes to; leave it empty for a build that only links with codes) and, for demos, EXPO_PUBLIC_RANGEFRONT_DEMO_*. See apps/mobile-expo/AGENTS.md.
Phones on your own server don't need their own build. The store app links to any server with a code from Admin > Work phones. The code's QR holds your APP_URL plus /api/v1, so APP_URL must be the public https address phones reach. A release build refuses a server that isn't https.
Run the processes
| Process | Command | Keep running with |
|---|---|---|
| Web and API | php-fpm behind nginx, or php artisan serve for development | nginx and php-fpm |
| Queue worker | php artisan horizon | systemd or Supervisor |
| Websockets | php artisan reverb:start --host=127.0.0.1 --port=8080 | systemd or Supervisor |
| Scheduler | * * * * * cd /path/to/apps/backend-laravel && php artisan schedule:run >> /dev/null 2>&1 | cron |
Horizon starts three supervisors that between them run every queue (see Queues). A plain queue:work is not enough, because it only listens to the queues you name.
Restart Horizon after every deploy (php artisan horizon:terminate, or restart the unit) so workers load the new code.
The Horizon dashboard (/horizon) is closed to everyone outside local development. To open it, put the emails of your platform administrators (people who run the installation, not a customer's staff) in PLATFORM_ADMIN_EMAILS, comma separated, and sign in to the Horizon page itself with one of those accounts' console email and password (the browser shows a basic-auth prompt; use HTTPS). Leave it empty to keep Horizon closed. The setting is read from config/access.php.
Check the install with GET /up (see Health check) and php artisan about.
Environment variables
Everything below is in apps/backend-laravel/.env.example unless the table says otherwise.
App
| Variable | Meaning |
|---|---|
APP_NAME | Product name. Also the default mail sender name |
APP_ENV | local, production and so on. production turns the PDF renderer to headless Chromium and strict plugin catalogue checks off |
APP_KEY | Encryption key. Generate with key:generate and back it up |
APP_DEBUG | false in production |
APP_URL | The public address of the site. Pay, quote, tracking and portal links, reply webhooks and OAuth callbacks are built from it |
CONSOLE_URL | Where the console is served, when that isn't APP_URL. Invitation and password reset links point here |
PLATFORM_ADMIN_EMAILS | Comma separated emails allowed into Horizon outside local development |
INVITE_TTL_DAYS, LOGIN_ATTEMPTS | Days an invitation lasts (7), and wrong passwords allowed per email and address before sign-in locks for a minute (5) |
CONSOLE_SESSION_HOURS, CONSOLE_REMEMBER_DAYS | How long a console sign-in lasts (12 hours), or with "Keep me signed in" (30 days) |
APP_LOCALE, APP_FALLBACK_LOCALE, APP_FAKER_LOCALE | Language settings |
APP_MAINTENANCE_DRIVER | file or cache for maintenance mode |
APP_MAINTENANCE_STORE | Cache store for maintenance mode (commented out) |
PHP_CLI_SERVER_WORKERS | Workers for php artisan serve (commented out) |
BCRYPT_ROUNDS | Password hashing cost |
Logging
| Variable | Meaning |
|---|---|
LOG_CHANNEL | Where logs go. stack |
LOG_STACK | Channels in the stack. single |
LOG_DEPRECATIONS_CHANNEL | Channel for deprecation notices. null |
LOG_LEVEL | Minimum level. Use warning or error in production |
Database
| Variable | Meaning |
|---|---|
DB_CONNECTION | pgsql, mysql or sqlite |
DB_HOST, DB_PORT | Database server. The dev stack uses 127.0.0.1 and 54329 |
DB_DATABASE, DB_USERNAME, DB_PASSWORD | Database and login |
On MySQL, string columns in composite indexes are limited, so AppServiceProvider sets the default string length to 191 when the connection is mysql.
Session, cache and queue
| Variable | Meaning |
|---|---|
SESSION_DRIVER | database. The API uses bearer tokens, so sessions matter little. The console's own sign-in lasts CONSOLE_SESSION_HOURS (12) or, with "Keep me signed in", CONSOLE_REMEMBER_DAYS (30) |
SESSION_LIFETIME | Minutes |
SESSION_ENCRYPT | Encrypt session data |
SESSION_PATH, SESSION_DOMAIN | Cookie scope |
BROADCAST_CONNECTION | reverb for live updates, or null to turn them off |
FILESYSTEM_DISK | Default storage disk. s3 |
QUEUE_CONNECTION | redis in production. sync runs jobs inline |
DB_QUEUE_TABLE | Table name if the queue uses the database (queue_jobs) |
CACHE_STORE | redis |
CACHE_PREFIX | Key prefix. Set it when several apps share Redis (commented out) |
REDIS_CLIENT | phpredis or predis |
REDIS_HOST, REDIS_PASSWORD, REDIS_PORT | Redis server. .env.example is set for the dev Docker stack, which publishes Redis on 63799. On a server, set the port of your own Redis, usually 6379 |
REDIS_QUEUE_RETRY_AFTER | Seconds before a job a worker has not finished is handed to another worker. Default 600. Keep it above the longest Horizon timeout (300) or jobs run twice |
When sharing a Redis server, use dedicated REDIS_DB and REDIS_CACHE_DB numbers so apps don't collide, and set HORIZON_PREFIX.
Mail
The built-in email service uses these. Each business can switch to its own provider in Admin, which ignores them. See SMS and email setup.
| Variable | Meaning |
|---|---|
MAIL_MAILER | log writes mail to the log, smtp sends it |
MAIL_SCHEME | smtps or null |
MAIL_HOST, MAIL_PORT | SMTP server. .env.example is set for the dev Docker stack, where Mailpit listens on 1025. On a server, use your provider's host and port |
MAIL_USERNAME, MAIL_PASSWORD | SMTP login |
MAIL_FROM_ADDRESS, MAIL_FROM_NAME | Default sender |
Twilio (built-in SMS and the phone agent)
| Variable | Meaning |
|---|---|
TWILIO_ACCOUNT_SID, TWILIO_AUTH_TOKEN | The server's Twilio account |
TWILIO_FROM | Sending number |
TWILIO_MESSAGING_SERVICE_SID | A messaging service instead of a number |
TWILIO_BASE_URI | API address |
TWILIO_STATUS_CALLBACK_URL | Address Twilio posts delivery receipts to, for alert texts |
TWILIO_TIMEOUT | Request timeout in seconds |
TWILIO_RETRY_AFTER_MINUTES | Wait before retrying a failed alert text |
TWILIO_RETRYABLE_ERROR_CODES | Twilio error codes that are worth retrying |
Push notifications (Expo)
| Variable | Meaning |
|---|---|
EXPO_PUSH_URL, EXPO_RECEIPTS_URL | Expo push API addresses |
EXPO_ACCESS_TOKEN | Expo access token, if you've enabled push security |
EXPO_PUSH_TIMEOUT | Seconds |
EXPO_RECEIPT_DELAY_MINUTES | How long to wait before asking for receipts |
EXPO_RECEIPT_BATCH_SIZE | Receipts per request |
EXPO_RETRY_AFTER_MINUTES | Wait before retrying a failed push |
Monitoring alerts
| Variable | Meaning |
|---|---|
MONITORING_ALERT_RESEND_BATCH_SIZE | Alert notifications resent per run |
MONITORING_ALERT_RESEND_MAX_ATTEMPTS | Attempts before giving up |
Stripe
| Variable | Meaning |
|---|---|
STRIPE_SECRET_KEY | Platform secret key. Needed for card payments and for subscription billing |
STRIPE_WEBHOOK_SECRET | Signing secret for the subscription billing webhook |
STRIPE_API_BASE, STRIPE_API_VERSION, STRIPE_TIMEOUT | API address, pinned version, timeout |
STRIPE_WEBHOOK_TOLERANCE_SECONDS | Oldest signature accepted. 300 |
STRIPE_CHECKOUT_SUCCESS_URL, STRIPE_CHECKOUT_CANCEL_URL, STRIPE_PORTAL_RETURN_URL | Where subscription billing sends the browser back |
STRIPE_CONNECT_WEBHOOK_SECRET | Signing secret for the Connect webhook (card payments) |
STRIPE_CONNECT_APPLICATION_FEE_PERCENT | Platform fee on card payments. 0 for none |
STRIPE_CONNECT_COUNTRY | Country for new connected accounts. AU |
See payments setup.
Uploads and antivirus
| Variable | Meaning |
|---|---|
ATTACHMENT_ANTIVIRUS_DRIVER | basic (built-in checks for unsafe SVG and the EICAR test file) or clamav |
ATTACHMENT_ANTIVIRUS_FAIL_OPEN | false rejects a file when the scanner is down. Keep it false |
CLAMAV_HOST, CLAMAV_PORT, CLAMAV_UNIX_SOCKET | Where ClamAV listens |
CLAMAV_TIMEOUT, CLAMAV_CHUNK_SIZE, CLAMAV_MAX_BYTES | Scan timeout, chunk size and largest file |
Council reporting
| Variable | Meaning |
|---|---|
COUNCIL_REPORT_DISPATCH_ON_SUBMIT | true sends a queued submission straight away. false waits for the 15 minute retry command |
COUNCIL_REPORT_DISPATCH_DELAY_SECONDS | Delay before an immediate dispatch |
COUNCIL_REPORT_DELIVERY_BATCH_SIZE, COUNCIL_REPORT_RECEIPT_BATCH_SIZE | Submissions handled per run |
COUNCIL_REPORT_MAX_ATTEMPTS | Delivery attempts before failing |
COUNCIL_REPORT_RETRY_AFTER_MINUTES | Wait between attempts |
COUNCIL_REPORT_API_TIMEOUT | Seconds for council API calls |
COUNCIL_REPORT_SFTP_DISK | Filesystem disk used for SFTP delivery |
Accounting and OAuth
| Variable | Meaning |
|---|---|
ACCOUNTING_SYNC_INVOICE_BATCH_SIZE | Invoices per sync run |
ACCOUNTING_SYNC_API_TIMEOUT | Seconds |
XERO_OAUTH_AUTHORIZE_URL, XERO_OAUTH_TOKEN_URL, XERO_OAUTH_CONNECTIONS_URL | Xero endpoints |
XERO_OAUTH_REDIRECT_URI | Must match the redirect in your Xero app. APP_URL/api/v1/integrations/oauth/xero/callback |
XERO_OAUTH_SCOPES | Space separated scopes |
XERO_OAUTH_STATE_TTL_MINUTES | How long a sign-in attempt stays valid |
XERO_OAUTH_TIMEOUT | Seconds |
XERO_OAUTH_TOKEN_REFRESH_WINDOW_SECONDS | Refresh a token this long before it expires |
Route providers
All optional. Without keys the built-in straight-line optimiser is used.
| Variable | Meaning |
|---|---|
ROUTE_OPTIMIZATION_TRAFFIC_AWARE | Use live traffic where the provider supports it |
GOOGLE_ROUTES_API_KEY, GOOGLE_ROUTES_BASE_URI | Google Routes |
GOOGLE_ROUTES_TIMEOUT, _RETRIES, _RETRY_SLEEP_MS | Timeout and retry behaviour |
GOOGLE_ROUTES_MAX_ELEMENTS | Largest matrix |
GOOGLE_ROUTES_TRAFFIC_AWARE | Traffic for Google only |
MAPBOX_ACCESS_TOKEN, MAPBOX_BASE_URI | Mapbox |
MAPBOX_PROFILE, MAPBOX_TRAFFIC_PROFILE | Routing profiles |
MAPBOX_TIMEOUT, MAPBOX_RETRIES, MAPBOX_RETRY_SLEEP_MS | Timeout and retries |
MAPBOX_MAX_COORDINATES, MAPBOX_TRAFFIC_MAX_COORDINATES | Stop limits |
MAPBOX_TRAFFIC_AWARE | Traffic for Mapbox only |
OPENROUTESERVICE_API_KEY, OPENROUTESERVICE_BASE_URI | openrouteservice |
OPENROUTESERVICE_PROFILE | Profile, driving-car |
OPENROUTESERVICE_TIMEOUT, _RETRIES, _RETRY_SLEEP_MS | Timeout and retries |
OPENROUTESERVICE_MAX_LOCATIONS | Stop limit |
Object storage
| Variable | Meaning |
|---|---|
AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY | Storage credentials |
AWS_DEFAULT_REGION | Region |
AWS_BUCKET | Bucket name |
AWS_USE_PATH_STYLE_ENDPOINT | true for MinIO or SeaweedFS |
AWS_ENDPOINT | Custom endpoint for MinIO or another S3-compatible service |
Realtime (Reverb)
| Variable | Meaning |
|---|---|
REVERB_APP_ID, REVERB_APP_KEY, REVERB_APP_SECRET | Credentials shared between Laravel and clients |
REVERB_HOST, REVERB_PORT, REVERB_SCHEME | The address browsers connect to. Use your public host, port 443 and https in production |
REVERB_SERVER_HOST, REVERB_SERVER_PORT | Where the Reverb process listens |
Web
| Variable | Meaning |
|---|
Scheduled work and failure alerts
All optional. They are in .env.example, commented out.
| Variable | Meaning | Default |
|---|---|---|
OPERATIONS_DUE_JOB_LOOKAHEAD_DAYS | How far ahead draft jobs are created for work falling due | 14 |
OPERATIONS_MOBILE_SYNC_RETENTION_DAYS | Applied field-app sync events older than this are deleted | 180 |
OPERATIONS_LOCATION_EVENT_RETENTION_DAYS | Technician position history older than this is deleted | 90 |
OPERATIONS_UPLOAD_SESSION_RETENTION_DAYS | Uploads never finished, or refused, are cleaned up after this | 7 |
OPERATIONS_COUNCIL_DEADLINE_WARN_HOURS | The office is warned this long before a council deadline | 48 |
OPERATIONS_ROUTE_MATRIX_TTL_DAYS | Cached route legs that upcoming jobs still need are kept this long | 30 |
OPERATIONS_MESSAGE_FROM_HOUR, OPERATIONS_MESSAGE_UNTIL_HOUR | Customer messages only go out between these local hours | 8, 20 |
OPERATIONS_DUE_JOBS_LOCAL_HOUR | The local hour each business gets its due-job run | 5 |
OPERATIONS_CHUNK_SIZE | Rows handled per database chunk | 200 |
HEALTH_SCHEDULER_MAX_AGE_SECONDS | Age of the scheduler heartbeat that fails the health check | 300 |
HEALTH_QUEUE_MAX_BACKLOG | Jobs waiting before the health check fails | 1000 |
FAILED_JOB_ALERT_EMAILS | Platform admins emailed when a job fails for good, comma separated | none |
WEBHOOK_ALLOW_HTTP, WEBHOOK_ALLOW_PRIVATE_HOSTS | Let outgoing webhooks use plain http or private and internal addresses. Leave off in production; for local testing only | false |
FAILED_JOB_ALERT_THROTTLE_MINUTES | At most one email per job type in this time | 15 |
The first five can be set per business under settings.operations on the organization record, which wins over these values. A business sets the first, how far ahead due work is raised, under Admin > Recurring work. The business timezone decides every "local" hour.
Other settings
These are read by the code. They are in .env.example, commented out.
| Variable | Meaning | Default |
|---|---|---|
ANTHROPIC_API_KEY and the other ANTHROPIC_*, AI_* | AI helpers. See AI setup | unset |
TWILIO_VOICE, TWILIO_VOICE_WEBHOOK_BASE_URL, PHONE_AGENT_* | The phone agent. See AI setup | see page |
TURNSTILE_SITE_KEY, TURNSTILE_SECRET_KEY, TURNSTILE_VERIFY_URL | Server-wide Cloudflare Turnstile for the booking form | unset |
PDF_RENDERER | headless_chromium or local | headless_chromium in production |
PDF_CHROMIUM_BINARY | Path to Chromium or Chrome | empty |
PDF_RENDER_TIMEOUT_SECONDS, PDF_RENDER_MAX_ATTEMPTS | Per-render limits | 60, 3 |
PDF_TEMPLATE_VERSION | Recorded on each PDF | built in |
RANGEFRONT_UPLOAD_DISK | Disk for direct uploads | FILESYSTEM_DISK |
RANGEFRONT_UPLOAD_URL_TTL | Seconds a signed upload address lasts | 900 |
QUICKBOOKS_OAUTH_*, MYOB_OAUTH_*, SERVICEM8_OAUTH_*, GOOGLE_CALENDAR_OAUTH_*, MICROSOFT_365_OAUTH_* | Authorise and token addresses, REDIRECT_URI and SCOPES for each provider. Set *_REDIRECT_URI to APP_URL/api/v1/integrations/oauth/<provider>/callback | provider defaults |
INTEGRATION_OAUTH_STATE_TTL_MINUTES, _TIMEOUT, _TOKEN_REFRESH_WINDOW_SECONDS | Shared OAuth limits | 10, 10, 300 |
EXTENSIBILITY_RETHROW | true lets exceptions from plugins through, for tests | false |
EXTENSIBILITY_STRICT | Refuse hooks that aren't in the catalogue | on outside production |
HORIZON_PATH, HORIZON_DOMAIN, HORIZON_NAME, HORIZON_PREFIX | The Horizon dashboard address and name, and the Redis key prefix | horizon |
DEMO_READ_ONLY | Block all writes. Public demo only | false |
SANCTUM_STATEFUL_DOMAINS, SANCTUM_TOKEN_PREFIX | Sanctum. The console does not use Sanctum's stateful cookie mode, so SANCTUM_STATEFUL_DOMAINS can stay at its default | defaults |
CONSOLE_COOKIE_NAME, CONSOLE_COOKIE_SAME_SITE, CONSOLE_COOKIE_DOMAIN, CONSOLE_COOKIE_SECURE | The console's httpOnly sign-in cookie. Defaults suit a console and API on one host over HTTPS (Secure is set automatically when the request is HTTPS). Use same_site=none only when they are on different sites | rangefront_console, lax, none, auto |
CORS_ALLOWED_ORIGINS | Extra browser origins allowed to call the API with credentials, comma separated. The console origin (CONSOLE_URL or APP_URL) is always allowed. Leave empty when everything is served from one host | empty |
Laravel's own variables (DB_URL, REDIS_URL, SESSION_*, LOG_*, AWS_URL and so on) also work as the framework documents.
Queues
Jobs run through Redis, managed by Horizon. config/horizon.php starts three supervisors, in every environment, so each queue has a worker:
| Supervisor | Queues | Timeout | Used for |
|---|---|---|---|
realtime | critical, notifications, webhooks, default | 60 s | Customer messages, monitoring alerts, team push, outgoing webhooks, anything unnamed |
documents | documents, reports, uploads | 180 s | PDF rendering with Chromium, upload scanning |
integrations | integrations, council, routing, imports | 300 s | Accounting and calendar syncs, council delivery, data imports |
Each retries a failed job three times with a growing delay (the supervisor's backoff). Production runs up to 8, 4 and 3 worker processes. Staging and any other environment name (including local) run fewer, so no queue is ever left without a worker. Horizon raises an alert in its dashboard when a queue waits too long (waits in the config).
If you add a queue, add it to a supervisor and to waits. A test (HorizonQueueCoverageTest) reads every queue name in the code and fails if one has no worker, and fails if a timeout is not below REDIS_QUEUE_RETRY_AFTER.
Failed jobs are stored in the database (failed_jobs) for 30 days. php artisan queue:failed lists them and php artisan queue:retry all retries. Set FAILED_JOB_ALERT_EMAILS to have the platform admins emailed when one fails for good, at most once per job type every 15 minutes.
PDF rendering with PDF_RENDERER=local is for development. In production use headless_chromium and make sure the worker's user can run the binary.
Scheduled commands
php artisan schedule:run needs to run every minute. These commands are scheduled in routes/console.php:
| Command | When | Does |
|---|---|---|
rangefront:generate-due-jobs | Hourly, acts at 5am local per business | Creates draft jobs from active recurring plans and from systems whose service or pump-out is coming due, then tells dispatch. Also tells dispatch how many booked stops from earlier days were never started |
rangefront:send-service-reminders | Hourly | Sends service reminders for upcoming visits, for businesses with a reminder rule on |
rangefront:quotes:follow-up | Hourly | Reminds customers about unanswered quotes and expires old ones |
rangefront:send-invoice-reminders | Hourly | Sends the due invoice reminders |
rangefront:send-statements | Hourly | Sends monthly statements on each business's chosen day |
rangefront:council-report-deadline-check | Hourly | Notifies the office when a council report is near, or past, its deadline |
rangefront:retry-council-submissions | Every 15 minutes | Delivers queued council submissions and retries failures |
rangefront:council-submissions:poll-receipts | Hourly | Checks council receipts |
rangefront:expire-upload-sessions | Hourly | Deletes the stored file of uploads abandoned or refused for over 7 days, and marks them expired |
rangefront:prune-mobile-sync-events | Daily at 03:30 | Deletes applied field-app sync events older than 180 days |
rangefront:prune-location-events | Daily at 03:45 | Deletes technician position history older than 90 days |
rangefront:prune-twig-cache | Weekly, Sunday 04:15 | Deletes compiled templates not used for 30 days |
rangefront:refresh-route-matrix-cache | Daily at 03:00 | Drops expired route legs and extends those next week's jobs still need |
rangefront:monitoring-alerts:deliver | Every 15 minutes | Delivers alert notifications and escalations |
rangefront:monitoring-alerts:expo-receipts | Every 15 minutes | Reconciles push receipts |
rangefront:monitoring-alerts:resend-failures | Every 15 minutes | Resends alerts after transient failures |
horizon:snapshot | Every 5 minutes | Horizon dashboard metrics |
sanctum:prune-expired, queue:prune-failed | Daily | Removes expired API tokens and failed jobs older than 30 days |
A heartbeat task also runs every minute and records the time. The health check reads it.
How they behave:
- Overlap. Each has
withoutOverlappingwith an expiry, so a crashed run frees its lock in 30 to 180 minutes, not a day. Each also hasonOneServer, which needs a cache store shared by your servers (Redis). - Timezones. Work that touches customers runs every hour, and each business is handled in its own timezone. Reminders, quote follow-ups and statements only go out between 8am and 8pm local (statements and invoice reminders from 9am). Statements go on the business's chosen day, or the last day of a shorter month. Quotes still expire overnight.
- Running twice. Every command is safe to run again. Messages carry a dedupe key, council notices are recorded in
scheduled_notice_log, and a plan date or system due date only ever makes one job, even if its draft was cancelled. - One business failing. Each business is handled on its own. If one throws, the error is logged and reported, the rest carry on, and the maintenance commands exit with an error so you can see it.
- Summary. Each maintenance command logs a one-line summary of what it did.
Each command takes --organization=<id or slug> where it applies to one business at a time, which is handy for testing. rangefront:generate-due-jobs also takes --force to run outside the 5am hour. Plugins can add their own scheduled work.
Draft jobs from plans appear in Drafts for dispatch to schedule. Someone can still choose Generate on a plan to make visits further ahead. See schedule.
Other commands worth knowing:
| Command | Does |
|---|---|
rangefront:install-defaults <org> [--enable-messages] | Installs the commissioning form and standard messages |
rangefront:tenant:create, :list, :suspend, :resume | Create and switch off businesses. See above |
rangefront:openapi [--output=path] | Writes the OpenAPI contract |
rangefront:plugin:list, :make, :install, :enable, :disable, :uninstall | Plugin lifecycle. See extending |
rangefront:hooks:docs [--check] | Regenerates or checks the hook catalogue in docs/extending.md |
Health check
GET /up checks the install and returns JSON:
json
{"status":"ok","checks":{"database":{...},"redis":{...},"queue":{...},"scheduler":{...}}}| Check | Fails when |
|---|---|
database | A query can't run |
redis | Redis doesn't answer (only checked if the queue, cache or sessions use it) |
queue | The queue can't be reached. More than HEALTH_QUEUE_MAX_BACKLOG jobs waiting (usually Horizon is down) only makes the status degraded, so a backlog never takes web servers out of rotation |
scheduler | The heartbeat is older than 5 minutes, which means cron isn't running schedule:run |
status is ok, degraded or down. down returns HTTP 503, so point your load balancer or uptime monitor at it. degraded returns 200 and means a check has nothing to measure yet, or the queue backlog is over its limit. The scheduler shows unknown until its first heartbeat, and stays that way if cron was never set up, so alert on degraded as well in production. The response never includes error text.
Storage
Photos, signatures, imports and generated PDFs go to object storage. The browser and the phone upload straight to the bucket with a short-lived signed address (RANGEFRONT_UPLOAD_URL_TTL), and the server then verifies the file's hash and scans it. The bucket therefore has to accept PUT requests from your console's address and from the field app, so set a CORS rule for it. Objects live under organizations/{organization_id}/.... Keep the bucket private, turn on versioning and server-side encryption, and see backup and restore.
If you use FILESYSTEM_DISK=local, the signed local fallback is meant for development.
Laravel also needs storage/ and bootstrap/cache/ writable by the PHP user.
Mail
Out of the box, mail goes through the MAIL_* settings, and MAIL_MAILER=log just writes it to the log, which is right for development and wrong for production. Either set a real SMTP server here, or have each business connect its own provider under Admin > Email and SMS. Business-level settings win. Texts work the same way with TWILIO_*.
Mail needs the queue worker. Customer messages are queued on notifications.
Checks after installing
GET /upreturns"status":"ok". Give the scheduler a minute or two to write its first heartbeat.- Horizon shows workers running (
php artisan horizon:status), with all three supervisors in its dashboard. php artisan schedule:listshows the commands above.- Generate a PDF for a job and check it appears.
- Send a test email and text from Admin > Email and SMS.
- Open the console, sign in as the owner you invited, and check the live-update dot goes green.
- Request a password reset from the sign-in page and check the email arrives (it uses the same mail settings).