Skip to content

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 ​

NeedNotes
PHP 8.3 or newerLaravel 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
ComposerFor apps/backend-laravel
DatabasePostgreSQL (the dev stack uses the PostGIS image) or MySQL 8. Tests run on SQLite
RedisQueues, cache and Horizon. The predis client is bundled. Use REDIS_CLIENT=predis if phpredis isn't installed
S3-compatible storageAWS S3, MinIO or similar, for photos, signatures and PDFs
Headless ChromiumRenders PDFs in production. Set PDF_CHROMIUM_BINARY
A process managersystemd or Supervisor, to keep Horizon and Reverb running
CronTo run the scheduler every minute
Node 20.19 or newer, on a build machineBuilds 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 packs

Keep 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/Brisbane

This 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:

CommandDoes
rangefront:tenant:listLists 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/dist

Serve 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:

VariableMeaning
VITE_RANGEFRONT_API_BASE_URLAPI base. Defaults to /api/v1/, which suits a single-host setup
VITE_MAP_TILE_URLMap tile address. Defaults to OpenStreetMap
VITE_MAP_TILE_ATTRIBUTIONAttribution text for the tiles
VITE_FIELD_APP_URLAdds a "Field app" link to the console
VITE_FORCE_DEMO_LOGIN, VITE_ENABLE_DEMO_LOGIN, VITE_DEMO_EMAIL, VITE_DEMO_PASSWORDAuto 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 ​

ProcessCommandKeep running with
Web and APIphp-fpm behind nginx, or php artisan serve for developmentnginx and php-fpm
Queue workerphp artisan horizonsystemd or Supervisor
Websocketsphp artisan reverb:start --host=127.0.0.1 --port=8080systemd or Supervisor
Scheduler* * * * * cd /path/to/apps/backend-laravel && php artisan schedule:run >> /dev/null 2>&1cron

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 ​

VariableMeaning
APP_NAMEProduct name. Also the default mail sender name
APP_ENVlocal, production and so on. production turns the PDF renderer to headless Chromium and strict plugin catalogue checks off
APP_KEYEncryption key. Generate with key:generate and back it up
APP_DEBUGfalse in production
APP_URLThe public address of the site. Pay, quote, tracking and portal links, reply webhooks and OAuth callbacks are built from it
CONSOLE_URLWhere the console is served, when that isn't APP_URL. Invitation and password reset links point here
PLATFORM_ADMIN_EMAILSComma separated emails allowed into Horizon outside local development
INVITE_TTL_DAYS, LOGIN_ATTEMPTSDays 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_DAYSHow long a console sign-in lasts (12 hours), or with "Keep me signed in" (30 days)
APP_LOCALE, APP_FALLBACK_LOCALE, APP_FAKER_LOCALELanguage settings
APP_MAINTENANCE_DRIVERfile or cache for maintenance mode
APP_MAINTENANCE_STORECache store for maintenance mode (commented out)
PHP_CLI_SERVER_WORKERSWorkers for php artisan serve (commented out)
BCRYPT_ROUNDSPassword hashing cost

Logging ​

VariableMeaning
LOG_CHANNELWhere logs go. stack
LOG_STACKChannels in the stack. single
LOG_DEPRECATIONS_CHANNELChannel for deprecation notices. null
LOG_LEVELMinimum level. Use warning or error in production

Database ​

VariableMeaning
DB_CONNECTIONpgsql, mysql or sqlite
DB_HOST, DB_PORTDatabase server. The dev stack uses 127.0.0.1 and 54329
DB_DATABASE, DB_USERNAME, DB_PASSWORDDatabase 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 ​

VariableMeaning
SESSION_DRIVERdatabase. 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_LIFETIMEMinutes
SESSION_ENCRYPTEncrypt session data
SESSION_PATH, SESSION_DOMAINCookie scope
BROADCAST_CONNECTIONreverb for live updates, or null to turn them off
FILESYSTEM_DISKDefault storage disk. s3
QUEUE_CONNECTIONredis in production. sync runs jobs inline
DB_QUEUE_TABLETable name if the queue uses the database (queue_jobs)
CACHE_STOREredis
CACHE_PREFIXKey prefix. Set it when several apps share Redis (commented out)
REDIS_CLIENTphpredis or predis
REDIS_HOST, REDIS_PASSWORD, REDIS_PORTRedis 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_AFTERSeconds 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.

VariableMeaning
MAIL_MAILERlog writes mail to the log, smtp sends it
MAIL_SCHEMEsmtps or null
MAIL_HOST, MAIL_PORTSMTP 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_PASSWORDSMTP login
MAIL_FROM_ADDRESS, MAIL_FROM_NAMEDefault sender

Twilio (built-in SMS and the phone agent) ​

