Skip to content

Configuration

Every runtime setting lives in one file: data/config.json. The app creates it on first boot with auto-detected defaults and rewrites it whenever you change a setting in the dashboard (Settings) or via the API (PUT /api/v1/config). You can also edit it by hand while the app is stopped.

The schema is defined by AppConfig in src/finance/app_config.py — if this table and the code ever disagree, the code wins (and a docs fix is welcome).

Release stability: pre-1.0, keys may be renamed or removed between minor versions. releases.md asks every release to call out config changes in the changelog.

KeyTypeDefaultWhat it does
storage"sqlite" | "dynamodb""sqlite"Storage backend. SQLite uses data/finance.db. DynamoDB is an explicit opt-in for the AWS path and is never auto-selected — set it here if you run the Lambda stack. Region comes from the AWS_REGION env var (falls back to us-west-2).
demo_modebooltrue on first runServes the seeded demo database (data/demo.db) instead of your real data, and blocks uploads/imports. The dashboard shows a demo banner. Flip to false when you wire up real email ingestion.
user_idstring"default"Partition owner for budget/override records. Single-user app — leave it alone unless you know why you’re changing it.
timezoneIANA zone name"America/Los_Angeles"Drives day/month bucketing, “latest transaction” age math, and the daily-summary schedule. Set it to your zone (e.g. "Europe/Berlin"); invalid names fall back to Pacific with a logged warning.
ai_categorization_enabledboolauto: true iff OPENAI_API_KEY is setMaster switch for AI transaction categorization. Off, every new transaction lands as miscellaneous for manual categorization.
ai_extraction_enabledboolauto: true iff OPENAI_API_KEY is setConsent to rescue unreadable emails with AI: when no parser can read an email, its full body is sent to the configured AI provider for a validated extraction. Distinct from ai_categorization_enabled — either can be on without the other. Off, unreadable emails are captured to Needs review without any AI call (no email body reaches the AI, not even the relevance classifier). Absent from an existing config, it preserves the prior consent: it inherits a persisted ai_categorization_enabled (which gated extraction before the two were split), falling back to the key check only when neither AI key was ever persisted.
ai_statement_parsing_enabledboolfalseConsent to parse statement PDFs with AI: when no built-in bank parser can read an uploaded statement, its extracted text is sent to the provider in summary_provider and the reply is validated — every amount must appear verbatim in the PDF text — before anything is saved. Never auto-enables (a statement is the most sensitive document the app touches), so it is always an explicit opt-in from Settings → Intelligence.
ai_receipt_parsing_enabledboolfalseConsent to parse receipt attachments with AI: sends a receipt photo or PDF to the provider in summary_provider, only when you ask (the parse action in the attach dialog). The reply is validated fail-closed and amounts are checked against your transactions before anything is linked. Never auto-enables — always an explicit opt-in from Settings → Intelligence.
tax_tracking_enabledbooltrueWhether tax receipt tracking is active. When on, the Tax receipts workspace tab, its /tax route, and the per-row “Flag as tax item” control on the Transactions and Journal pages are all available. Turning it off (Settings → Features) hides all three; existing tax flags are preserved and reappear if re-enabled. Defaults on so upgrading users keep their tax flags.
summary_provider"openai" | "claude_cli" | "codex" | "gemini_cli" | "disabled""disabled"Which AI provider writes journal daily summaries and insights briefings. The CLI providers shell out to a locally installed, authenticated CLI; openai uses the API key.
enable_daily_summariesbooltrueWhether the in-process scheduler generates journal daily summaries at all (only takes effect when summary_provider is not "disabled").
daily_summary_schedule_time"HH:MM" (24h)"19:00"When the daily summary runs, evaluated in timezone. Invalid values fall back to the default with a logged warning.
agent_tokenslist[]Bearer-token records for agent/API access — sha256 hashes only, never raw tokens. Manage via make agent-token / make agent-token-show / make agent-token-revoke; don’t hand-edit. See agent-access.md.
app_password_hashstring | nullabsentArgon2id hash of the dashboard password. Absent means TOFU mode: the API allows unauthenticated access and the dashboard nags you to set a password. Set it from Settings → Password. To reset a forgotten password, stop the app and remove this key from data/config.json — the dashboard returns to TOFU mode.
session_versionint0Bumped by “sign out everywhere” — invalidates every existing session cookie without rotating the secret.
session_signing_secret64-char hexauto-generated on first useHMAC key for session cookies. Treat it like a password; rotate only if it leaks (rotation signs everyone out).
auth_bypass_for_devboolfalseDev-only escape hatch: skips cookie auth on /api/v1/* even when a password is set (bearer enforcement still applies). Never enable on an instance that binds beyond localhost — anyone on the network gets full read/write access.
  • Secrets and infrastructure env varsOPENAI_API_KEY, IMAP_*, notification provider settings (NOTIFICATION_URL, NOTIFICATION_PROVIDER, TWILIO_*, SNS_TOPIC_ARN), AWS_REGION, SERVE_FRONTEND, CORS_ALLOWED_ORIGINS — come from the environment / .env. .env.example is the complete, commented list.
  • Budgets, category overrides, merchant aliases — stored in the database, managed in the dashboard.

When data/config.json is absent, the app writes one with storage: "sqlite" and demo_mode: true — even when AWS credentials are present on the machine. ai_categorization_enabled and ai_extraction_enabled both auto-detect from OPENAI_API_KEY. After that first write, the file is authoritative: auto-detection never overrides a persisted value.