Every setting Dompetku reads from its environment. New to this? An environment variable is a named setting (like APP_PASSWORD) that you give to the server instead of writing it into the code — so your secrets never end up on GitHub.
Where to set them
| Place | How |
|---|---|
| Vercel (production) | Project → Settings → Environment Variables → add Key/Value → then Deployments → ⋯ → Redeploy. Changes only apply after a redeploy. |
Local .env (development) |
cp .env.example .env, fill in values. Never commit .env. |
| n8n | Environment of the n8n container/process (e.g. docker-compose.yml). See n8n variables and N8N.md. |
Warning
All app variables are server-only. None of them start with VITE_, so none are ever sent to the browser. Keep it that way — never rename a secret to VITE_….
Step-by-step setup: SELF-HOSTING.md. Template: .env.example.
After running the v16 section of supabase/schema.sql, Settings → Integrasi can store the bot, AI, email and n8n values below in the database — like Vercel environment variables, but without editing .env or redeploying. Changes apply within ~60 s per server instance (immediately on the instance that saved them).
Precedence: value saved in Settings (if valid) → env var → built-in default. Nothing saved (or no v16 table) = exactly the env behaviour you have today. Remove in Settings deletes the database value and falls back to env.
| Can be set in Settings → Integrasi | Env only (never manageable from the UI) |
|---|---|
N8N_API_KEY*, BOT_ALLOWED_CHAT_IDS, BOT_TEXT_AI, BOT_AI_DAILY_LIMIT |
SUPABASE_URL, SUPABASE_SERVICE_ROLE_KEY (needed to read the database at all) |
AI_API_URL, AI_API_KEY*, AI_MODEL, AI_MODEL_TEXT |
SESSION_SECRET, SETTINGS_ENCRYPTION_KEY, APP_USERNAME, APP_PASSWORD, APP_TOTP_SECRET (guard access to the settings themselves) |
RESEND_API_KEY*, EMAIL_FROM, EMAIL_TO |
DEMO_MODE, SENTRY_DSN, APP_TIMEZONE/BOT_DEFAULT_ACCOUNT (those two are in Settings → Aplikasi, v14), FALLBACK_USD_IDR, … |
TELEGRAM_BOT_TOKEN* (optional, Telegram direct mode) |
* Secrets are write-only, like Vercel: they are encrypted at rest (AES-256-GCM, random IV per value) and the server never sends them back to the browser — Settings only shows the source (Database / Env / Belum diatur) and the last 4 characters. The integration_settings table is not included in JSON backups, so re-enter secrets after restoring onto a new instance.
| Name | Required? | Example | What it does | Security |
|---|---|---|---|---|
SETTINGS_ENCRYPTION_KEY |
No | <openssl rand -base64 48> |
Key material (≥ 32 chars) for encrypting secrets saved in Settings → Integrasi and for the Telegram webhook secret. When empty (or shorter), a key is derived from SESSION_SECRET with HKDF and a separate label. |
Secret. Changing it (or SESSION_SECRET when this is empty) makes saved secrets unreadable: the app then falls back to env and Settings asks you to re-enter them. Set it if you ever plan to rotate SESSION_SECRET. |
Warning
Rotating SESSION_SECRET without SETTINGS_ENCRYPTION_KEY logs you out and invalidates every secret saved in Settings → Integrasi (and the Telegram webhook secret). Nothing crashes — env values are used — but re-enter the secrets and click Pasang webhook again afterwards.
| Name | Example | What it does | How to obtain | Security |
|---|---|---|---|---|
SUPABASE_URL |
https://your-project-ref.supabase.co |
Address of your Supabase database API. | Supabase → Project Settings → Data API → Project URL. | Not secret on its own. |
SUPABASE_SERVICE_ROLE_KEY |
sb_secret_… or eyJ… (legacy service_role) |
Server key used for all database and Storage access. RLS is on with no policies, so only this key can read data. | Supabase → Project Settings → API Keys → Secret key (or Legacy → service_role). |
Highly sensitive — full database access. Never in browser, logs, screenshots. Rotate in Supabase if leaked. |
APP_USERNAME |
me |
The single login username. | Choose it. | Treat as private. |
APP_PASSWORD |
<long random password> |
The login password (compared in constant time; 8 failed attempts per 15 min lock login). Once you change the password in Profile (schema v17) the stored hash is used instead and this value stops working — see Password changed in the app. | Password manager. | Secret. Use a long unique password. |
SESSION_SECRET |
<openssl rand -base64 48> |
HMAC key that signs the login cookie (valid 7 days). Must be ≥ 32 characters, otherwise login fails. | openssl rand -base64 48 (see SELF-HOSTING §3). |
Secret. Changing it logs everyone out. |
| Name | Required? | Example | What it does | Security |
|---|---|---|---|---|
N8N_API_KEY |
For bot, reminders, backups, any /api/public/n8n/* call |
<openssl rand -base64 48> |
Shared key that n8n sends in the x-api-key header (or Authorization: Bearer). Must be ≥ 24 characters; empty/shorter makes every n8n endpoint answer 503, a wrong value gives 401. |
Secret. Same value goes into the n8n Header Auth credential. The backup endpoint returns all your data, so protect it like a password. |
After running the v17 section of supabase/schema.sql, Profile (avatar menu, or Settings → Profil & password) lets you change your name, address, photo and password. The new password is stored in app_users.password_hash as a salted scrypt hash (scrypt$v1$N$r$p$salt$hash); from then on the login uses that hash and APP_PASSWORD is ignored. Changing it also signs out every other device. Without v17 (or before your first change) login works exactly as before with APP_PASSWORD.
| Name | Example | What it does | Security |
|---|---|---|---|
APP_PASSWORD_RESET |
true (only to recover) |
Recovery switch. true ignores the stored password hash so APP_PASSWORD works again. Each login with it is logged (server log + activity "Login dengan APP_PASSWORD_RESET"). Any other value = off. |
Only someone with access to your hosting env can set it. Change the password in Profile, then remove it and redeploy. |
| Name | Example | What it does | How to obtain | Security |
|---|---|---|---|---|
APP_TOTP_SECRET |
JBSWY3DPEHPK3PXP… (base32, ≥ 16 chars) |
Enables a 6-digit authenticator code after the password. Empty = password-only. An invalid value is ignored (2FA off) and flagged in Settings. | App → Settings → Two-step verification (2FA) → Generate secret key. | Secret. Keep a backup in your password manager. Removing it + redeploy disables 2FA. |
| Name | Default | Example | What it does |
|---|---|---|---|
AI_API_URL |
Lovable AI gateway (only works on Lovable) | https://generativelanguage.googleapis.com/v1beta/openai/chat/completions |
Full URL of an OpenAI-compatible chat-completions endpoint. Must end in /chat/completions. Set it on Vercel. |
AI_API_KEY |
— | <provider API key> |
Sent as Authorization: Bearer …. Without it, OCR and AI parsing return an error (non-AI chat parsing still works). Secret. |
AI_MODEL |
google/gemini-2.5-flash |
gemini-2.5-flash, gpt-4o-mini |
Model for receipt photos — must support images (vision). Name format depends on the provider. |
AI_MODEL_TEXT |
value of AI_MODEL |
gemini-2.5-flash-lite |
Cheaper model for parsing ambiguous chat messages. |
LOVABLE_API_KEY |
— | — | Set automatically by Lovable; used as the key when AI_API_KEY is empty. Don't set it on Vercel. |
Provider examples (Gemini, OpenAI, OpenRouter, Ollama): SELF-HOSTING §6.2.
Proxies and routers (e.g. 9router, LiteLLM) work too: the app sends "stream": false, and if the proxy still answers with a streamed (text/event-stream) response, the chunks are joined automatically. If the bot replies "Respons AI tidak bisa dibaca", check that AI_API_URL is the full /chat/completions URL.
| Name | Required? | Example | What it does |
|---|---|---|---|
BOT_ALLOWED_CHAT_IDS |
Yes, if you use the bot | 123456789,987654321 |
Comma-separated Telegram chat IDs allowed to use the bot (group chats have negative IDs, supergroups start with -100; see N8N.md). Fails closed: empty = every chat is refused before any database access, and the bot replies with the chat ID to add. |
BOT_DEFAULT_ACCOUNT |
No | BCA |
Account name used when a message doesn't mention one. Must match an existing account name. Can be overridden in Settings → App (v14). |
BOT_TEXT_AI |
No (default auto) |
auto | always | never |
When chat messages may use AI: only when ambiguous, always, or never (zero AI tokens for chat; receipts still use AI). |
BOT_AI_DAILY_LIMIT |
No (default 50) |
50 | 0 |
Max bot AI calls (chat parsing + photo OCR) per chat per app-local day; the bot then asks for the quick format. 0 = unlimited. Counted in ai_usage (v15); without it, today's AI/OCR drafts. |
TELEGRAM_BOT_TOKEN |
Only for direct mode | 123456789:AA… |
Enables Telegram direct mode: Telegram calls POST /api/public/telegram/webhook on the app itself, no n8n relay. Not needed with n8n. Secret. |
| Name | Default | Example | What it does |
|---|---|---|---|
APP_TIMEZONE |
Asia/Jakarta |
Asia/Makassar, Europe/Berlin |
IANA time zone that defines "today" for new transactions, reminders, reports and recurring items. Use the same zone in n8n (GENERIC_TIMEZONE). Can be overridden in Settings → App (v14). |
FALLBACK_USD_IDR |
16000 |
16250 |
USD→IDR rate used only when the live rate API (open.er-api.com) fails and no rate is stored yet. |
Settings override (v14). After running the v14 section of
supabase/schema.sql, Settings → Aplikasi stores the app name, tagline, logo, time zone, base currency (display preference only — totals stay in IDR), landing page toggle, bot default account and default reminder days in the single-rowapp_settingstable. Values set there win overAPP_TIMEZONE/BOT_DEFAULT_ACCOUNT; empty fields (or a missing table) fall back to these env vars and built-in defaults. Changes apply within ~60 s per server instance.
All three are needed for POST /api/public/n8n/reminders-send-email. Not needed if n8n sends email itself.
| Name | Example | What it does | How to obtain |
|---|---|---|---|
RESEND_API_KEY |
re_… |
Resend API key. Secret. | https://resend.com → API Keys. |
EMAIL_FROM |
Dompetku <noreply@mail.example.com> |
Sender; the domain must be verified in Resend. | Resend → Domains → add DNS records your domain provider shows. |
EMAIL_TO |
you@example.com |
Default recipient(s), comma-separated. A request body to overrides it. |
— |
| Name | Example | What it does |
|---|---|---|
SENTRY_DSN |
https://<key>@o000000.ingest.sentry.io/0000000 |
Also sends server errors to Sentry (plain fetch, 3 s timeout, never fails a request). Without it errors are still logged as one JSON line in Vercel Logs. From Sentry → Project Settings → Client Keys (DSN). Not highly secret, but keep it private to avoid spam events. |
See DEMO.md.
| Name | Example | What it does | Security |
|---|---|---|---|
DEMO_MODE |
true (only on a demo instance) |
Turns the instance into a public demo: shows APP_USERNAME/APP_PASSWORD on the login page with one-click sign-in, ignores 2FA, disables AI/uploads/import/restore/settings/n8n routes, caps rows and rate-limits writes. Any other value = off. |
Never on an instance with real data — it publishes the login. Use a separate, throwaway Supabase project. |
PUBLIC_DEMO_URL |
https://demo.dompetku.ilramdhan.dev |
On your main instance: shows a Coba demo button (navbar, hero, final CTA) linking to a public demo. Must be https://; empty = hidden. |
Not secret. |
Only the public pages (/, /privacy, /terms) are indexable; the app, the login page and every page of a DEMO_MODE instance send noindex, nofollow. /robots.txt is generated by the app (blocks /api/ and app paths; Disallow: / in demo mode). Each public page is server-rendered in Indonesian by default and in English with ?lang=en (e.g. /?lang=en), so both languages are crawlable. /llms.txt gives AI assistants a short project overview with links (404 in demo mode).
| Name | Example | What it does | Security |
|---|---|---|---|
PUBLIC_SITE_URL |
https://dompetku.example.com |
Your instance's public address (http/https, no query). Adds <link rel="canonical">, og:url, absolute og:image/twitter:image, serves /sitemap.xml (both languages with xhtml:link alternates), hreflang alternates (id, en, x-default) and a Sitemap: line in /robots.txt. Empty = none of those (sitemap 404). |
Not secret. |
GOOGLE_SITE_VERIFICATION |
AbC123… |
Google Search Console "HTML tag" token → <meta name="google-site-verification"> on the landing page (/) only. A pasted full <meta> tag is unwrapped. Ignored on a DEMO_MODE instance. |
Not secret (public in the page). |
BING_SITE_VERIFICATION |
0123ABCD… |
Bing Webmaster Tools token → <meta name="msvalidate.01"> on the landing page only. Ignored on a DEMO_MODE instance. |
Not secret (public in the page). |
| Name | Where | What it does |
|---|---|---|
DEMO_RESET_CONFIRM |
GitHub Actions / shell | Must be yes (plus --allow-remote) before scripts/seed-demo.mjs touches a non-local database. Set by demo-reset.yml. |
SUPABASE_PROJECT_ID |
local shell / .env |
Project ref (the abcdefghijkl in your Supabase URL) for bun run gen:types. Not used by the app; don't set it on Vercel. |
| Name | Set by | Used for |
|---|---|---|
VERCEL |
Vercel builds | vite.config.ts switches the build target to Vercel when present. |
VERCEL_ENV |
Vercel | environment tag on Sentry events. |
VERCEL_GIT_COMMIT_SHA |
Vercel | release tag on Sentry events. |
NODE_ENV |
runtime | Fallback environment tag for Sentry. |
These live in the n8n environment (not Vercel) and are read by the workflow templates in n8n/ via $env.…. n8n must allow it: N8N_BLOCK_ENV_ACCESS_IN_NODE=false. Full setup: N8N.md.
| Name | Used by | Example | What it does |
|---|---|---|---|
FINTRACK_URL |
01, 02, 05 | https://your-app.vercel.app |
Base URL of your Dompetku deployment (no trailing slash). |
TELEGRAM_BOT_TOKEN |
01, 02, 03, 04 | <token from @BotFather> |
Telegram Bot API token. Secret. (The app reads its own TELEGRAM_BOT_TOKEN only for direct mode.) |
TELEGRAM_ALLOWED_CHAT_IDS |
01 | 123456789 |
Comma-separated chat IDs n8n forwards to the app (first filter; the app re-checks BOT_ALLOWED_CHAT_IDS). |
TELEGRAM_BOT_USERNAME |
01 | chatbotfinance |
Optional. Bot username without @; makes group mention/reply matching exact. Without it any @…bot mention counts. |
TELEGRAM_ADMIN_CHAT_ID |
02, 03 | 123456789 |
Chat that receives scheduled reminders/reports and workflow error alerts. |
BACKUP_EMAIL_FROM |
05 (email variant) | noreply@mail.example.com |
Sender for emailed backups. |
BACKUP_EMAIL_TO |
05 (email variant) | you@example.com |
Recipient for emailed backups. |
The N8N_API_KEY value is not an n8n environment variable — it goes into an n8n Header Auth credential (Name x-api-key).