Ctrl K
ComunidadGuías…

docs/configuration.md

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
# Configuration

Everything is read from the environment (`@nestjs/config`). `.env.example` at
the repository root is the annotated template; this page is the reference.

## Required

| Variable                 | What it is                                                                                                                                                                                                                                                                                  |
| ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `DATABASE_URL`           | PostgreSQL DSN, e.g. `postgres://mynextscreen:secret@localhost:5432/mynextscreen`                                                                                                                                                                                                           |
| `JWT_ACCESS_SECRET`      | Signing secret for access tokens. **Minimum 32 characters** — the backend refuses to start with anything shorter. Generate with `openssl rand -base64 48`.                                                                                                                                  |
| `SECRETS_ENCRYPTION_KEY` | Encrypts every stored secret (per-org SMTP passwords, ntfy tokens, TV devmode passphrases, screen pairing keys) with AES-256-GCM. **Exactly 32 bytes, base64** — the backend refuses to start without it. Generate with `openssl rand -base64 32`. See [Secrets at rest](#secrets-at-rest). |

## Required in production

| Variable          | What happens without it                                                                                                             |
| ----------------- | ----------------------------------------------------------------------------------------------------------------------------------- |
| `PUBLIC_BASE_URL` | CORS fails closed rather than reflecting whatever origin asks. Also the base for links in account emails.                           |
| `PLAYER_BASE_URL` | The player's origin is not allow-listed for CORS, and the add-screen hint cannot tell operators which address to open on a display. |

## Secrets at rest

No secret is ever stored in plaintext. Values the backend only has to compare
(passwords, reset/verify tokens, refresh tokens, screen API keys, agent tokens)
are hashed. Values it has to hand on again (SMTP passwords, ntfy tokens, TV
devmode passphrases, the API key parked for a pairing screen) are encrypted
with `SECRETS_ENCRYPTION_KEY`, and a `CHECK` constraint on each of those columns
makes PostgreSQL itself reject anything that is not encrypted.

**Upgrading from a release where the key was optional:** set the key and
deploy. On its first start the backend encrypts every value an older release
stored in plaintext, then validates the constraints, before it serves a single
request. Nothing else to do; the step is safe to repeat. Stop the old backend
before starting the new one — do not run both versions side by side across this
upgrade. If the key does not match the one the stored secrets were encrypted
with, the backend refuses to start and says so.

**Back the key up separately from database dumps.** A dump only contains
ciphertext; without the key those secrets are gone and have to be re-entered.
Do not rotate the key by simply replacing it — the backend would no longer be
able to read the stored values.

## Two-factor authentication (MFA)

Users can protect their account with a TOTP authenticator app (SHA-1, six
digits, 30-second steps — what every common app supports). There is nothing to
configure in the environment; the TOTP secret is one more value encrypted with
`SECRETS_ENCRYPTION_KEY`, and the ten single-use recovery codes are stored as
hashes only.

**Who has to use it.** MFA belongs to the user, not to an organisation. It is
mandatory for a user who is a super-admin, or who belongs to at least one
organisation that requires it — and then it applies to every organisation that
user belongs to. Everybody else may set it up voluntarily under
_Settings → Security_. A user for whom MFA is mandatory and who has not set it
up yet can still sign in with the password, but only reaches the set-up wizard
until it is done; nobody is locked out by switching the requirement on.

**Requiring it for an organisation.** An organisation admin switches it on in
the organisation settings. Only an admin whose own session is MFA-confirmed can
switch it on, so nobody locks themselves out. Members without MFA get an email
(at most once a day per organisation) and are taken through the set-up on their
next request. Switching it off is always possible.

**New or lost device.** Moving the factor to a new phone under
_User settings → Security_ needs the password and a code from the current
authenticator — or, if that is gone, one of the recovery codes (_Use a
recovery code_). The same goes for switching optional MFA off and for issuing
new recovery codes. Each recovery code works once, and the user is emailed
whenever one is used. A user who has neither the authenticator nor a recovery
code left needs an admin to reset the second factor, which also ends all of
that user's sessions and emails them:

- an organisation admin, from the member list, for members of their
  organisation — not for super-admins, not for themselves, and not for users
  who also belong to an organisation the admin does not administer (the reset
  applies everywhere, so a super-admin has to do it then);
- a super-admin, from the user list, for anyone but themselves.

After a reset a user for whom MFA is mandatory sets it up again at the next
sign-in. Every reset is written to the audit log of each of the user's
organisations.

**Upgrading to the first release with MFA:** nothing to configure beyond the
mandatory `SECRETS_ENCRYPTION_KEY` (see above). Super-admins are taken into the
set-up wizard on their next request — including sessions that were open during
the upgrade — and cannot use the dashboard until they have finished it. Have
an authenticator app ready before deploying.

Ten wrong codes for one user within 15 minutes block further attempts for the
rest of that window (`429`).

### Emergency: the only super-admin is locked out

If a super-admin has lost both the authenticator and the recovery codes and
there is no second super-admin who could reset them, remove the second factor
directly in the database. With the Ansible deployment (rootless Podman), as the
deploy user on the server — replace the email address:

```bash
podman exec -i mynextscreen-postgres psql -U mynextscreen -d mynextscreen <<'SQL'
BEGIN;
DELETE FROM user_mfa_recovery_codes
  WHERE user_id = (SELECT id FROM users WHERE email = lower('admin@example.org'));
DELETE FROM user_mfa
  WHERE user_id = (SELECT id FROM users WHERE email = lower('admin@example.org'));
COMMIT;
SQL
```

The super-admin then signs in with the password and is taken straight into the
set-up wizard; MFA stays mandatory. Be aware of what this path does not do:
it writes **no audit entry**, and it does not end sessions that are already
open. Anyone with shell access to the database host can do this, which is one
more reason to keep that access as narrow as the dashboard's.

## Storage and media

| Variable                            | Default                 | What it is                                                                                                         |
| ----------------------------------- | ----------------------- | ------------------------------------------------------------------------------------------------------------------ |
| `MEDIA_BASE_PATH`                   | `./media`               | Root for originals and transcoded files. Layout: `{base}/{organisationId}/originals\|transcoded/{contentId}.{ext}` |
| `MAX_FILE_SIZE_BYTES`               | —                       | Upload limit                                                                                                       |
| `FFMPEG_PATH`                       | system `PATH`           | FFmpeg binary                                                                                                      |
| `FFMPEG_VIDEO_CRF`                  | `18`                    | 0 lossless, 18 visually lossless, 23 default, 28 low                                                               |
| `FFMPEG_VIDEO_PRESET`               | `slow`                  | `ultrafast` … `veryslow`                                                                                           |
| `FFMPEG_VIDEO_MAXRATE` / `_BUFSIZE` | `8M` / `16M`            | Bitrate cap and rate-control buffer                                                                                |
| `MAX_CONCURRENT_LIVE_STREAMS`       | `4`                     | Simultaneous live encoders. Each one can saturate a core.                                                          |
| `HLS_OUTPUT_DIR`                    | `/tmp/mynextscreen-hls` | Where live segments are written                                                                                    |

Per-organisation storage limits (original and transcoded, counted separately)
are set in the dashboard, not here.

## Sessions and cookies

| Variable          | Default                                | What it is                                |
| ----------------- | -------------------------------------- | ----------------------------------------- |
| `REDIS_URL`       | `redis://localhost:6379`               | Transcoding queue and user refresh tokens |
| `JWT_ACCESS_TTL`  | `15m`                                  | Access-token lifetime                     |
| `JWT_REFRESH_TTL` | `30d`                                  | User refresh-token lifetime               |
| `COOKIE_SECURE`   | `true` only when `NODE_ENV=production` | Set explicitly to override                |
| `COOKIE_SAMESITE` | `strict`                               | `strict` \| `lax` \| `none`               |

Screen sessions are not configurable: a screen holds a 15-minute access token
and a refresh token that rotates on every use and lives 180 days in PostgreSQL.

## Mail

Account mail — verification, invitations, password resets, email changes — goes
through these. Per-organisation SMTP for _notifications_ is configured in the
dashboard and is separate.

| Variable                      | Default                                     | What it is                                                    |
| ----------------------------- | ------------------------------------------- | ------------------------------------------------------------- |
| `SMTP_HOST` / `SMTP_PORT`     | `localhost` / `1025`                        | In development these point at Mailpit                         |
| `SMTP_USER` / `SMTP_PASSWORD` | —                                           | Authentication is skipped when both are empty                 |
| `SMTP_FROM`                   | `myNextScreen <noreply@mynextscreen.local>` | Envelope sender                                               |
| `SMTP_SECURE`                 | `false`                                     | `true` for implicit TLS on 465; `false` for STARTTLS or plain |

## Sign-up

| Variable                                  | Default | What it is                                                                                               |
| ----------------------------------------- | ------- | -------------------------------------------------------------------------------------------------------- |
| `SIGNUP_ENABLED`                          | `true`  | `false` rejects public registration with 403 and hides it in the UI                                      |
| `SIGNUP_DEFAULT_STORAGE_ORIGINAL_BYTES`   | 5 GiB   | Limit granted to a self-created organisation                                                             |
| `SIGNUP_DEFAULT_STORAGE_TRANSCODED_BYTES` | 5 GiB   | As above, counted separately                                                                             |
| `SIGNUP_UNVERIFIED_TTL_HOURS`             | `24`    | How long a never-verified sign-up survives before an hourly job deletes it and its orphaned organisation |

There is no environment variable that creates the first super-admin. On an empty
database the dashboard offers a one-time setup screen instead, and that route
closes as soon as a user exists.

## Hardening

| Variable                          | Default                  | What it is                                                                                                                                                                                                                                                                              |
| --------------------------------- | ------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `OUTBOUND_ALLOWED_HOSTS`          | —                        | Comma-separated exceptions to the SSRF guard. Organisation-configurable targets — ntfy URL, per-org SMTP host, live-stream source — may not resolve to loopback, private, link-local or CGNAT addresses unless listed here. Needed for an internal relay, e.g. `ntfy.lan,192.168.1.50`. |
| `SITE_AGENT_OFFLINE_THRESHOLD_MS` | `180000`                 | How long without a heartbeat before a site agent counts as offline. Deliberately longer than the screens' threshold: an agent missing one check-in matters far less than a display going dark, and a venue uplink that blips should not paint the whole site red.                       |
| `SITE_AGENT_APP_ID`               | `com.mynextscreen.webos` | The webOS application a site agent keeps in the foreground.                                                                                                                                                                                                                             |

One more knob lives in the player rather than the environment:
`window.__SIGNAGE_TRUSTED_ORIGINS__` in `apps/player/src/index.html` decides
which origins may hand a player its server URL over `postMessage`. Deployments
that do not use the LG webOS shell should pin it — see the comment in that file.

## Observability

| Variable               | Default | What it is                                                                                                                                                                                    |
| ---------------------- | ------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `METRICS_SCRAPE_TOKEN` | —       | Bearer token Prometheus must present to scrape `GET /api/metrics`. **Unset disables the endpoint (404), never exposes it.** `openssl rand -base64 32`. See [Observability](observability.md). |