# Configuration Migration Guide ## Upgrading to the payment rule editor API release (#262) - **Two new tables, no action needed.** The sync-history database gains `actual_schedule_snapshot` (the Actual schedules captured by each sync) and `payment_alert_rule` (rules created in the dashboard), created on first start. A downgrade ignores them. - **New routes.** `GET /api/dashboard/payment-schedules/:server`, `GET`, `POST`, `PUT` and `DELETE /api/dashboard/payment-rules/:server[/:alertId]` and `POST /api/dashboard/payment-preview/:server`; the writes need `dashboard.allowConfigWrites: true` and dashboard auth other than `none`. See [HEALTH_CHECK.md](HEALTH_CHECK.md#payment-rules-262). - **`GET /api/dashboard/capabilities` gains `channels`**, the channels that currently have a destination. A client that compares the whole body must accept the new key. - **`GET /api/dashboard/payments` `source`** is now `"dashboard"` for a dashboard rule (it was always `"config"`). - **Empty `alerts` is allowed.** The schema no longer requires `scheduleAlerts.alerts` to have an item, so `"scheduleAlerts": { "alerts": [] }` opts a server in to dashboard rules. - **Reset semantics.** A history reset (one server or all) deletes the schedule snapshot but keeps dashboard rules, which are configuration. Delete a rule with `DELETE /api/dashboard/payment-rules/:server/:alertId`. ## Upgrading to the alert controls release (#261) - **New table, no action needed.** The sync-history database gains `payment_alert_override`, created on first start. It holds the dashboard's alert switch and mute (with the mute reason, up to 200 characters). A downgrade ignores it. - **New optional config key** `scheduleAlerts.alerts[].enabled` (default `true`; `false` switches that alert off permanently) and a new template key `scheduleAlerts.templates.muteEnded`. Nothing changes unless you use them. - **Dashboard controls need writes.** The switch and the mute form work only with `dashboard.allowConfigWrites: true` and dashboard auth other than `none`; otherwise they are shown disabled. - **Mute-end message.** After a mute ends, each destination receives one summary message if anything was held back; a mute with nothing to report ends silently. ## Upgrading to the Origin check release (#259) - **Cross-origin browser POSTs are refused.** `sync`, `dismiss-error`, `reset-history` and `test-notification` now return 403 `origin_rejected` when a browser sends an `Origin` or `Referer` that is not same-origin and not in `dashboard.allowedOrigins`. curl, scripts and Home Assistant (no browser headers) are unaffected. - **WARNING: `dashboard.allowedOrigins` now also authorizes these POSTs**, not only `/ws/logs`. Review the list and remove any origin you do not fully control. - **Behind a reverse proxy**, if the dashboard buttons now fail with 403, the proxy is rewriting `Host`. Use `proxy_set_header Host $http_host;` (nginx), or add the page origin to `dashboard.allowedOrigins`. - **Body errors are JSON.** Malformed or oversized bodies on these routes now return JSON (`invalid_json` 400, `payload_too_large` 413, `invalid_body` 400 for other unreadable bodies) instead of an HTML error page. - New optional setting `dashboard.allowConfigWrites` (default `false`); nothing changes unless you enable it. ## Upgrading to 1.11.0 **Read this before upgrading if you use notifications.** Four changes alter behaviour for existing configs. Two can make a channel go *silent*, one can make a channel start *talking*, and one can stop the service from starting. ### Check your config first (30 seconds) Validate against the new schema **before** you switch over, without touching your running service: ```bash docker run --rm -v ./config:/app/config:ro \ ghcr.io/agigante80/actual-sync:latest npm run validate-config ``` Anything reported here is something that would otherwise fail at startup. (This check was itself broken in 1.11.0 and 1.11.1 — under a bind mount it silently skipped **schema** validation, so it reported configs as valid that the service then refused to start. Use a release later than 1.11.1 for it to be meaningful.) ### 1. `notifyOnSuccess` now actually works — and `never` silences failures too `notifications.telegram.notifyOnSuccess` has existed in the schema for a long time but **gated nothing**: every channel notified on every sync regardless of what you set. From 1.11.0 it is enforced, and it now exists on every channel — with one exception, legacy `webhooks.telegram` entries, covered in §3 below. The trap is the name. `notifyOnSuccess: "never"` reads like *"never notify me on success"* — but it means **"turn this channel off entirely, failures included"**. | If you meant | Use | |---|---| | Stop telling me about *successful* syncs | **`errors_only`** | | Turn this channel off completely | `never` | **Action:** if any config of yours sets `notifyOnSuccess: "never"` and you actually wanted quiet successes, change it to `errors_only` before upgrading. On startup the service logs a `WARN` naming every channel it has muted, so check your logs after the first boot: ``` [WARN] Channels muted by notifyOnSuccess: "never" — these will NOT notify on failures either ``` This also applies to a mode set with the Telegram `/notify` command. Before 1.11.0 that command changed nothing on the dispatch path, so a value typed months ago as a no-op now takes effect. An explicit `notifications.telegram.notifyOnSuccess` in your config overrides a stored `/notify` value on restart; if config is silent, the stored value applies. ### 1b. Seeded your config from the example? Telegram gets quieter This is the most widely-affected change, and it needs no action — but it will surprise you if you don't expect it. Every `config.example.json` up to 1.10.1 shipped this: ```json "telegram": { "notifyOnSuccess": "errors_only" } ``` It did nothing, because the setting gated nothing. If you seeded your config from the example and left that line alone, **Telegram now stops notifying on successful syncs** — which is what the line always claimed it did. **Action:** none, if that is what you want. If you would rather keep every-sync notifications on Telegram, change it to `"always"` or delete the key. The example no longer sets it per-channel. ### 2. Enabled email now requires a sender and at least one recipient `notifications.email.enabled: true` combined with a missing `from`, a missing `to`, or an empty `to: []` previously validated fine — and then silently delivered nothing. That is now **rejected at startup**: ``` Configuration is invalid — it does not match the schema: - /notifications/email/to: must NOT have fewer than 1 items ``` **Action:** either fill in `from` and `to`, or set `enabled: false`. A disabled email block with an empty `to` is still perfectly valid, so placeholder stubs are unaffected. If you need to boot before you can fix the config, `CONFIG_STRICT=false` downgrades schema failures to warnings. Use it to get running, not as a permanent setting — the channel still cannot deliver. ### 3. Legacy `webhooks.telegram` entries now actually deliver A `notifications.webhooks.telegram` entry has never sent a message — the code path that would read it was unreachable. It works now. Consequence: an entry has **no `enabled` flag — its presence is its enablement**. If you have a legacy entry *and* `notifications.telegram.enabled: false`, you were receiving nothing and will now start receiving messages. **Action:** if you want that channel to stay off, either **remove the entry**, or set it on the top-level block: ```json "notifications": { "telegram": { "enabled": false, "notifyOnSuccess": "never" }, "webhooks": { "telegram": [ { "name": "legacy", "botToken": "…", "chatId": "…" } ] } } ``` > **Do not** put `notifyOnSuccess` on the legacy entry itself. Legacy `webhooks.telegram` items are the one exception to "every channel supports it" — the schema has no such key there, and because an unknown key is only an advisory warning the service still starts and silently ignores it. The channel would keep sending. ### Also worth knowing - **Delivery reporting is honest now.** The log line `Sync notification sent` only appears when at least one channel actually delivered; a total failure logs `Sync notification failed on every channel` at WARN. If you have monitoring that greps for the old line, it will now correctly go quiet when your alerting is broken. - **Undelivered notifications no longer consume the rate-limit budget.** A broken transport is therefore retried on every scheduled sync instead of being rate-limited away. See [Rate Limiting](NOTIFICATIONS.md#rate-limiting). - **A dead Telegram bot token is logged once**, not every two seconds. Full reference: [docs/NOTIFICATIONS.md](NOTIFICATIONS.md#notification-mode-notifyonsuccess). --- ## Overview As of December 2025, Actual-sync has moved from hardcoded server configuration to external configuration files. This provides better flexibility and security. --- ## What Changed ### Before (Hardcoded Configuration) Server configuration was embedded in `sync_all_banks.js`: ```javascript const servers = [ { name: 'Main', url: process.env.SERVICE_MAIN_URL || 'http://actual-main:5006', password: process.env.SERVICE_MAIN_PASSWORD || 'hunter2', // ... } ]; ``` ### After (External Configuration) Server configuration is now in `config.json`: ```json { "servers": [ { "name": "Main", "url": "http://actual-main:5006", "password": "your_password", "syncId": "your_sync_id", "dataDir": "/app/dataDir_Main_temp" } ], "sync": { "maxRetries": 5, "baseRetryDelayMs": 3000, "schedule": "03 03 */2 * *" } } ``` --- ## Migration Steps ### Step 1: Install Dependencies Update to the latest version and install new dependencies: ```bash cd /path/to/Actual-sync npm install ``` ### Step 2: Create config.json Copy the example configuration: ```bash cp config/config.example.json config/config.json ``` ### Step 3: Migrate Your Settings If you were using environment variables, migrate them to `config.json`: **Environment Variables → config.json Mapping:** | Environment Variable | config.json Path | |---------------------|------------------| | `SERVICE_MAIN_URL` | `servers[0].url` | | `SERVICE_MAIN_PASSWORD` | `servers[0].password` | | `SERVICE_MAIN_SYNC_ID` | `servers[0].syncId` | | `SERVICE_MAIN_DIR` | `servers[0].dataDir` | | `SERVICE_ALEJANDRO_URL` | `servers[1].url` | | `SERVICE_ALEJANDRO_PASSWORD` | `servers[1].password` | | `SERVICE_ALEJANDRO_SYNC_ID` | `servers[1].syncId` | | `SERVICE_ALEJANDRO_DIR` | `servers[1].dataDir` | **Example Migration:** If your `.env` file had: ```env SERVICE_MAIN_URL=https://budget.example.com SERVICE_MAIN_PASSWORD=mySecurePassword123 SERVICE_MAIN_SYNC_ID=abc-def-ghi-123 SERVICE_MAIN_DIR=/data/main ``` Your `config.json` should have: ```json { "servers": [ { "name": "Main", "url": "https://budget.example.com", "password": "mySecurePassword123", "syncId": "abc-def-ghi-123", "dataDir": "/data/main" } ] } ``` ### Step 4: Configure Multiple Servers To add more servers, add entries to the `servers` array: ```json { "servers": [ { "name": "Personal", "url": "https://personal.example.com", "password": "password1", "syncId": "sync-id-1", "dataDir": "/data/personal" }, { "name": "Business", "url": "https://business.example.com", "password": "password2", "syncId": "sync-id-2", "dataDir": "/data/business" } ] } ``` ### Step 5: Adjust Sync Settings (Optional) Customize retry behavior and schedule: ```json { "sync": { "maxRetries": 5, // Number of retry attempts (0-10) "baseRetryDelayMs": 3000, // Base delay for exponential backoff (milliseconds) "schedule": "0 2 * * *" // Cron: Run at 2 AM daily } } ``` **Common Cron Schedules:** - Every day at 2 AM: `0 2 * * *` - Every 6 hours: `0 */6 * * *` - Every other day at 3:03 AM: `03 03 */2 * *` (default) - Every hour: `0 * * * *` ### Step 6: Secure Your Configuration **Important Security Steps:** 1. Set appropriate file permissions: ```bash chmod 600 config.json ``` 2. Verify `config.json` is in `.gitignore`: ```bash grep "config.json" .gitignore ``` 3. Never commit `config.json` to version control ### Step 7: Validate Configuration Test your configuration before running: ```bash npm run validate-config ``` Or just start the service (it validates on startup): ```bash npm start ``` ### Step 8: Test Sync Run a manual sync to verify everything works: ```bash npm run sync ``` --- ## Troubleshooting ### "Configuration file not found" **Error:** ``` ❌ Failed to load configuration: Configuration file not found: ./config.json ``` **Solution:** Create `config/config.json` from the example: ```bash cp config/config.example.json config/config.json ``` Then edit with your settings. --- ### "Invalid JSON in configuration file" **Error:** ``` ❌ Failed to load configuration: Invalid JSON in configuration file: Unexpected token... ``` **Solution:** - Check for missing commas between properties - Check for trailing commas (not allowed in JSON) - Validate JSON syntax: https://jsonlint.com/ - Use a JSON validator: ```bash node -e "JSON.parse(require('fs').readFileSync('config/config.json', 'utf8'))" ``` --- ### "Configuration validation failed" **Error:** ``` ❌ Failed to load configuration: Configuration validation failed: - /servers/0/password: must be string ``` **Solution:** Fix the specific field mentioned in the error. Common issues: - Missing required fields (name, url, password, syncId, dataDir) - Wrong data types (e.g., number instead of string) - Invalid URLs (must start with http:// or https://) --- ### Warning: "Using unencrypted HTTP connection" **Warning:** ``` ⚠️ Warning: Server "Main" uses unencrypted HTTP connection. Consider using HTTPS for production: http://example.com ``` **Solution:** This is a warning, not an error. Consider using HTTPS in production: ```json { "url": "https://example.com" // Use HTTPS instead of HTTP } ``` For local/internal servers (localhost, 127.0.0.1), HTTP is acceptable. --- ### Warning: "Weak password" **Warning:** ``` ⚠️ Warning: Server "Main" has a weak password (< 8 characters). Consider using a stronger password for security. ``` **Solution:** Use a longer, more secure password (16+ characters recommended): ```json { "password": "aVeryLongAndSecurePassword123!@#" } ``` --- ## Backward Compatibility ### Environment Variables Still Work The `.env` file is still loaded, but it's no longer used for server configuration. You can remove the old environment variables: ```bash # These are no longer needed: # SERVICE_MAIN_URL # SERVICE_MAIN_PASSWORD # SERVICE_MAIN_SYNC_ID # SERVICE_MAIN_DIR # SERVICE_ALEJANDRO_URL # SERVICE_ALEJANDRO_PASSWORD # SERVICE_ALEJANDRO_SYNC_ID # SERVICE_ALEJANDRO_DIR ``` ### Rolling Back If you need to roll back to the old version: 1. Check out the previous version: ```bash git checkout ``` 2. Reinstall dependencies: ```bash npm install ``` Your `.env` file will work again with the old version. --- ## Configuration Reference ### Complete Example ```json { "$schema": "./config.schema.json", "servers": [ { "name": "Production", "url": "https://budget.example.com", "password": "SecurePassword123!", "syncId": "abc-123-def-456", "dataDir": "/var/lib/actual-sync/production" }, { "name": "Testing", "url": "http://localhost:5006", "password": "TestPassword", "syncId": "test-sync-id", "dataDir": "/tmp/actual-sync-test" } ], "sync": { "maxRetries": 5, "baseRetryDelayMs": 3000, "schedule": "0 2 * * *" }, "logging": { "level": "info" } } ``` ### Field Descriptions **servers** (required, array): - **name** (required, string): Human-readable server name - **url** (required, string): Actual Budget server URL - **password** (required, string): Server password - **syncId** (required, string): Budget sync ID from Actual settings - **dataDir** (required, string): Local directory for budget cache **sync** (optional, object): - **maxRetries** (optional, integer, default: 5): Max retry attempts (0-10) - **baseRetryDelayMs** (optional, integer, default: 3000): Base retry delay in ms - **schedule** (optional, string, default: "03 03 */2 * *"): Cron expression **logging** (optional, object): - **level** (optional, string, default: "info"): Log level (debug/info/warn/error) --- ## Getting Help If you encounter issues during migration: 1. Check the [troubleshooting section](#troubleshooting) 2. Validate your JSON syntax 3. Review the example configuration: `config.example.json` 4. Check the schema: `config.schema.json` 5. Open an issue with your error message (remove sensitive data!) --- **Migration completed?** You're all set — your budgets now sync from `config.json`.