VariableMeaning
TWILIO_ACCOUNT_SID, TWILIO_AUTH_TOKENThe server's Twilio account
TWILIO_FROMSending number
TWILIO_MESSAGING_SERVICE_SIDA messaging service instead of a number
TWILIO_BASE_URIAPI address
TWILIO_STATUS_CALLBACK_URLAddress Twilio posts delivery receipts to, for alert texts
TWILIO_TIMEOUTRequest timeout in seconds
TWILIO_RETRY_AFTER_MINUTESWait before retrying a failed alert text
TWILIO_RETRYABLE_ERROR_CODESTwilio error codes that are worth retrying

Push notifications (Expo) ​

VariableMeaning
EXPO_PUSH_URL, EXPO_RECEIPTS_URLExpo push API addresses
EXPO_ACCESS_TOKENExpo access token, if you've enabled push security
EXPO_PUSH_TIMEOUTSeconds
EXPO_RECEIPT_DELAY_MINUTESHow long to wait before asking for receipts
EXPO_RECEIPT_BATCH_SIZEReceipts per request
EXPO_RETRY_AFTER_MINUTESWait before retrying a failed push

Monitoring alerts ​

VariableMeaning
MONITORING_ALERT_RESEND_BATCH_SIZEAlert notifications resent per run
MONITORING_ALERT_RESEND_MAX_ATTEMPTSAttempts before giving up

Stripe ​

VariableMeaning
STRIPE_SECRET_KEYPlatform secret key. Needed for card payments and for subscription billing
STRIPE_WEBHOOK_SECRETSigning secret for the subscription billing webhook
STRIPE_API_BASE, STRIPE_API_VERSION, STRIPE_TIMEOUTAPI address, pinned version, timeout
STRIPE_WEBHOOK_TOLERANCE_SECONDSOldest signature accepted. 300
STRIPE_CHECKOUT_SUCCESS_URL, STRIPE_CHECKOUT_CANCEL_URL, STRIPE_PORTAL_RETURN_URLWhere subscription billing sends the browser back
STRIPE_CONNECT_WEBHOOK_SECRETSigning secret for the Connect webhook (card payments)
STRIPE_CONNECT_APPLICATION_FEE_PERCENTPlatform fee on card payments. 0 for none
STRIPE_CONNECT_COUNTRYCountry for new connected accounts. AU

See payments setup.

Uploads and antivirus ​

VariableMeaning
ATTACHMENT_ANTIVIRUS_DRIVERbasic (built-in checks for unsafe SVG and the EICAR test file) or clamav
ATTACHMENT_ANTIVIRUS_FAIL_OPENfalse rejects a file when the scanner is down. Keep it false
CLAMAV_HOST, CLAMAV_PORT, CLAMAV_UNIX_SOCKETWhere ClamAV listens
CLAMAV_TIMEOUT, CLAMAV_CHUNK_SIZE, CLAMAV_MAX_BYTESScan timeout, chunk size and largest file

Council reporting ​

VariableMeaning
COUNCIL_REPORT_DISPATCH_ON_SUBMITtrue sends a queued submission straight away. false waits for the 15 minute retry command
COUNCIL_REPORT_DISPATCH_DELAY_SECONDSDelay before an immediate dispatch
COUNCIL_REPORT_DELIVERY_BATCH_SIZE, COUNCIL_REPORT_RECEIPT_BATCH_SIZESubmissions handled per run
COUNCIL_REPORT_MAX_ATTEMPTSDelivery attempts before failing
COUNCIL_REPORT_RETRY_AFTER_MINUTESWait between attempts
COUNCIL_REPORT_API_TIMEOUTSeconds for council API calls
COUNCIL_REPORT_SFTP_DISKFilesystem disk used for SFTP delivery

Accounting and OAuth ​

VariableMeaning
ACCOUNTING_SYNC_INVOICE_BATCH_SIZEInvoices per sync run
ACCOUNTING_SYNC_API_TIMEOUTSeconds
XERO_OAUTH_AUTHORIZE_URL, XERO_OAUTH_TOKEN_URL, XERO_OAUTH_CONNECTIONS_URLXero endpoints
XERO_OAUTH_REDIRECT_URIMust match the redirect in your Xero app. APP_URL/api/v1/integrations/oauth/xero/callback
XERO_OAUTH_SCOPESSpace separated scopes
XERO_OAUTH_STATE_TTL_MINUTESHow long a sign-in attempt stays valid
XERO_OAUTH_TIMEOUTSeconds
XERO_OAUTH_TOKEN_REFRESH_WINDOW_SECONDSRefresh a token this long before it expires

Route providers ​

All optional. Without keys the built-in straight-line optimiser is used.

