# Configuration reference

> Every runtime setting — hardened profiles, database, application values, file-based secrets, Canvas/LTI endpoints, SEB options, and secret rotation.

[Canonical HTML page](https://safeonlineexam.com/docs/deploy/configuration)

The application reads its configuration from process environment variables (or
mounted secret files). It does **not** load `.env` files itself — Compose loads an
env file explicitly, and Cloud Run injects values. Protect any local production
file with `chmod 600` and keep it out of version control.

## Profiles and validation [#profiles-and-validation]

The active profile is the first non-empty value among `APP_ENV`, `NODE_ENV`, and
the alias `SPRING_PROFILES_ACTIVE`:

| Resolved value          | Result                                          |
| ----------------------- | ----------------------------------------------- |
| `prod` / `production`   | Production profile and **hardened** validation. |
| `test`                  | Test profile; in-memory repositories.           |
| Anything else, or unset | Development profile.                            |

<Callout type="warn" title="Cloud Run is always hardened">
  Cloud Run is treated as hardened even for an isolated `APP_ENV=dev` service.
  Hardened validation requires real database, Canvas, LTI, OAuth, secret, and URL
  values; it also requires a valid certificate when certificate encryption is
  enabled. **Startup fails before listening** if configuration is unsafe or
  incomplete. That fail-fast is intentional — a misconfigured exam tool should not
  accept traffic.
</Callout>

## PostgreSQL [#postgresql]

PostgreSQL 17+ is the supported durable store. The app uses ordinary PostgreSQL
protocol settings and is not tied to a managed provider.

| Variable                         | Default      | Notes                                                                             |
| -------------------------------- | ------------ | --------------------------------------------------------------------------------- |
| `DATABASE_HOST`                  | `127.0.0.1`  | Host, IP, or Unix socket directory. Required when hardened.                       |
| `DATABASE_PORT`                  | `5432`       | 1–65535. Stays `5432` for Cloud SQL sockets.                                      |
| `DATABASE_NAME`                  | `canvas_seb` | Dedicated application database. Required when hardened.                           |
| `DATABASE_USER`                  | `canvas_seb` | Application role. Required when hardened.                                         |
| `DATABASE_PASSWORD`              | —            | Required when hardened. Prefer `DATABASE_PASSWORD_FILE`.                          |
| `DATABASE_PASSWORD_FILE`         | —            | Readable file with the password. Conflicts with the direct variable.              |
| `DATABASE_SSL_MODE`              | `disable`    | `disable`, `require`, `verify-ca`, `verify-full`. Verify over untrusted networks. |
| `DATABASE_POOL_MAX`              | `5`          | Per-process max, 1–100. Size the DB for pool max × instances/jobs.                |
| `DATABASE_CONNECTION_TIMEOUT_MS` | `10000`      | 100–120000 ms.                                                                    |
| `DATABASE_STATEMENT_TIMEOUT_MS`  | `30000`      | 100–600000 ms.                                                                    |
| `DATABASE_CLEANUP_BATCH_SIZE`    | `500`        | 1–10000. Read by the cleanup command, not normal startup.                         |

Cloud Run connects to Cloud SQL with
`DATABASE_HOST=/cloudsql/PROJECT:REGION:INSTANCE` and `DATABASE_SSL_MODE=disable`
(the authenticated socket is local to the sandbox). An external managed PostgreSQL
should normally use `verify-full` with a trusted chain; for a private CA, mount
the CA file and set `NODE_EXTRA_CA_CERTS` before startup.

Run `npm run db:migrate` (or the migration job/service) before a new revision, and
`npm run db:cleanup` on a schedule. `/ready` fails until every checked-in
migration is applied.

## Required application values [#required-application-values]

| Variable                             | Purpose                                      | Hardened requirement                                                                                           |
| ------------------------------------ | -------------------------------------------- | -------------------------------------------------------------------------------------------------------------- |
| `NODE_ENV`                           | Node runtime mode.                           | `production` for the deployed image.                                                                           |
| `APP_ENV`                            | Application profile.                         | `dev` (isolated non-prod) or `prod`.                                                                           |
| `PORT`                               | HTTP port.                                   | Defaults to `8080`; platforms may inject it.                                                                   |
| `TOOL_URL`                           | Public origin of this deployment.            | HTTPS origin only; no path/query/credentials.                                                                  |
| `CANVAS_DOMAIN`                      | Connected Canvas origin.                     | HTTPS origin only.                                                                                             |
| `LTI_CLIENT_ID`                      | LTI 1.3 Developer Key client ID.             | Required.                                                                                                      |
| `LTI_PRIVATE_KEY`                    | RSA private JWK for tool signing.            | RSA 2048+ bits, exponent 65537, RS256.                                                                         |
| `LTI_DEPLOYMENT_ID_CHECKING_ENABLED` | Enforce the deployment-ID allowlist.         | Defaults `true`; `false` only for controlled self-service.                                                     |
| `LTI_DEPLOYMENT_ID`                  | Installed External App deployment ID.        | Required when checking is enabled; comma/newline allowlist.                                                    |
| `CANVAS_API_CLIENT_ID`               | Canvas API OAuth client ID.                  | Required and **distinct** from the LTI key.                                                                    |
| `CANVAS_API_CLIENT_SECRET`           | Canvas API OAuth secret.                     | Required secret.                                                                                               |
| `SESSION_SECRET`                     | Express session signing secret.              | ≥32 chars, different from state encryption.                                                                    |
| `STATE_ENCRYPTION_KEY`               | AES-GCM material for opaque LTI/OAuth state. | ≥32 chars, different from session signing.                                                                     |
| `SEB_CONFIG_ENCRYPTION_CERT_PEM`     | Public X.509 cert used to encrypt `.seb`.    | Required when certificate encryption is enabled; valid end-entity RSA cert whose Key Usage permits encryption. |

`SEB_CONFIG_ENCRYPTION_CERT_PATH` is the file alternative for the public
certificate. The matching **private** key is never a server input. When
encryption is disabled, neither certificate input is required and the public
certificate endpoints return `404`.

<Callout type="warn" title="Two keys, never mixed">
  `LTI_CLIENT_ID` (LTI 1.3 Developer Key) and `CANVAS_API_CLIENT_ID` (API OAuth
  Developer Key) come from **different** Canvas registrations. Mixing them breaks
  either signed launches or Canvas API authorization. Likewise, `SESSION_SECRET` and
  `STATE_ENCRYPTION_KEY` must be different values.
</Callout>

## Deployment-ID policy [#deployment-id-policy]

`LTI_DEPLOYMENT_ID_CHECKING_ENABLED=true` (default) restricts launches to the IDs
in `LTI_DEPLOYMENT_ID`. Set it to `false` only when the configured Canvas issuer
and LTI client ID are intentionally trusted to create course-level installs.
Disabled mode still requires Canvas's signed deployment-ID claim and retains token
signature, issuer, audience, nonce, target-link, and browser/state validation — it
removes only the preconfigured allowlist.

## File-based secrets [#file-based-secrets]

These values accept a mutually exclusive `_FILE` alternative, read once at startup:

* `DATABASE_PASSWORD_FILE`
* `LTI_PRIVATE_KEY_FILE`
* `CANVAS_API_CLIENT_SECRET_FILE`
* `SESSION_SECRET_FILE`
* `STATE_ENCRYPTION_KEY_FILE`
* `SEB_QUIT_PASSWORD_FILE`

Setting both a direct value and its `_FILE` form is rejected. Docker/Kubernetes
secret mounts work without provider-specific SDKs. The checked-in
`compose.secrets.yaml` override wires these file paths into the app, migration, and
cleanup processes and supplies PostgreSQL its own `POSTGRES_PASSWORD_FILE`.

## Canvas and LTI endpoints [#canvas-and-lti-endpoints]

Canvas cloud defaults are built in. Override the authorization and JWKS URLs for a
Canvas with different LTI endpoints. `LTI_ISSUER` must equal the `iss` in that
Canvas's launch — &#x2A;*do not assume it equals `CANVAS_DOMAIN`.**

| Variable              | Default                                                | Notes                                                     |
| --------------------- | ------------------------------------------------------ | --------------------------------------------------------- |
| `LTI_ISSUER`          | `https://canvas.instructure.com`                       | HTTPS issuer URL.                                         |
| `LTI_KEY_SET_URL`     | `https://sso.canvaslms.com/api/lti/security/jwks`      | Canvas platform JWKS endpoint.                            |
| `LTI_AUTH_URL`        | `https://sso.canvaslms.com/api/lti/authorize_redirect` | Canvas OIDC authorization endpoint.                       |
| `CANVAS_API_BASE_URL` | `${CANVAS_DOMAIN}/api/v1`                              | Must match the configured Canvas origin when hardened.    |
| `CANVAS_REDIRECT_URI` | `${TOOL_URL}/api/oauth2callback`                       | If supplied, must equal the exact callback when hardened. |

See [Self-hosted Canvas](/docs/canvas/self-hosted-canvas) for the override
procedure and the `sso.canvaslms.com` failure it prevents.

## SEB and diagnostics [#seb-and-diagnostics]

| Variable                                | Default   | Notes                                                                                                                                                                        |
| --------------------------------------- | --------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `SEB_QUIT_PASSWORD`                     | —         | Optional managed exit-password fallback; must pass password policy.                                                                                                          |
| `SEB_REQUIRED_DOMAINS`                  | empty     | Concrete reviewed hostnames; wildcards and IdP hosts rejected.                                                                                                               |
| `SEB_CONFIG_ENCRYPTION_ENABLED`         | `true`    | Set explicitly to `false` only when managed client-certificate distribution is not possible. The configuration is then plaintext unless an instructor sets a start password. |
| `SEB_CONFIG_ENCRYPTION_CERT_PATH`       | —         | Public certificate path (used by the Compose secret mount).                                                                                                                  |
| `SEB_CONFIG_ENCRYPTION_PUBLIC_KEY_PEM`  | —         | Local-dev fallback; insufficient when hardened.                                                                                                                              |
| `SEB_CONFIG_ENCRYPTION_PUBLIC_KEY_PATH` | —         | File form of the local public-key fallback.                                                                                                                                  |
| `HOST`                                  | `0.0.0.0` | Bind address.                                                                                                                                                                |
| `USE_IN_MEMORY_STORE`                   | `false`   | Local/test only; hardened runtimes reject it.                                                                                                                                |
| `APP_DEBUG_ENABLED`                     | `false`   | Hardened runtimes reject `true`.                                                                                                                                             |
| `APP_DETECTOR_DIAGNOSTICS_ENABLED`      | `false`   | Sanitized detector detail; production rejects `true`.                                                                                                                        |
| `APP_ASSET_VERSION`                     | —         | Optional client cache version; `K_REVISION` used when present.                                                                                                               |

Generated configurations use a Safari-compatible browser user agent on macOS so
Google Sheets selects its full-resolution canvas path; SEB appends its own marker.
Windows keeps its native Chromium-based SEB user agent.

<Callout type="warn" title="Certificate encryption is the default, not an incidental option">
  With `SEB_CONFIG_ENCRYPTION_ENABLED=true`, a hardened runtime requires a valid
  public X.509 certificate. Setting it to `false` preserves Config Key proof,
  one-time configuration grants, Canvas session handoff, URL filtering, and the SEB
  lockdown policy, but removes the device-specific protection on the downloaded
  file. Document and approve that compatibility posture; do not use it to work
  around a certificate or device-management incident.
</Callout>

## Secret rotation [#secret-rotation]

Rotate one secret at a time. Create a new immutable version, point the runtime at
the numbered version, deploy, smoke test, then disable the old version.

| Rotating…              | Consequence                                                                      |
| ---------------------- | -------------------------------------------------------------------------------- |
| `SESSION_SECRET`       | Invalidates active sessions.                                                     |
| `STATE_ENCRYPTION_KEY` | Invalidates outstanding opaque state.                                            |
| LTI signing material   | Requires Canvas/JWKS coordination.                                               |
| SEB certificate        | Requires distributing the new private identity and issuing fresh configurations. |

<Callout type="warn">
  For Cloud Run, always pin numbered Secret Manager versions — never `latest`. The
  exact secret names and bootstrap order are in
  [Google Cloud](/docs/deploy/google-cloud). Rotation runbooks are in
  [Operations](/docs/operate/operations).
</Callout>

## Data model (for reference) [#data-model-for-reference]

Runtime data lives in nine tables plus the `schema_migrations` ledger:
`admin_course_connections`, `admin_tool_presets`,
`admin_tool_preset_assignments`, `assessments`, `courses`, `canvas_oauth_tokens`,
`sessions`, `transient_states`, and `operation_locks`. You do not manage these by
hand — migrations own the schema and cleanup drains expired rows.