Skip to content

Environment variables

All runtime configuration is read from environment variables. The canonical reference is backend/.env.example in the repository. Copy it and edit:

cd backend
cp .env.example .env

Required

Variable Default Description
SECRET_KEY dev placeholder Django's signing key, and it also signs every JWT. Startup fails outside dev if it is left at the placeholder. It must be at least 32 bytes: HS256 needs a key as long as its hash, and a shorter one only produces a warning nobody reads. Changing it signs everybody out.
ENV_TYPE dev dev or prod. Controls where media is written and which security checks are enforced at startup.
DEBUG True Set to False in production.
ALLOWED_HOSTS localhost,127.0.0.1 Comma-separated.
FRONTEND_URL http://localhost:5173 The web app's base URL, and the one setting that carries every link this system emails: magic-link sign-in, the customer's invoice and estimate portal links, the CSAT survey, and internal assignment notices. Outside dev a loopback value is refused at startup, because left at the default it mails your customers a link to their own machine.

Database

There is no DATABASE_URL. The connection is assembled from five separate variables:

Variable Default
DBNAME crm_db
DBUSER postgres
DBPASSWORD postgres
DBHOST localhost
DBPORT 5432

PostgreSQL only, and the application user must not be a Postgres superuser. Superusers bypass row-level security, which is what isolates one organisation's rows from another's. See Postgres + RLS.

Connection pooling

Variable Default Description
DB_POOL_ENABLED False Turn on psycopg's connection pool.
DB_POOL_MIN_SIZE 2
DB_POOL_MAX_SIZE 10

CONN_MAX_AGE is pinned to 0 because Django refuses a non-zero value alongside a pool.

Celery

Variable Default
CELERY_BROKER_URL redis://localhost:6379/0
CELERY_RESULT_BACKEND redis://localhost:6379/0

Not optional if you want the scheduled work: recurring invoices, overdue marking, payment reminders, expired estimates, SLA breach scanning and stale-timer cleanup are all periodic jobs. Without a worker and a beat process, those features are silently inert.

Email

Variable Default Description
EMAIL_BACKEND console backend Use django_ses.SESBackend for AWS SES.
DEFAULT_FROM_EMAIL noreply@localhost
ADMIN_EMAIL admin@localhost
AWS_SES_REGION_NAME ap-south-1 Read only when the backend is SES.
AWS_SES_REGION_ENDPOINT derived from the region

SES falls back to IAM role credentials if AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY are unset. For plain SMTP, set EMAIL_BACKEND to Django's SMTP backend and add Django's own EMAIL_HOST family; the project does not define those itself.

Google OAuth

Variable Description
GOOGLE_CLIENT_ID Leave blank to disable Google sign-in.
GOOGLE_CLIENT_SECRET

Magic-link sign-in works without either, so OAuth is genuinely optional.

CORS, CSRF and proxies

Variable Default Description
CORS_ALLOWED_ORIGINS dev origins Comma-separated.
CORS_ALLOW_ALL False Do not enable in production.
CSRF_TRUSTED_ORIGINS dev origin
TRUST_PROXY_SSL_HEADER False Set to True only when TLS terminates at a proxy that is the only way in. It makes Django trust X-Forwarded-Proto, which is what lets HSTS emit a header at all. On direct HTTP it becomes a value any caller can forge.
DOMAIN_NAME http://localhost:8000 The API's own public origin, for example https://api.example.com. From django-crm 1.13.0 the task calendar feed URL and the web form embed snippets are built from it, and outside dev the backend refuses to start when it is a localhost or non-absolute URL.
NUM_PROXIES unset From django-crm 1.11.0. How many proxies in front of Django append to X-Forwarded-For (nginx with $proxy_add_x_forwarded_for is one). It decides the client IP the app records (audit log, web form submissions, estimate acceptance, sign-in tokens) and the per-IP throttles. Set it to the proxy count only when REMOTE_ADDR is the proxy itself, such as gunicorn behind nginx (1); unset, every visitor then shares one throttle bucket. Leave it unset under uvicorn --proxy-headers, which already resolves the client, or the proxy is counted twice. Setting it higher than the real number of proxies lets callers choose their own IP.
RELAY_SECRET unset From django-crm 1.12.0. Shared secret with the SvelteKit app, which fetches the public help center and estimate portal on the visitor's behalf. With the same value set on both, the app sends the visitor's address signed with it and the API records and throttles each visitor separately; without it they all count as the app's server. Headers without the secret are ignored, so a caller cannot choose its address. At least 32 characters or the API refuses to start. Generate with python -c "import secrets; print(secrets.token_urlsafe(48))".

Frontend

The SvelteKit app reads these variables:

Variable Description
PUBLIC_DJANGO_API_URL Base URL of the Django backend. The app's server calls the API here, so it can be an internal address such as http://backend:8000. The URLs the API hands back for use elsewhere (the task calendar feed URL, web form embed snippets) come from the backend's DOMAIN_NAME, not from this.
PUBLIC_SENTRY_DSN Optional error reporting.
RELAY_SECRET Optional. The same value as the API's RELAY_SECRET; see above. Private: never prefix it with PUBLIC_.
ADDRESS_HEADER, XFF_DEPTH Read by adapter-node itself. Behind a reverse proxy, set ADDRESS_HEADER=X-Forwarded-For and XFF_DEPTH=1 (one proxy that appends the visitor's address) so the app knows who the visitor is. Without them every visitor appears as the proxy, and RELAY_SECRET has nothing real to send.
HOST Read by adapter-node itself; defaults to 0.0.0.0. Behind a reverse proxy, set HOST=127.0.0.1 so only the proxy can reach the app. Reachable directly, a caller writes its own X-Forwarded-For and the app signs it as the visitor.

Anything prefixed PUBLIC_ is shipped to the browser. Never put a secret there.

Media and attachments

Files are written to the local filesystem under media/, always. There is no S3 or object-storage backend: the project configures no STORAGES or DEFAULT_FILE_STORAGE, so AWS_BUCKET_NAME in the example file is currently inert for uploads and the AWS keys matter only for SES. If you need object storage, that is a change to the Django settings rather than an environment variable.

A single attachment is capped at 25 MB, and CSV imports and lead uploads at 5 MB.

Corrections

This page previously documented DJANGO_SECRET_KEY, JWT_SIGNING_KEY, DATABASE_URL, REDIS_URL, EMAIL_HOST, EMAIL_HOST_USER, EMAIL_HOST_PASSWORD, EMAIL_PORT, GOOGLE_REDIRECT_URI, AWS_STORAGE_BUCKET_NAME, AWS_S3_ENDPOINT_URL, PUBLIC_API_URL and PUBLIC_GOOGLE_CLIENT_ID, and pointed at a .env.docker.example file. None of those names is read by crm/settings.py and that file does not exist, so anyone configuring a deployment from this page could not have started the application. Kept as a note rather than deleted, so somebody debugging a .env written from the old version can see what happened.