VariableMeaning
ROUTE_OPTIMIZATION_TRAFFIC_AWAREUse live traffic where the provider supports it
GOOGLE_ROUTES_API_KEY, GOOGLE_ROUTES_BASE_URIGoogle Routes
GOOGLE_ROUTES_TIMEOUT, _RETRIES, _RETRY_SLEEP_MSTimeout and retry behaviour
GOOGLE_ROUTES_MAX_ELEMENTSLargest matrix
GOOGLE_ROUTES_TRAFFIC_AWARETraffic for Google only
MAPBOX_ACCESS_TOKEN, MAPBOX_BASE_URIMapbox
MAPBOX_PROFILE, MAPBOX_TRAFFIC_PROFILERouting profiles
MAPBOX_TIMEOUT, MAPBOX_RETRIES, MAPBOX_RETRY_SLEEP_MSTimeout and retries
MAPBOX_MAX_COORDINATES, MAPBOX_TRAFFIC_MAX_COORDINATESStop limits
MAPBOX_TRAFFIC_AWARETraffic for Mapbox only
OPENROUTESERVICE_API_KEY, OPENROUTESERVICE_BASE_URIopenrouteservice
OPENROUTESERVICE_PROFILEProfile, driving-car
OPENROUTESERVICE_TIMEOUT, _RETRIES, _RETRY_SLEEP_MSTimeout and retries
OPENROUTESERVICE_MAX_LOCATIONSStop limit

Object storage ​

VariableMeaning
AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEYStorage credentials
AWS_DEFAULT_REGIONRegion
AWS_BUCKETBucket name
AWS_USE_PATH_STYLE_ENDPOINTtrue for MinIO or SeaweedFS
AWS_ENDPOINTCustom endpoint for MinIO or another S3-compatible service

Realtime (Reverb) ​

VariableMeaning
REVERB_APP_ID, REVERB_APP_KEY, REVERB_APP_SECRETCredentials shared between Laravel and clients
REVERB_HOST, REVERB_PORT, REVERB_SCHEMEThe address browsers connect to. Use your public host, port 443 and https in production
REVERB_SERVER_HOST, REVERB_SERVER_PORTWhere the Reverb process listens

Web ​

VariableMeaning

Scheduled work and failure alerts ​

All optional. They are in .env.example, commented out.

VariableMeaningDefault
OPERATIONS_DUE_JOB_LOOKAHEAD_DAYSHow far ahead draft jobs are created for work falling due14
OPERATIONS_MOBILE_SYNC_RETENTION_DAYSApplied field-app sync events older than this are deleted180
OPERATIONS_LOCATION_EVENT_RETENTION_DAYSTechnician position history older than this is deleted90
OPERATIONS_UPLOAD_SESSION_RETENTION_DAYSUploads never finished, or refused, are cleaned up after this7
OPERATIONS_COUNCIL_DEADLINE_WARN_HOURSThe office is warned this long before a council deadline48
OPERATIONS_ROUTE_MATRIX_TTL_DAYSCached route legs that upcoming jobs still need are kept this long30
OPERATIONS_MESSAGE_FROM_HOUR, OPERATIONS_MESSAGE_UNTIL_HOURCustomer messages only go out between these local hours8, 20
OPERATIONS_DUE_JOBS_LOCAL_HOURThe local hour each business gets its due-job run5
OPERATIONS_CHUNK_SIZERows handled per database chunk200
HEALTH_SCHEDULER_MAX_AGE_SECONDSAge of the scheduler heartbeat that fails the health check300
HEALTH_QUEUE_MAX_BACKLOGJobs waiting before the health check fails1000
FAILED_JOB_ALERT_EMAILSPlatform admins emailed when a job fails for good, comma separatednone
WEBHOOK_ALLOW_HTTP, WEBHOOK_ALLOW_PRIVATE_HOSTSLet outgoing webhooks use plain http or private and internal addresses. Leave off in production; for local testing onlyfalse
FAILED_JOB_ALERT_THROTTLE_MINUTESAt most one email per job type in this time15

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.

