# Environment variables

Lalabase is configured through environment variables — in the container installation via
the `.env` file.

## Required variables

```bash
# Base
RAILS_ENV=production
SECRET_KEY_BASE=<generated via bin/rails secret>

# Database
DATABASE_URL=postgres://lalabase:password@db:5432/lalabase_production

# Cache
REDIS_URL=redis://redis:6379/0

# Encryption of stored credentials (AI keys, repository tokens)
# Generate with: bin/rails db:encryption:init
ACTIVE_RECORD_ENCRYPTION_PRIMARY_KEY=<generated>
ACTIVE_RECORD_ENCRYPTION_DETERMINISTIC_KEY=<generated>
ACTIVE_RECORD_ENCRYPTION_KEY_DERIVATION_SALT=<generated>
```

<div class="docs-callout docs-callout--warning">
  <div class="docs-callout__title">Important</div>
  <p>Without the three <code>ACTIVE_RECORD_ENCRYPTION_*</code> variables Lalabase refuses to start in production. Store the values outside the server as well (e.g. in your password manager): a database backup is only partially readable without them — the encrypted columns remain ciphertext.</p>
</div>

## Operator and legal pages

The operator and the legal pages are not environment variables. After your first sign-in,
enter them in the admin area under **System Settings → Operator and legal pages**: the
operator name and the addresses of your imprint, privacy policy and terms.

Lalabase shows them in the footer of the sign-in page and the pages without sign-in, in
the consent when someone accepts an invitation, when AI is switched on and in the data
export. With an operator, the footer shows `© <year> <name>`; without one, the plain text
`powered by Lalabase`. An empty address field shows no link. Every address must be
complete and start with `https://` or `http://`, otherwise the form saves nothing.

**If you have no website of your own**, write the text itself instead: there is a text box
under each address field. Lalabase then serves the page, at `/legal/imprint`,
`/legal/privacy` and `/legal/terms`, and links it in the same places. Markdown is allowed
(headings, lists, links); embedded HTML is stripped, because the page is publicly
reachable.

An address **takes precedence** over the text. Two copies of the same privacy policy
eventually start to differ.

With both empty there is no page and no link. For an instance only your own team uses,
that is the normal case: the duty to publish an imprint begins when you offer the service
publicly.

## Registration through your own website (optional)

If your sign-up page runs as a separate program, it can create accounts through an endpoint.
Without this variable the endpoint does not exist: it answers like any unknown address. Self-hosted
instances rarely need it, because there you create users in the admin area or invite them.

```bash
# Generate with: openssl rand -hex 32
LALABASE_PROVISIONING_SECRET=<generated>

# Only during a rotation: the previous secret stays valid as well
LALABASE_PROVISIONING_SECRET_PREVIOUS=<previous value>
```

With the secret set, the endpoint also answers with a one-time link the website uses to sign the
new user in, so they never meet the sign-in form right after choosing a password.

The secret needs at least 64 characters. A shorter one keeps the endpoint shut, and Lalabase
writes a note to the log at boot: otherwise the symptom looks exactly like "not configured at
all". Rotating without a gap: set the new secret, move the old one to `_PREVIOUS`, switch the
website over, then drop `_PREVIOUS`.

So that `/users/sign_up` no longer leads anyone to this instance own form, add the address
of your sign-up page as well:

```bash
LALABASE_SIGNUP_URL=https://your-site.example/signup
```

Without this variable nothing changes, which is the normal case for a self-hosted instance.
With it, Lalabase redirects `/users/sign_up` there, temporarily (302), so removing the
variable removes the redirect.

Two things deliberately stay here: **invited guests** and **submitting** the form. An
invitation link carries a token that another sign-up page cannot redeem; it would create a
new organisation instead of following the invitation. And a form somebody still has open
should submit rather than lose what they typed.

The address must be complete and start with `https://` or `http://`. Any other value is
ignored, Lalabase writes a note to the log at boot, and the own form keeps answering.

Point it at an entry **without a language prefix** where you can: which language a visitor
gets is then decided by the page that knows its own addresses.

## AI keys (BYOK)

Lalabase follows the **bring-your-own-key** principle: you provide your own provider key,
so no data ever flows through third-party accounts.

```bash
OPENAI_API_KEY=sk-...
```

<div class="docs-callout docs-callout--tip">
  <div class="docs-callout__title">Tip</div>
  <p>Keep secrets out of version control. The <code>.env</code> belongs in <code>.gitignore</code>.</p>
</div>

## Email

To send notification emails, provide your SMTP credentials:

```bash
SMTP_ADDRESS=smtp.example.com
SMTP_PORT=587
SMTP_USERNAME=lalabase@example.com
SMTP_PASSWORD=<your-password>
```

**Without working mail delivery nobody can sign up.** Everyone who registers
receives a confirmation email and has three days to click the link in it. After
that, access is blocked until the address is confirmed — if the mail never
arrives, there is no way forward. Verify delivery right after installation with a
test registration.

Two groups are unaffected: users you create yourself in the admin area, and people
who join through an invitation link. Both count as confirmed immediately and
receive no confirmation email.

### Mail authentication (SPF, DKIM, DMARC)

Correct SMTP credentials only get your mail **sent**. Whether it **arrives** is
decided elsewhere: by three DNS records on your domain. Without them, Gmail and
Outlook file the confirmation mail as spam or drop it silently. The registration
then looks like a bug in Lalabase, even though delivery worked perfectly.

| Record | What it does |
|---|---|
| **SPF** | Lists the servers allowed to send in your name |
| **DKIM** | Cryptographically signs every outgoing mail |
| **DMARC** | Tells recipients what to do when a check fails — and sends you reports about it |

You create them at your domain's DNS provider, not in Lalabase. Most mail hosts
offer a form that generates the right record for you.

**The order matters, or you will lock yourself out:**

1. **Enable DKIM.** No risk — a missing signature can never deliver a mail, a present
   one can only help. Then verify the key really is in DNS yourself:
   `dig TXT <selector>._domainkey.<your-domain>`. Some hosts' status displays lag
   behind reality.
2. **Set SPF**, at first with the neutral `?all` ending. Only include servers that
   actually deliver. The machine running Lalabase usually does **not** qualify: the
   app hands its mail to the SMTP server from `SMTP_ADDRESS`, so it never faces the
   recipient itself. A needless authorization becomes a way in for mail sent in your
   name should that machine ever be compromised.
3. **Add DMARC** with `p=none` and a `rua=` address. `p=none` changes nothing about
   delivery but switches the reports on. **Without `rua=` the record is pointless** —
   you get no feedback and never learn whether your records work. Keep the address on
   the same domain, otherwise the specification requires an extra authorization record
   over there.
4. **Measure.** Send yourself a test mail to a mailbox at a major provider and read the
   headers (in Gmail: "Show original"). It says:

   ```
   Authentication-Results: ... spf=pass ... dkim=pass ... dmarc=pass
   ```

5. **Only once `spf=pass` shows up** should you move the SPF ending from `?all` to
   `~all`. Doing it earlier is harmful: `?all` means "no statement", while `~all` and
   `-all` mean "suspicious" and "reject". Tighten before your real sending path is
   authorized and you reject your own mail.

After a few weeks of clean DMARC reports you can raise `p=none` to `p=quarantine` and
later `p=reject`. Never start at `reject`.

<div class="docs-callout docs-callout--tip">
  <div class="docs-callout__title">Tip</div>
  <p>Leave forensic DMARC reports (<code>ruf=</code>) empty. They contain full copies of other people's failed mail, and the large providers do not send them anyway.</p>
</div>
