---
title: Configuration
section: Install and operate
order: 40
summary: The variables in .env.example, grouped by what they do, including both authentication modes and the settings that control who can register.
---

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.

```bash
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](/docs/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.

## Link to this site

| 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](/docs/operations/).
