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 |
|---|---|---|---|
|
yes |
|
Django secret key |
|
no |
|
Set |
|
no |
|
Comma-separated list of host/domain names |
|
no |
|
|
|
yes |
(generated in DEBUG) |
Base64 32-byte Fernet key |
|
no |
(empty) |
Public base URL (no trailing slash). Falls back to the request host if unset. |
|
no |
|
Disable the first-run onboarding wizard entirely. |
|
no |
|
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:
Onboarding config file (
heartlock_data/appconfig.json)HEARTLOCK_ENCRYPTION_KEYenvironment variableheartlock_data/secret.keyfile (dev only)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 |
|---|---|---|
|
no |
Comma-separated origins, e.g. |
|
no |
Comma-separated origins for unsafe cross-origin requests. Falls back to |
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 |
|---|---|---|
|
no |
Enable OIDC SSO. When all endpoint settings are present and this is not explicitly |
|
no |
OIDC client ID (canonical; legacy alias: |
|
no |
OIDC client secret (canonical; legacy alias: |
|
no |
e.g. |
|
no |
e.g. |
|
no |
e.g. |
|
no |
Default: |
|
no |
Optional; redirect to the IdP end-session endpoint on logout. |
|
no |
Optional; absolute callback URL. Defaults to |
|
no |
Default |
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:
heartlock_data/appconfig.json(set via the admin API)Environment variables (canonical names, then legacy
OIDC_OP_/OIDC_RP_aliases)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 |
|---|---|---|---|
|
no |
(empty) |
When empty, emails print to the console |
|
no |
|
|
|
no |
(empty) |
|
|
no |
(empty) |
|
|
no |
|
|
|
no |
|
|
|
no |
|
|
|
no |
|
Setting EMAIL_HOST automatically switches the backend from console to SMTP.
Override EMAIL_BACKEND explicitly if needed.
Production hardening
When DEBUG=False:
SECURE_SSL_REDIRECTdefaults toTrue— terminate TLS at a reverse proxy and let it sendX-Forwarded-Proto, or setSECURE_SSL_REDIRECT=Falseexplicitly.SESSION_COOKIE_SECUREis forced toTrue.CSRF_COOKIE_SECUREis forced toTrue.HSTS headers are enabled (
SECURE_HSTS_SECONDS = 2592000,SECURE_HSTS_INCLUDE_SUBDOMAINS = True,SECURE_HSTS_PRELOAD = True).SECURE_CONTENT_TYPE_NOSNIFFis forced toTrue.
Docker image details
The multi-stage Dockerfile:
``ui-build`` stage (
node:22-bookworm-slim) — installs Angular dependencies, runs the production build, and rewrites asset paths to/static/ui/....``final`` stage (
python:3.14-slim) — installs Python dependencies, copies the backend source and built UI, creates a non-rootheartlockuser, and runscollectstatic && 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, orheartlock_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.