Configuration

The variables in .env.example, grouped by what they do, including both authentication modes and the settings that control who can register.

View Markdown
Browse documentation
On this page

Everything is configured with environment variables. pnpm run setup creates .env from .env.example when it is missing. Edit the resulting file privately. The file in the repository is the source of truth and has the same notes inline. Keep secrets in a secret manager or an untracked env file, not in version control.

Database

Variable Default Purpose
DATABASE_URL postgresql://postgres:postgres@localhost:5432/finance Connection string for any standard PostgreSQL. docker compose up -d starts one that matches this default.

In production, connect as a dedicated non-superuser role without BYPASSRLS. Superusers skip row level security, and row level security is what keeps companies apart at the database level.

Authentication

There are two modes. AUTH_MODE picks one, and Balsa won't silently fall back to the other.

Better Auth (default)

AUTH_MODE=better-auth gives you email and password sign-in. Use it for anything reachable over a network and for any install with more than one person.

Variable Purpose
AUTH_MODE better-auth for email and password sign-in.
BETTER_AUTH_SECRET Secret for signing sessions. At least 32 random bytes; openssl rand -base64 32 works.
BETTER_AUTH_URL The app's canonical URL, such as http://localhost:3000 or your HTTPS origin.
BETTER_AUTH_TRUSTED_ORIGINS Optional. Extra allowed origins, comma separated. This is the only way to add one.
AUTH_ALLOW_SIGN_UP Optional. true opens registration; it's off in production unless you set it. Turn it on while people register, then remove it.
AUTH_SIGN_UP_CODE Optional, but set it whenever sign-up is on and other people can reach the server. Registrations without the code are refused on the server.

The sign-up code is the only thing standing between the public and your install. A server-side hook checks it in constant time, regardless of what the sign-in page does. It also protects LOCAL_OWNER_EMAIL: the first Better Auth account registered with that address takes over the existing local owner and their data. If sign-up is open with no code, whoever registers that address first gets that data. In production, Balsa logs a warning at startup if sign-up is on without a code.

Balsa doesn't send email, so there are no invitations or password-reset emails. The code controls who can register, and anyone registered can work in the install.

Disabled

AUTH_MODE=disabled removes the sign-in screen. It's meant for one person on a machine they trust.

Variable Purpose
AUTH_MODE disabled for a single-user local install with no sign-in.
LOCAL_OWNER_EMAIL The owner's email. In disabled mode it's the identity everything runs as; in Better Auth mode, the account registered with it owns the original company and its data.
LOCAL_OWNER_NAME Optional display name. Defaults to User.
LOCAL_OWNER_ID Optional stable id. Defaults to local-owner.
ALLOW_AUTH_DISABLED Must be true to run disabled mode in production.

Disabled mode offers no protection. Put it behind a VPN, a reverse proxy, or some other network access control, and don't expose it to the internet.

Because LOCAL_OWNER_EMAIL identifies the owner in both modes, you can start out privately with auth disabled and later switch the same install, with the same data, to Better Auth.

Model provider

Variable Default Purpose
MODEL_PROVIDER openrouter openai or openrouter.
MODEL_NAME auto Default model, shown in the composer as Auto.
OPENAI_API_KEY none Required when the provider is openai.
OPENROUTER_API_KEY none Required when the provider is openrouter.
MODEL_OPTIONS none Optional. Extra models for the composer, as comma-separated Label=id pairs. Auto always comes first.

Application

Variable Purpose
APP_SECRET Application signing secret.
CRON_SECRET Bearer token for the scheduled reconciliation and calculation endpoints, checked in constant time. Set it before turning those jobs on.
HEALTHCHECK_SECRET Bearer token for the health endpoint. Without it, production health checks return 404.
DEFAULT_COMPANY_NAME Name for the company created on first setup. It doesn't affect companies added later from the account menu, and it's ignored if the owner has a COMPANIES list.
COMPANIES Companies to create for the owner. See below.

COMPANIES

A comma-separated list of Name or Name:CUR entries. Currency defaults to USD.

COMPANIES=Acme Holdings:USD,Beta GmbH:EUR

The companies are created on the owner's first sign-in, and the first one is the default. Names added later are created at the next sign-in. Editing or removing an entry won't rename or delete anything. New company in the account menu does the same job by hand. See Companies and users.

File storage

Variable Default Purpose
FILE_STORAGE local Where uploaded source files are stored.
FILE_STORAGE_PATH .data/files Directory for local files. Each company gets its own prefix.

Keep this directory out of any web root, lock down its permissions, and back it up the way you would any financial records.

Variable Purpose
NEXT_PUBLIC_SITE_URL Optional. URL of a public docs site. If set, the app header shows a How Balsa works link to it; if not, there's no link.

This optional setting links the application to its documentation. Database and model-provider settings can also point at external services. Self-hosted storage does not imply that model interpretation runs locally.

Check configuration

Run pnpm run doctor after changes. It checks the selected provider, required secrets, database, migrations and storage without printing secret values. Restart the application after changing server configuration; rebuild when changing public build-time values.

The full Docker Compose profile overrides the app container's database connection, file-storage path and Better Auth URL for its bundled local services. Review the Compose configuration as well as .env before adapting it to a different host. See Operating an installation.