Server Configuration

All configuration is environment-driven, optionally backed by a .env file (via django-environ). A subset of settings — the public base URL, database URL, and encryption key — may also be set through the first-run onboarding wizard and persisted to heartlock_data/appconfig.json; for those, the config file takes precedence over environment variables.

Core settings

Variable

Required in prod

Default

Notes

SECRET_KEY

yes

django-insecure-dev-key-change-me-in-production

Django secret key

DEBUG

no

True

Set False in production

ALLOWED_HOSTS

no

localhost,127.0.0.1,0.0.0.0

Comma-separated list of host/domain names

DATABASE_URL

no

sqlite:///heartlock_data/db.sqlite3

postgres://user:pass@host:5432/db in compose. Overridden by the onboarding config file.

HEARTLOCK_ENCRYPTION_KEY

yes

(generated in DEBUG)

Base64 32-byte Fernet key

HEARTLOCK_APP_URL

no

(empty)

Public base URL (no trailing slash). Falls back to the request host if unset.

HEARTLOCK_DISABLE_ONBOARDING

no

False

Disable the first-run onboarding wizard entirely.

HEARTLOCK_INVITE_EXPIRY_DAYS

no

7

Days before a project invitation link expires.

Encryption at rest

Every secret value stored in Heartlock is encrypted with Fernet symmetric encryption using HEARTLOCK_ENCRYPTION_KEY. You must set this explicitly in production.

To generate a key:

python -c "from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())"

Warning

If you lose the encryption key, all stored secret values become irrecoverable. Store it securely (a secrets manager, not a plaintext file in version control). Rotating the key requires re-encrypting existing secrets — changing the key value without re-encrypting will make all stored secret values permanently unreadable.

In development (DEBUG=True), if the key is unset, one is generated and persisted to heartlock_data/secret.key.

Precedence for the encryption key:

  1. Onboarding config file (heartlock_data/appconfig.json)

  2. HEARTLOCK_ENCRYPTION_KEY environment variable

  3. heartlock_data/secret.key file (dev only)

  4. Ephemeral in-memory key (not-yet-onboarded production)

Database

Heartlock supports SQLite (for local development / small deployments) and PostgreSQL (recommended for production).

SQLite (default)

When DATABASE_URL is unset and no onboarding config exists, Heartlock uses sqlite:///heartlock_data/db.sqlite3 (the file is created automatically under the project root).

PostgreSQL

DATABASE_URL=postgres://heartlock:password@db:5432/heartlock

The Docker Compose stack runs PostgreSQL 17 by default. The psycopg package (binary) is included in requirements.txt.

The DB_CONN_MAX_AGE setting (default 60) controls persistent-connection lifetime in seconds. Set to 0 for a fresh connection per request.

CORS and CSRF

Variable

Required in prod

Notes

CORS_ALLOWED_ORIGINS

no

Comma-separated origins, e.g. https://app.example.com

CSRF_TRUSTED_ORIGINS

no

Comma-separated origins for unsafe cross-origin requests. Falls back to CORS_ALLOWED_ORIGINS if unset.

Example:

CORS_ALLOWED_ORIGINS=https://heartlock.example.com
CSRF_TRUSTED_ORIGINS=https://heartlock.example.com

OIDC single sign-on

Heartlock acts as an OIDC client (relying party) to an external identity provider — Keycloak, Authentik, Auth0, Dex, Google, etc.

Variable

Required

Notes

OIDC_ENABLE

no

Enable OIDC SSO. When all endpoint settings are present and this is not explicitly False, SSO activates automatically.

OIDC_CLIENT_ID

no

OIDC client ID (canonical; legacy alias: OIDC_RP_CLIENT_ID)

OIDC_CLIENT_SECRET

no

OIDC client secret (canonical; legacy alias: OIDC_RP_CLIENT_SECRET)

OIDC_AUTHORIZATION_ENDPOINT

no

e.g. https://idp.example.com/auth (alias: OIDC_OP_AUTHORIZATION_ENDPOINT)

OIDC_TOKEN_ENDPOINT

no

