Environment Variables Migration Plan
Status: โ Implemented (Branding configuration migrated to SiteBranding model)
Developer Note: This document describes the migration of branding settings from environment variables to database-backed configuration. The platform-level branding is now managed via the SiteBranding model.
Overview
This document defines which configuration options should remain as environment variables and which should be migrated to UI-based management via Django management commands and admin interface.
Guiding Principles:
1. Infrastructure & Security โ Environment Variables (.env file)
2. Branding & Customization โ Management Commands + UI (Django admin/custom views)
3. Operational Settings โ Keep flexible (can be env vars with UI overrides)
Current Theme System Architecture
CheckTick uses a 3-tier theme cascade with the following precedence (highest to lowest):
- Survey-level themes (
Survey.styleJSON field) - Overrides all - Organization-level themes (
Organizationtheme fields) - Overrides platform defaults - Platform-level themes (
SiteBrandingmodel + ENV fallbacks) - Base defaults
Implementation Details
Platform Level (what we're migrating):
- Currently: ENV variables (
BRAND_*) โsettings.pyโcontext_processors.py - After migration:
SiteBrandingmodel (DB) โcontext_processors.pyโ templates - ENV variables become fallbacks only if
SiteBrandingnot configured
Organization Level (already implemented):
Organizationmodel has theme fields:default_theme,theme_preset_light,theme_preset_dark,theme_light_css,theme_dark_css- Applied in
context_processors.pyif user is organization member - Overrides platform defaults
Survey Level (already implemented):
Survey.styleJSON field contains theme overrides- Applied per-survey in views/templates
- Highest priority - overrides both platform and organization themes
Theme CSS Generation
The system uses generate_theme_css_for_brand() function (in checktick_app/core/themes.py) to:
- Take DaisyUI preset names (e.g., "nord", "business")
- Generate CSS variables for runtime theme switching
- Allow custom CSS overrides from DaisyUI Theme Generator
This works at all three levels (platform, organization, survey).
Category 1: KEEP as Environment Variables
These settings are infrastructure-level, security-sensitive, or need to be set before the application starts. They should remain in .env files.
Core Infrastructure
| Variable | Purpose | Why ENV? |
|---|---|---|
DEBUG |
Enable debug mode | Security - must be False in production |
SECRET_KEY |
Django secret key | Security - required at startup |
ALLOWED_HOSTS |
Allowed domain names | Security - required at startup |
CSRF_TRUSTED_ORIGINS |
HTTPS origins for CSRF | Security - required at startup |
SECURE_SSL_REDIRECT |
Force HTTPS redirects | Security - required at startup |
SITE_URL |
Base URL for emails | Infrastructure - needed for emails |
Database Configuration
| Variable | Purpose | Why ENV? |
|---|---|---|
DATABASE_URL |
Database connection string | Infrastructure - required at startup |
POSTGRES_DB |
PostgreSQL database name | Infrastructure - for included container |
POSTGRES_USER |
PostgreSQL username | Infrastructure - for included container |
POSTGRES_PASSWORD |
PostgreSQL password | Security - credential management |
Email Provider (SMTP)
| Variable | Purpose | Why ENV? |
|---|---|---|
DEFAULT_FROM_EMAIL |
From address for emails | Infrastructure - needed at startup |
SERVER_EMAIL |
Error report email | Infrastructure - needed at startup |
EMAIL_HOST |
SMTP server hostname | Infrastructure - connection required |
EMAIL_PORT |
SMTP server port | Infrastructure - connection required |
EMAIL_USE_TLS |
Use TLS encryption | Infrastructure - connection required |
EMAIL_USE_SSL |
Use SSL encryption | Infrastructure - connection required |
EMAIL_HOST_USER |
SMTP username | Security - credential management |
EMAIL_HOST_PASSWORD |
SMTP password | Security - credential management |
EMAIL_TIMEOUT |
SMTP timeout | Infrastructure - performance tuning |
Rationale: Email provider settings require credentials and are infrastructure-level configuration. These should remain in environment variables for security and deployment flexibility.
Authentication (SSO/OIDC)
| Variable | Purpose | Why ENV? |
|---|---|---|
OIDC_RP_CLIENT_ID_GOOGLE |
Google OAuth client ID | Security - credential management |
OIDC_RP_CLIENT_SECRET_GOOGLE |
Google OAuth secret | Security - credential management |
OIDC_RP_CLIENT_ID_AZURE |
Azure AD client ID | Security - credential management |
OIDC_RP_CLIENT_SECRET_AZURE |
Azure AD secret | Security - credential management |
OIDC_OP_TENANT_ID_AZURE |
Azure AD tenant ID | Security - credential management |
OIDC_OP_JWKS_ENDPOINT_GOOGLE |
Google JWKS endpoint | Infrastructure - required at startup |
OIDC_OP_JWKS_ENDPOINT_AZURE |
Azure JWKS endpoint | Infrastructure - required at startup |
Rationale: OAuth/OIDC credentials are security-sensitive and must be available at application startup. These belong in environment variables or secret management systems.
Spam Protection
| Variable | Purpose | Why ENV? |
|---|---|---|
HCAPTCHA_SITEKEY |
hCaptcha site key | Infrastructure - public key for forms |
HCAPTCHA_SECRET |
hCaptcha secret key | Security - credential management |
Rationale: Third-party service credentials should be in environment variables for security.
External APIs
| Variable | Purpose | Why ENV? |
|---|---|---|
EXTERNAL_DATASET_API_URL |
RCPCH API endpoint | Infrastructure - required at startup |
EXTERNAL_DATASET_API_KEY |
RCPCH API key | Security - credential management |
LLM_URL |
LLM API endpoint | Infrastructure - service connection |
LLM_API_KEY |
LLM API key | Security - credential management |
LLM_AUTH_TYPE |
LLM auth method | Infrastructure - connection config |
Rationale: External API credentials and endpoints are infrastructure configuration that should remain in environment variables.
Account Tier System (NEW)
| Variable | Purpose | Why ENV? |
|---|---|---|
SELF_HOSTED |
Enable self-hosted mode | Infrastructure - changes tier behavior |
PAYMENT_PROVIDER |
Payment provider name | Infrastructure - integration required |
PAYMENT_API_KEY |
Payment provider key | Security - credential management |
PAYMENT_WEBHOOK_SECRET |
Payment webhook secret | Security - credential management |
Rationale: These control fundamental application behavior and payment integrations, requiring environment-level configuration.
Data Governance (Optional)
| Variable | Purpose | Why ENV? |
|---|---|---|
CHECKTICK_DEFAULT_RETENTION_MONTHS |
Default retention period | Policy - organizational default |
CHECKTICK_MAX_RETENTION_MONTHS |
Maximum retention allowed | Policy - compliance constraint |
CHECKTICK_DOWNLOAD_LINK_EXPIRY_DAYS |
Export link expiry | Policy - security setting |
CHECKTICK_WARN_BEFORE_DELETION_DAYS |
Warning schedule | Policy - notification timing |
Rationale: These are policy-level settings that typically don't change frequently. Can remain as environment variables with sensible defaults. Could optionally be moved to UI in future if needed.
Category 2: MIGRATE to Management Commands + UI
These settings are customization-focused and benefit from UI-based management. They should be removed from environment variables and managed via Django admin or custom views.
Branding & Theming
| Current Variable | New Management Method | UI Location |
|---|---|---|
BRAND_TITLE |
โ Remove from ENV | Django Admin โ SiteBranding |
BRAND_ICON_URL |
โ Remove from ENV | Django Admin โ SiteBranding (icon_url field) |
BRAND_ICON_URL_DARK |
โ Remove from ENV | Django Admin โ SiteBranding (icon_url_dark field) |
BRAND_ICON_ALT |
โ Remove from ENV | Django Admin โ SiteBranding (new field) |
BRAND_ICON_TITLE |
โ Remove from ENV | Django Admin โ SiteBranding (new field) |
BRAND_ICON_SIZE_CLASS |
โ Remove from ENV | Django Admin โ SiteBranding (new field) |
BRAND_ICON_SIZE |
โ Remove from ENV | Django Admin โ SiteBranding (new field) |
BRAND_THEME |
โ Remove from ENV | Django Admin โ SiteBranding (default_theme field) |
BRAND_THEME_PRESET_LIGHT |
โ Remove from ENV | Django Admin โ SiteBranding (theme_preset_light field) |
BRAND_THEME_PRESET_DARK |
โ Remove from ENV | Django Admin โ SiteBranding (theme_preset_dark field) |
BRAND_FONT_HEADING |
โ Remove from ENV | Django Admin โ SiteBranding (font_heading field) |
BRAND_FONT_BODY |
โ Remove from ENV | Django Admin โ SiteBranding (font_body field) |
BRAND_FONT_CSS_URL |
โ Remove from ENV | Django Admin โ SiteBranding (font_css_url field) |
BRAND_THEME_CSS_LIGHT |
โ Remove from ENV | Django Admin โ SiteBranding (theme_light_css field) |
BRAND_THEME_CSS_DARK |
โ Remove from ENV | Django Admin โ SiteBranding (theme_dark_css field) |
Management Command:
# CLI configuration for initial setup or automation
python manage.py configure_branding \
--theme-light=nord \
--theme-dark=business \
--logo=path/to/logo.png \
--logo-dark=path/to/logo-dark.png
UI Access:
- Self-hosted: Superusers access via /admin/core/sitebranding/ or custom view at /branding/
- Hosted SaaS: Enterprise tier users access custom branding view at /branding/
Benefits of UI Management: 1. โ No container restarts required for theme changes 2. โ File upload support for logos (better UX than mounting volumes) 3. โ Live preview of changes 4. โ Validation and error feedback 5. โ Version control via Django migrations for schema changes 6. โ Backup and restore via database dumps
Category 3: HYBRID (ENV with UI Override)
Some settings may benefit from having both environment variable defaults AND UI override capability.
Example: Site Title
# In settings.py - use ENV var as default
SITE_TITLE = os.environ.get('SITE_TITLE', 'CheckTick')
# In template/view - check SiteBranding model first
branding = SiteBranding.objects.first()
site_title = branding.site_title if branding and branding.site_title else settings.SITE_TITLE
This allows: - Quick setup via ENV var for basic deployments - UI customization for advanced users who want live changes - Fallback to ENV var if database not available
Recommendation: Start with pure UI management (Category 2). Add ENV fallbacks only if users request it for specific use cases.
Implementation Plan
Phase 1: Update SiteBranding Model โ
The SiteBranding model already exists with most fields. Additions needed:
class SiteBranding(models.Model):
# Existing fields (already implemented)
default_theme = models.CharField(...)
icon_url = models.URLField(...)
icon_file = models.FileField(...)
icon_url_dark = models.URLField(...)
icon_file_dark = models.FileField(...)
font_heading = models.CharField(...)
font_body = models.CharField(...)
font_css_url = models.URLField(...)
theme_preset_light = models.CharField(...)
theme_preset_dark = models.CharField(...)
theme_light_css = models.TextField(...)
theme_dark_css = models.TextField(...)
# NEW fields to add:
site_title = models.CharField(max_length=255, default='CheckTick', blank=True)
icon_alt = models.CharField(max_length=255, default='CheckTick', blank=True)
icon_title = models.CharField(max_length=255, default='CheckTick', blank=True)
icon_size_class = models.CharField(max_length=50, default='w-6 h-6', blank=True)
Phase 2: Create Management Command โ
File: checktick_app/core/management/commands/configure_branding.py
Already designed in docs/account-tiers-implementation.md. Supports:
- --theme-light, --theme-dark
- --logo, --logo-dark
- --default-theme
- Additional flags for other fields as needed
Phase 3: Create/Enhance Branding UI โ
Option A: Django Admin (simpler, faster to implement)
- Register SiteBranding model in admin
- Use existing admin interface
- Good for self-hosted superusers
Option B: Custom View (better UX, more control)
- Create custom view at /branding/
- File upload with preview
- Theme selector with live preview
- Better for Enterprise tier users
Recommendation: Start with Option A (admin), add Option B in future release if needed.
Phase 4: Update Context Processors
Update checktick_app/core/context_processors.py to use SiteBranding model instead of environment variables:
def branding(request):
"""Inject branding settings into all templates."""
from .models import SiteBranding
branding, _ = SiteBranding.objects.get_or_create(pk=1)
return {
'site_title': branding.site_title or 'CheckTick',
'brand_icon_url': branding.icon_url,
'brand_icon_file': branding.icon_file,
# ... etc
}
Phase 5: Update Documentation
Update all self-hosting documentation to:
1. Remove branding environment variables from examples
2. Add instructions for using python manage.py configure_branding
3. Add instructions for accessing Django admin branding configuration
4. Update quickstart guide to focus on essential ENV vars only
Files to Update:
- โ
docs/self-hosting-quickstart.md
- โ
docs/self-hosting-configuration.md
- โ
docs/self-hosting-themes.md
- โ
.env.example
- โ
.env.selfhost
Phase 6: Migration Strategy
For existing deployments currently using ENV vars:
- Create migration to populate
SiteBrandingfrom ENV vars on first run - Deprecation warning in startup logs if branding ENV vars are detected
- Gradual removal - support ENV vars for 2 releases, then remove
Example migration:
def migrate_env_to_db(apps, schema_editor):
"""One-time migration of environment variables to SiteBranding model."""
import os
SiteBranding = apps.get_model('core', 'SiteBranding')
branding, created = SiteBranding.objects.get_or_create(pk=1)
# Only update if fields are empty (don't override existing DB values)
if not branding.site_title and os.environ.get('BRAND_TITLE'):
branding.site_title = os.environ.get('BRAND_TITLE')
if not branding.theme_preset_light and os.environ.get('BRAND_THEME_PRESET_LIGHT'):
branding.theme_preset_light = os.environ.get('BRAND_THEME_PRESET_LIGHT')
# ... etc for all fields
branding.save()
Updated .env Template
Minimal Self-Hosting .env
# ===================================================================
# CheckTick Self-Hosting Configuration
# ===================================================================
# ========================
# REQUIRED: Security
# ========================
SECRET_KEY=your-very-long-random-secret-key-here
DEBUG=False
ALLOWED_HOSTS=yourdomain.com,localhost
CSRF_TRUSTED_ORIGINS=https://yourdomain.com
SECURE_SSL_REDIRECT=True
SITE_URL=https://yourdomain.com
# ========================
# REQUIRED: Database
# ========================
# Option 1: Use included PostgreSQL (default)
POSTGRES_DB=checktick
POSTGRES_USER=checktick
POSTGRES_PASSWORD=change-this-secure-password
# Option 2: External database (uncomment and use docker-compose.external-db.yml)
# DATABASE_URL=postgresql://user:pass@host:5432/checktick
# ========================
# REQUIRED: Email Provider
# ========================
[email protected]
[email protected]
# Choose ONE email provider:
# Gmail (for testing)
EMAIL_HOST=smtp.gmail.com
EMAIL_PORT=587
EMAIL_USE_TLS=True
[email protected]
EMAIL_HOST_PASSWORD=your-app-specific-password
# OR Mailgun (recommended for production)
# EMAIL_HOST=smtp.eu.mailgun.org
# EMAIL_PORT=587
# EMAIL_USE_TLS=True
# [email protected]
# EMAIL_HOST_PASSWORD=your-mailgun-smtp-password
EMAIL_TIMEOUT=10
# ========================
# REQUIRED: External Datasets
# ========================
EXTERNAL_DATASET_API_URL=https://api.rcpch.ac.uk
EXTERNAL_DATASET_API_KEY=your-rcpch-api-key-here
# ========================
# OPTIONAL: Account Tiers
# ========================
# Set to true for self-hosted deployments (gives everyone Enterprise features)
SELF_HOSTED=true
# For hosted SaaS only:
# PAYMENT_PROVIDER=ryft
# PAYMENT_API_KEY=your-payment-api-key
# PAYMENT_WEBHOOK_SECRET=your-webhook-secret
# ========================
# OPTIONAL: Authentication
# ========================
# Google OAuth
# OIDC_RP_CLIENT_ID_GOOGLE=your-google-client-id
# OIDC_RP_CLIENT_SECRET_GOOGLE=your-google-client-secret
# Microsoft 365 / Azure AD
# OIDC_RP_CLIENT_ID_AZURE=your-azure-client-id
# OIDC_RP_CLIENT_SECRET_AZURE=your-azure-client-secret
# OIDC_OP_TENANT_ID_AZURE=your-tenant-id
# ========================
# OPTIONAL: Spam Protection
# ========================
# HCAPTCHA_SITEKEY=your-site-key
# HCAPTCHA_SECRET=your-secret-key
# ========================
# OPTIONAL: AI Features
# ========================
# LLM_URL=https://api.openai.com/v1/chat/completions
# LLM_API_KEY=your-api-key-here
# LLM_AUTH_TYPE=bearer
# ========================
# OPTIONAL: Data Governance
# ========================
# CHECKTICK_DEFAULT_RETENTION_MONTHS=6
# CHECKTICK_MAX_RETENTION_MONTHS=24
# CHECKTICK_DOWNLOAD_LINK_EXPIRY_DAYS=7
# CHECKTICK_WARN_BEFORE_DELETION_DAYS=30,7,1
# ===================================================================
# BRANDING & THEMES: Now configured via Django admin or CLI
# Use: python manage.py configure_branding
# Or: Django Admin โ Core โ Site Branding
# ===================================================================
Size reduction: ~40 lines removed (all BRAND_* variables)
Scheduled Tasks Configuration
Scheduled tasks (cron jobs) should remain as documented - these are operational requirements, not configuration:
Required Cron Jobs
# Data governance (REQUIRED for GDPR compliance)
0 2 * * * docker compose exec -T web python manage.py process_data_governance
# Survey progress cleanup (Recommended)
0 3 * * * docker compose exec -T web python manage.py cleanup_survey_progress
# External datasets sync (Recommended)
0 4 * * * docker compose exec -T web python manage.py sync_external_datasets
# NHS Data Dictionary sync (Recommended - weekly)
0 5 * * 0 docker compose exec -T web python manage.py sync_nhs_dd_datasets
# Question group templates sync (Optional)
0 6 * * * docker compose exec -T web python manage.py sync_global_question_group_templates
No changes needed - these remain as scheduled tasks in cron/systemd/Kubernetes CronJob.
Summary
Environment Variables: 37 variables
- Security: 6 (SECRET_KEY, DEBUG, ALLOWED_HOSTS, CSRF_TRUSTED_ORIGINS, SECURE_SSL_REDIRECT, SITE_URL)
- Database: 4 (DATABASE_URL, POSTGRES_DB, POSTGRES_USER, POSTGRES_PASSWORD)
- Email: 9 (DEFAULT_FROM_EMAIL, SERVER_EMAIL, EMAIL_HOST, EMAIL_PORT, EMAIL_USE_TLS, EMAIL_USE_SSL, EMAIL_HOST_USER, EMAIL_HOST_PASSWORD, EMAIL_TIMEOUT)
- Authentication: 7 (OIDC_RP_CLIENT_ID_GOOGLE, OIDC_RP_CLIENT_SECRET_GOOGLE, OIDC_RP_CLIENT_ID_AZURE, OIDC_RP_CLIENT_SECRET_AZURE, OIDC_OP_TENANT_ID_AZURE, OIDC_OP_JWKS_ENDPOINT_GOOGLE, OIDC_OP_JWKS_ENDPOINT_AZURE)
- Spam Protection: 2 (HCAPTCHA_SITEKEY, HCAPTCHA_SECRET)
- External APIs: 5 (EXTERNAL_DATASET_API_URL, EXTERNAL_DATASET_API_KEY, LLM_URL, LLM_API_KEY, LLM_AUTH_TYPE)
- Account Tiers: 4 (SELF_HOSTED, PAYMENT_PROVIDER, PAYMENT_API_KEY, PAYMENT_WEBHOOK_SECRET)
- Data Governance: 4 (CHECKTICK_DEFAULT_RETENTION_MONTHS, CHECKTICK_MAX_RETENTION_MONTHS, CHECKTICK_DOWNLOAD_LINK_EXPIRY_DAYS, CHECKTICK_WARN_BEFORE_DELETION_DAYS) - Optional
Migrated to UI: 15 variables
- All
BRAND_*variables (15 total) - Managed via Django admin or
python manage.py configure_branding
Benefits of This Approach
โ
Cleaner .env files - Focus on essential infrastructure config
โ
Better UX - Self-hosters can customize branding without editing files
โ
No restarts needed - Theme changes apply immediately
โ
File uploads - Upload logos directly via UI instead of mounting volumes
โ
Security - Credentials stay in ENV vars where they belong
โ
Flexibility - Self-hosted and SaaS use same codebase
โ
Enterprise features - White-labeling via UI for Enterprise tier
โ
Version control - Theme schema changes via Django migrations
Next Steps
- โ Review this document and approve approach
- โณ Add new fields to
SiteBrandingmodel - โณ Create migration to add new fields
- โณ Update context processors to use model instead of ENV vars
- โณ Update templates to use new context variables
- โณ Create
configure_brandingmanagement command - โณ Update Django admin for
SiteBranding - โณ Update documentation (all self-hosting docs)
- โณ Update
.env.exampleand.env.selfhosttemplates - โณ Test migration path for existing deployments