VariableMeaningDefault
ANTHROPIC_API_KEY and the other ANTHROPIC_*, AI_*AI helpers. See AI setupunset
TWILIO_VOICE, TWILIO_VOICE_WEBHOOK_BASE_URL, PHONE_AGENT_*The phone agent. See AI setupsee page
TURNSTILE_SITE_KEY, TURNSTILE_SECRET_KEY, TURNSTILE_VERIFY_URLServer-wide Cloudflare Turnstile for the booking formunset
PDF_RENDERERheadless_chromium or localheadless_chromium in production
PDF_CHROMIUM_BINARYPath to Chromium or Chromeempty
PDF_RENDER_TIMEOUT_SECONDS, PDF_RENDER_MAX_ATTEMPTSPer-render limits60, 3
PDF_TEMPLATE_VERSIONRecorded on each PDFbuilt in
RANGEFRONT_UPLOAD_DISKDisk for direct uploadsFILESYSTEM_DISK
RANGEFRONT_UPLOAD_URL_TTLSeconds a signed upload address lasts900
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>/callbackprovider defaults
INTEGRATION_OAUTH_STATE_TTL_MINUTES, _TIMEOUT, _TOKEN_REFRESH_WINDOW_SECONDSShared OAuth limits10, 10, 300
EXTENSIBILITY_RETHROWtrue lets exceptions from plugins through, for testsfalse
EXTENSIBILITY_STRICTRefuse hooks that aren't in the catalogueon outside production
HORIZON_PATH, HORIZON_DOMAIN, HORIZON_NAME, HORIZON_PREFIXThe Horizon dashboard address and name, and the Redis key prefixhorizon
DEMO_READ_ONLYBlock all writes. Public demo onlyfalse
SANCTUM_STATEFUL_DOMAINS, SANCTUM_TOKEN_PREFIXSanctum. The console does not use Sanctum's stateful cookie mode, so SANCTUM_STATEFUL_DOMAINS can stay at its defaultdefaults
CONSOLE_COOKIE_NAME, CONSOLE_COOKIE_SAME_SITE, CONSOLE_COOKIE_DOMAIN, CONSOLE_COOKIE_SECUREThe 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 sitesrangefront_console, lax, none, auto
CORS_ALLOWED_ORIGINSExtra 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 hostempty

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:

SupervisorQueuesTimeoutUsed for
realtimecritical, notifications, webhooks, default60 sCustomer messages, monitoring alerts, team push, outgoing webhooks, anything unnamed
documentsdocuments, reports, uploads180 sPDF rendering with Chromium, upload scanning
integrationsintegrations, council, routing, imports300 sAccounting 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:

CommandWhenDoes
rangefront:generate-due-jobsHourly, acts at 5am local per businessCreates 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-remindersHourlySends service reminders for upcoming visits, for businesses with a reminder rule on
rangefront:quotes:follow-upHourlyReminds customers about unanswered quotes and expires old ones
rangefront:send-invoice-remindersHourlySends the due invoice reminders
rangefront:send-statementsHourlySends monthly statements on each business's chosen day
rangefront:council-report-deadline-checkHourlyNotifies the office when a council report is near, or past, its deadline
rangefront:retry-council-submissionsEvery 15 minutesDelivers queued council submissions and retries failures
rangefront:council-submissions:poll-receiptsHourlyChecks council receipts
rangefront:expire-upload-sessionsHourlyDeletes the stored file of uploads abandoned or refused for over 7 days, and marks them expired
rangefront:prune-mobile-sync-eventsDaily at 03:30Deletes applied field-app sync events older than 180 days
rangefront:prune-location-eventsDaily at 03:45Deletes technician position history older than 90 days
rangefront:prune-twig-cacheWeekly, Sunday 04:15Deletes compiled templates not used for 30 days
rangefront:refresh-route-matrix-cacheDaily at 03:00Drops expired route legs and extends those next week's jobs still need
rangefront:monitoring-alerts:deliverEvery 15 minutesDelivers alert notifications and escalations
rangefront:monitoring-alerts:expo-receiptsEvery 15 minutesReconciles push receipts
rangefront:monitoring-alerts:resend-failuresEvery 15 minutesResends alerts after transient failures
horizon:snapshotEvery 5 minutesHorizon dashboard metrics
sanctum:prune-expired, queue:prune-failedDailyRemoves 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 withoutOverlapping with an expiry, so a crashed run frees its lock in 30 to 180 minutes, not a day. Each also has onOneServer, 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:

CommandDoes
rangefront:install-defaults <org> [--enable-messages]Installs the commissioning form and standard messages
rangefront:tenant:create, :list, :suspend, :resumeCreate and switch off businesses. See above
rangefront:openapi [--output=path]Writes the OpenAPI contract
rangefront:plugin:list, :make, :install, :enable, :disable, :uninstallPlugin 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":{...}}}
CheckFails when
databaseA query can't run
redisRedis doesn't answer (only checked if the queue, cache or sessions use it)
queueThe 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
schedulerThe 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 ​

  1. GET /up returns "status":"ok". Give the scheduler a minute or two to write its first heartbeat.
  2. Horizon shows workers running (php artisan horizon:status), with all three supervisors in its dashboard.
  3. php artisan schedule:list shows the commands above.
  4. Generate a PDF for a job and check it appears.
  5. Send a test email and text from Admin > Email and SMS.
  6. Open the console, sign in as the owner you invited, and check the live-update dot goes green.
  7. Request a password reset from the sign-in page and check the email arrives (it uses the same mail settings).

A$125 a month, or A$9,450 once with the source code. Contact us.