Scheduled Jobs
Cashlytics runs two recurring jobs. They are plain HTTP routes in the app that an external scheduler calls with a secret.
Jobs
| Job | Route | Default schedule (UTC) | Variable | What it does |
|---|---|---|---|---|
| Upcoming payments | GET /api/cron/upcoming-payments | 0 8 * * * | NOTIFICATION_SCHEDULE | Finds payments that are due and sends push notifications |
| Insights | GET /api/cron/insights | 0 6 * * * | INSIGHTS_SCHEDULE | Generates 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.
| Situation | Response |
|---|---|
CRON_SECRET is not set on the app | 500 with CRON_SECRET not configured |
| Header missing or wrong | 401 Unauthorized |
| Valid | 200 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.