# ============================================================================= # Ledgr — Environment Configuration # ============================================================================= # Copy this file to .env and fill in the required values. # # cp .env.example .env # # Everything has sensible defaults — with the provided docker-compose.yml, # secrets are auto-generated on first boot and persisted in the app data volume. # ============================================================================= # --------------- Required --------------- # Encrypts sensitive data (Plaid tokens, API keys). # Leave empty when using docker-compose: a key is generated on first boot and # stored in the app data volume (/data/encryption-key). To manage it yourself, # generate one with: # openssl rand -hex 32 # WARNING: If you lose this key, all encrypted data becomes unrecoverable. # Back up the key (or the app data volume) along with your database. ENCRYPTION_KEY= # Key rotation: add ENCRYPTION_KEY_V2 (openssl rand -hex 32), restart the app so # new writes use it, then run `pnpm rotate-keys` to re-encrypt existing rows. # ENCRYPTION_KEY remains required (it is version 1). Higher versions win. # ENCRYPTION_KEY_V2= # --------------- Recommended --------------- # Session signing secret. Leave empty when using docker-compose: one is # generated on first boot and stored in the app data volume. Without a data # volume, a temporary secret is generated on each start (users get logged out # on restart). Generate your own with: # openssl rand -base64 32 BETTER_AUTH_SECRET= # Change in production POSTGRES_PASSWORD=ledgr # Restricted DB role the app queries through, separate from DATABASE_URL's # admin/owner role — required for Postgres row-level security policies to # have any effect (superuser connections bypass RLS unconditionally). Set # automatically by docker-compose.yml; only needed here for a bare-metal/ # non-Docker setup. ensure-app-role.mjs creates the role on boot if it # doesn't exist yet. Optional — omit it and the app falls back to # DATABASE_URL, same as before this existed, just without RLS enforcement. # APP_DATABASE_URL=postgresql://ledgr_app:changeme@localhost:5433/ledgr # --------------- App URLs --------------- # Set these to your actual domain in production (e.g. https://ledgr.example.com) BETTER_AUTH_URL=http://localhost:4200 NEXT_PUBLIC_APP_URL=http://localhost:4200 # --------------- Plaid (optional) --------------- # Bank sync via Plaid. The app works without it — you can import via CSV. PLAID_CLIENT_ID= PLAID_SECRET= PLAID_ENV=production PLAID_SYNC_MODE=poll PLAID_WEBHOOK_URL= PLAID_OAUTH_REDIRECT_URI= # --------------- SimpleFIN (optional) --------------- # Bank sync via SimpleFIN — a simpler alternative to Plaid, no developer API # key required. Users paste a Setup Token from their own SimpleFIN Bridge # (https://www.simplefin.org). No config needed beyond the sync schedule # below; the connect option is always shown alongside Plaid's. # --------------- AI (optional) --------------- # Enable AI chat and auto-categorization. Bring your own API key. AI_PROVIDER= AI_MODEL= AI_API_KEY= AI_BASE_URL= AI_CONFIDENCE_THRESHOLD= AI_TOOL_CALLING= # --------------- MCP (optional) --------------- # Enable the MCP endpoint for AI agent access (Claude Code, etc.) MCP_ENABLED=false LEDGR_URL=http://localhost:4200 # --------------- Docker --------------- PORT=4200 DB_PORT=5433 # --- Scheduler --- # Master kill-switch. Set to "false" to disable all scheduled jobs # (use external cron, or simply turn it off in CI / tests). SCHEDULER_ENABLED=true # Cron expressions are in server-local timezone. # Nightly balance snapshot — writes one balance_history row per active account. SCHEDULER_SNAPSHOT_CRON=15 3 * * * # Daily Plaid "safety-sync" — catches transactions missed by webhooks. SCHEDULER_SAFETY_SYNC_CRON=30 4 * * * # Daily SimpleFIN sync — SimpleFIN has no webhooks, so this poll is the only # sync trigger (not just a backstop like the Plaid safety-sync above). SCHEDULER_SIMPLEFIN_SYNC_CRON=45 4 * * *