Skip to main content

Scheduled Jobs

Cashlytics runs two recurring jobs. They are plain HTTP routes in the app that an external scheduler calls with a secret.

Jobs​

JobRouteDefault schedule (UTC)VariableWhat it does
Upcoming paymentsGET /api/cron/upcoming-payments0 8 * * *NOTIFICATION_SCHEDULEFinds payments that are due and sends push notifications
InsightsGET /api/cron/insights0 6 * * *INSIGHTS_SCHEDULEGenerates the findings shown by the assistant for every user. A failure for one user does not stop the run for the others

All schedules use standard five-field cron syntax and are interpreted in UTC. For example, 0 7 * * * runs at 07:00 UTC.

The cron container​

docker-compose.selfhost.yml includes a cashlytics-cron service based on alpine:3.20. On start it installs curl, writes a crontab with both jobs, and runs crond. Each job calls the app over the internal Compose network (http://cashlytics:3000) with the header Authorization: Bearer <CRON_SECRET>. The container starts only after the app is healthy. It is part of the stack in every setup, even if you do not use push reminders.

The demo stack (docker-compose.demo.yml) has no cron container.

Authentication with CRON_SECRET​

Both routes compare the Authorization header with Bearer <CRON_SECRET> in constant time.

SituationResponse
CRON_SECRET is not set on the app500 with CRON_SECRET not configured
Header missing or wrong401 Unauthorized
Valid200 with a JSON summary

Set CRON_SECRET in .env; both the app and the cron container read it. Generate one with openssl rand -hex 32.

Push notifications also need the VAPID keys, see Push Notifications. Without them the upcoming-payments job has nothing to send.

Run a job manually​

From the host, if port 3000 is reachable:

curl -H "Authorization: Bearer $CRON_SECRET" http://localhost:3000/api/cron/upcoming-payments
curl -H "Authorization: Bearer $CRON_SECRET" http://localhost:3000/api/cron/insights

Example responses:

{ "success": true, "notified": 1, "total": 3 }
{ "success": true, "totalGenerated": 12, "users": 1, "failed": 0 }

To check the scheduler itself, look at the container logs and call the route from inside it:

docker compose -f docker-compose.selfhost.yml logs cashlytics-cron
docker compose -f docker-compose.selfhost.yml exec cashlytics-cron \
sh -c 'curl -s -H "Authorization: Bearer $CRON_SECRET" $APP_URL/api/cron/insights'

Use your own scheduler​

You can drop the cron container and call the routes from any scheduler (host crontab, systemd timer, Kubernetes CronJob, a hosted cron service):

0 8 * * * curl -s -H "Authorization: Bearer YOUR_CRON_SECRET" https://your-domain.tld/api/cron/upcoming-payments
0 6 * * * curl -s -H "Authorization: Bearer YOUR_CRON_SECRET" https://your-domain.tld/api/cron/insights

CRON_SECRET must still be set on the app container. NOTIFICATION_SCHEDULE and INSIGHTS_SCHEDULE are only read by the Compose cron container; with your own scheduler, the schedule is whatever you configure there. Use GET requests.