e.g. https://idp.example.com/token (alias: OIDC_OP_TOKEN_ENDPOINT)

OIDC_USERINFO_ENDPOINT

no

e.g. https://idp.example.com/me (alias: OIDC_OP_USERINFO_ENDPOINT)

OIDC_SCOPES

no

Default: openid email profile (alias: OIDC_RP_SCOPES)

OIDC_END_SESSION_ENDPOINT

no

Optional; redirect to the IdP end-session endpoint on logout.

OIDC_REDIRECT_URI

no

Optional; absolute callback URL. Defaults to <host>/oidc/callback/.

OIDC_CREATE_UNKNOWN_USER

no

Default True; auto-provision a local user when the IdP returns an unknown email.

Example configuration for Keycloak:

OIDC_ENABLE=true
OIDC_CLIENT_ID=heartlock
OIDC_CLIENT_SECRET=your-client-secret
OIDC_AUTHORIZATION_ENDPOINT=https://keycloak.example.com/realms/myrealm/protocol/openid-connect/auth
OIDC_TOKEN_ENDPOINT=https://keycloak.example.com/realms/myrealm/protocol/openid-connect/token
OIDC_USERINFO_ENDPOINT=https://keycloak.example.com/realms/myrealm/protocol/openid-connect/userinfo
OIDC_END_SESSION_ENDPOINT=https://keycloak.example.com/realms/myrealm/protocol/openid-connect/logout

OIDC admin API

Superusers can also configure OIDC through the admin API at /api/v1/admin/oidc/, which writes to the persisted config file. See API Reference for endpoint details.

Config-file precedence

OIDC settings follow the same precedence as other config-file-managed values:

  1. heartlock_data/appconfig.json (set via the admin API)

  2. Environment variables (canonical names, then legacy OIDC_OP_ / OIDC_RP_ aliases)

  3. Built-in defaults

Email / SMTP

In development, leave EMAIL_HOST empty and Django prints emails to the console. For production, point these at your SMTP relay.

Variable

Required in prod

Default

Notes

EMAIL_HOST

no

(empty)

When empty, emails print to the console

EMAIL_PORT

no

587

EMAIL_HOST_USER

no

(empty)

EMAIL_HOST_PASSWORD

no

(empty)

EMAIL_USE_TLS

no

True

EMAIL_USE_SSL

no

False

EMAIL_TIMEOUT

no

10

DEFAULT_FROM_EMAIL

no

Heartlock <noreply@heartlock.local>

Setting EMAIL_HOST automatically switches the backend from console to SMTP. Override EMAIL_BACKEND explicitly if needed.

Production hardening

When DEBUG=False:

  • SECURE_SSL_REDIRECT defaults to True — terminate TLS at a reverse proxy and let it send X-Forwarded-Proto, or set SECURE_SSL_REDIRECT=False explicitly.

  • SESSION_COOKIE_SECURE is forced to True.

  • CSRF_COOKIE_SECURE is forced to True.

  • HSTS headers are enabled (SECURE_HSTS_SECONDS = 2592000, SECURE_HSTS_INCLUDE_SUBDOMAINS = True, SECURE_HSTS_PRELOAD = True).

  • SECURE_CONTENT_TYPE_NOSNIFF is forced to True.

Docker image details

The multi-stage Dockerfile:

  1. ``ui-build`` stage (node:22-bookworm-slim) — installs Angular dependencies, runs the production build, and rewrites asset paths to /static/ui/....

  2. ``final`` stage (python:3.14-slim) — installs Python dependencies, copies the backend source and built UI, creates a non-root heartlock user, and runs collectstatic && migrate && hypercorn.

The server binds 0.0.0.0:8082. STATIC_URL defaults to static/ and STATIC_ROOT to ./staticfiles.

Security notes

  • Never commit .env, db.sqlite3, or heartlock_data/.

  • Service tokens (CLI tokens) are stored hashed, not in plaintext.

  • Secret values are encrypted at rest with Fernet; see Encryption at rest.

  • The onboarding wizard is only available while no administrator account exists. Once a superuser is created, it is permanently closed.