Configuration

All settings live in main/settings.py. Copy main/settings.example.py as your starting point.


Django Core

Acceptance target isolation

The Deploy LTI dev workflow sets LTI_TARGET_ENVIRONMENT=acceptance and the six required target URLs: CITIZENGO_URL, HAZTEOIR_URL, DONUI_BASE_URL, HAZTEOIR_DONUI_BASE_URL, BACKEND_BASE_URL, and FRAPI_BASE_URL. LTI_ALLOWED_TARGET_HOSTS contains their exact hostnames. Production hostnames are always blocked and cannot be re-enabled through the allowlist.

These hostnames and URLs are non-secret deployment configuration and are applied to both the service and cron job on every dev deployment. Credentials remain separately managed secret bindings and the workflow does not replace them.

The runtime validates the complete manifest in authenticated readiness. Browser navigation installs Chrome request interception before opening a page and authorizes every navigation and subresource against the exact acceptance-host allowlist before dispatch. It also validates the requested and final URL after redirects. Missing, unapproved, non-HTTPS, credentialed, fragmented, IP-based, or production targets fail closed. The readiness response reports only target names and hostnames.

Persisted donation links intentionally retain their production-shaped URLs so the same records remain valid for production execution. In acceptance only, the browser navigation boundary resolves exact donate.citizengo.org and donate.hazteoir.org targets to the configured sandbox DonUI bases while preserving the path and query string. The legacy /es-ho CitizenGO-hosted path resolves to HAZTEOIR_DONUI_BASE_URL. This does not allow production redirects: the browser's exact-host interception and final-target validation remain active.

Target policy violations are deterministic configuration failures. Recurring tests still schedule their next normal occurrence, but do not create an immediate retry clone for TargetEnvironmentError; this prevents duplicate incident tasks while preserving the recurring schedule.

The current acceptance manifest is documented in docs/q3-e2e-environments.md; credentials remain in Secret Manager.

Setting Description
SECRET_KEY Django secret key — generate a unique one for production
DEBUG True for development, False for production
ALLOWED_HOSTS List of allowed hostnames
LOGIN_URL Authentication URL, defaults to /admin/login/
STATIC_ROOT Path for collected static files
API_CLIENT_TOKEN_HASH_KEY Required stable HMAC key for hashing Agent API bearer tokens; keep separate from DJANGO_SECRET_KEY in Cloud Run
API_CLIENT_TOKEN_HASH_FALLBACK_KEYS Comma-separated previous hash keys accepted during token-key rotation
AGENT_API_AUTH_FAILURE_LIMIT Optional failed-auth throttle limit per token and remote address, backed by cache plus ApiAuditLog for cross-instance checks; defaults to 30
AGENT_API_AUTH_FAILURE_WINDOW_SECONDS Optional failed-auth throttle window in seconds; defaults to 60
AGENT_API_AUTH_FAILURE_AUDIT_LIMIT Optional cap for anonymous failed-auth ApiAuditLog writes per remote address and window; defaults to 120
AGENT_API_AUTH_FAILURE_AUDIT_WINDOW_SECONDS Optional window for the anonymous failed-auth audit write cap; defaults to 60
AGENT_API_AUTH_FAILURE_AUDIT_RETENTION_DAYS Optional retention window for anonymous failed-auth audit rows pruned from the public Agent API surface; defaults to 7
AGENT_API_AUTH_FAILURE_AUDIT_PRUNE_INTERVAL_SECONDS Optional cache-backed interval between anonymous failed-auth audit pruning attempts; defaults to 3600
AGENT_API_TRUST_X_FORWARDED_FOR Optional Cloud Run/proxy audit source setting. Keep False unless the deployment path normalizes X-Forwarded-For; when True, Agent API audit/throttle uses forwarded IPs only when REMOTE_ADDR matches AGENT_API_TRUSTED_PROXY_CIDRS
AGENT_API_TRUSTED_PROXY_CIDRS Optional comma-separated trusted proxy CIDRs for X-Forwarded-For; the Agent API drops trusted proxy hops from the right and uses the nearest untrusted forwarded IP
AGENT_API_RUNTIME_SERVICE Optional GitHub environment variable overriding the Cloud Run service name checked/configured by Agent API runtime scripts; defaults to lti-dev or lti-prod
AGENT_API_RUNTIME_JOBS Optional comma-separated GitHub environment variable overriding the Cloud Run job names checked/configured by Agent API runtime scripts; defaults to lti-dev-cron or lti-prod-cron; add only approved, active jobs here

Database

Local Docker/development settings use MySQL via PyMySQL:

DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.mysql',
        'NAME': 'cgo_testing_interface',
        'USER': 'db',
        'PASSWORD': 'your_password',
    }
}

CI uses SQLite via main.settings_ci for fast isolated test runs. Cloud Run uses PostgreSQL via main.settings_cloudrun, with connection values supplied through environment variables and Secret Manager.

Cloud Run service and job revisions must already expose API_CLIENT_TOKEN_HASH_KEY as a version-pinned Secret Manager environment binding before Agent API clients are provisioned. Literal Cloud Run environment values and floating latest secret aliases are rejected by the runtime check. Deploy workflows do not block image deployment on this runtime check. If the key is absent at runtime, the public app can still start and Agent API authentication rejects tokens until the setting is fixed; API client provisioning and token hashing still require the explicit key.

Provision or repair the Cloud Run binding with the manual Configure Agent API Runtime Config GitHub workflow. Use lti-dev or lti-prod as the confirmation value and the Secret Manager secret name as secret_name. The optional secret_version input must be a numeric Secret Manager version. When omitted, the workflow resolves and pins the latest enabled version at workflow runtime. The workflow binds the secret reference only; it does not print or receive the secret value, and it does not bind the floating latest alias. During hash-key rotation, optionally set fallback_secret_name to a Secret Manager secret whose value is the comma-separated API_CLIENT_TOKEN_HASH_FALLBACK_KEYS string, plus fallback_secret_version when a specific version should be pinned. The same workflow then binds both API_CLIENT_TOKEN_HASH_KEY and API_CLIENT_TOKEN_HASH_FALLBACK_KEYS, allowing existing tokens to authenticate with the fallback key and be rehashed to the primary key on successful use. When rotating keys, run the check script directly to verify the fallback binding on the service and jobs. Remove the fallback binding after the rotation window closes and no fallback key should be accepted.

For prod, the workflow runs under the GitHub production environment and should be protected with required reviewers in repository settings. The workflow also requires confirm_prod_runtime_secret_binding=bind-prod-agent-api-runtime before it will change the prod runtime secret binding.

The same operation can also be run locally with:

API_CLIENT_TOKEN_HASH_SECRET_NAME=lti-api-client-token-hash-key \
  scripts/configure_agent_api_runtime_config.sh dev
API_CLIENT_TOKEN_HASH_SECRET_NAME=lti-api-client-token-hash-key \
  scripts/configure_agent_api_runtime_config.sh prod
API_CLIENT_TOKEN_HASH_SECRET_NAME=lti-api-client-token-hash-key \
API_CLIENT_TOKEN_HASH_SECRET_VERSION=3 \
  scripts/configure_agent_api_runtime_config.sh prod
API_CLIENT_TOKEN_HASH_SECRET_NAME=lti-api-client-token-hash-key \
API_CLIENT_TOKEN_HASH_SECRET_VERSION=4 \
API_CLIENT_TOKEN_HASH_FALLBACK_SECRET_NAME=lti-api-client-token-hash-fallback-keys \
API_CLIENT_TOKEN_HASH_FALLBACK_SECRET_VERSION=2 \
  scripts/configure_agent_api_runtime_config.sh prod

The script binds the Secret Manager secret as API_CLIENT_TOKEN_HASH_KEY on the Cloud Run service and the active matching cron jobs without printing the secret value. It verifies that the selected version is enabled and stores a versioned binding such as lti-api-client-token-hash-key:3, so each Cloud Run revision uses the intended hash key version. If API_CLIENT_TOKEN_HASH_FALLBACK_SECRET_NAME is set, the check script also requires API_CLIENT_TOKEN_HASH_FALLBACK_KEYS to be bound from that fallback secret with a numeric version rather than latest.

Deploy workflows do not run Agent API runtime-config preflight or Agent API smoke as deploy gates. Dev and prod deploys ship the image, smoke /system_health, promote the candidate revision, and then update the matching cron job image. Dev deploy also verifies lti-dev-cron report recipient routing before traffic promotion. Agent API readiness is checked through the runtime-config workflow, rollout runbook, focused smoke script, runner smoke, scheduled QA, or manual diagnostics, not through prod-only recovery controls or deploy-time bypasses.

The first Agent API rollout remains explicit, but it is handled as configuration and validation work instead of as deploy UI ceremony:

  1. Deploy the code so migrations create the Agent API schema.
  2. Bind API_CLIENT_TOKEN_HASH_KEY with the manual runtime-config workflow.
  3. Create/provision the read-scope smoke ApiClient token in the target database after migrations are present.
  4. Store the raw smoke token as LTI_AGENT_API_SMOKE_TOKEN where the smoke command or scheduled diagnostics can read it.
  5. Run the Agent API smoke and runner smoke outside the deploy workflow:

bash AGENT_API_SMOKE_TOKEN=<read-scope-token> scripts/smoke_agent_api_runtime.sh <cloud-run-url>

Follow the Agent API rollout runbook in docs/agent-api-rollout.md for the first rollout, runner smoke, token rotation, and the required check that Cloud Run starts through the Dockerfile.prod entrypoint which runs migrations before Gunicorn.

For prod runtime-secret mutation, the manual Configure Agent API Runtime Config workflow also verifies through the GitHub API that the production environment has required reviewers before it binds secrets.

The Agent API smoke uses a read-scope token to check authenticated Agent API health and schema readiness outside image deploys. The smoke script writes the Authorization header to a short-lived curl config file with mode 600 instead of passing the bearer token as a raw process argument. Runner write routes for next/lease/event/artifact/complete are covered by the unit and integration suite rather than by granting the deploy smoke secret runner-write capability. Invalid-token throttling behavior is covered by the unit suite rather than by deliberately poisoning the live remote throttle window during every deploy. The authenticated Agent API health payload also acts as a migration gate: it must report that the Phase 3C test_interface.0014_unique_execution_lease_idempotency migration is applied in the target database. Migration readiness is checked before bearer authentication only to avoid querying Agent API auth/audit tables before they exist; the unauthenticated response is a generic 503, does not include migration names, and writes a best-effort audit row when the audit table is available. If the schema is ready but API_CLIENT_TOKEN_HASH_KEY is missing, Agent API routes return a generic 503 runtime-config failure before token validation, record audit rows, and throttle repeated readiness probes from the same remote with 429 responses. Once the schema and runtime config are ready, /agent-api/v1/health requires the read-scope bearer token and returns detailed schema readiness only to the authenticated smoke/client. Dockerfile.prod runs python manage.py migrate --fake-initial --noinput in the Cloud Run entrypoint before Gunicorn starts, so the smoke verifies that startup migration path before job image updates proceed.

Scenario Manager Import

Use the import_scenario_manager management command to import reusable scenario definitions into the Django Scenario Library:

python manage.py import_scenario_manager --json-file scenario-manager-export.json
python manage.py import_scenario_manager --mysql --mysql-host 127.0.0.1 --mysql-port 3306

Supported options:

Option Description
--json-file Read a JSON export with sections, scenarios, section_scenarios, steps, and screenshots tables
--mysql Read the same table shape from a compatible MySQL database
--source Legacy source key used with legacy_id for idempotent imports
--dry-run Run the import inside a rolled-back transaction
--adopt-existing Attach legacy IDs to matching manual records when they have been verified as the same entities
--prune-missing Delete previously imported or adopted records missing from a complete, clean current export
--confirm-prune Required for a real prune; use the dry-run preview token shaped as <source>:complete-export:delete-N
--mysql-host, --mysql-port, --mysql-database, --mysql-user MySQL connection settings for direct imports
--mysql-password-env Environment variable that stores the MySQL password; defaults to SCENARIO_MANAGER_MYSQL_PASSWORD

--prune-missing is destructive and is refused when any row is skipped during the import. Run --dry-run --prune-missing first and review the prune preview warnings, including affected legacy IDs and the generated delete-count token, before running a real prune.

Email

Setting Description
EMAIL_EMAIL The "from" email address
EMAIL_PASSWORD App-specific password from Google App Passwords
EMAIL_REPORT_RECIPIENTS Optional comma- or semicolon-separated override for scheduled Email Reports recipients; when set, task arguments are ignored
EMAIL_GC Provider-specific mailbox for GoCardless donation tests; do not fall back to EMAIL_EMAIL
EMAIL_STRIPE Provider-specific mailbox for Stripe donation tests; do not fall back to EMAIL_EMAIL

FRAPI

LTI has two FRAPI access paths during the Cloud Run migration:

Setting Description
FRAPI_BASE_URL Public FRAPI Cloud Run base URL, for example https://frapi.citizengo.org
FRAPI_HEALTH_URL Optional explicit health endpoint override; when empty LTI uses <FRAPI_BASE_URL>/v1/health/
FRAPI_DB_HOST, FRAPI_DB_PORT, FRAPI_DB_NAME, FRAPI_DB_USER, FRAPI_DB_PASSWORD Optional direct MySQL read path for FRAPI reporting and donation verification; when incomplete LTI falls back to SSH_*
FRAPI_DB_CONNECT_TIMEOUT Connection and write timeout in seconds; defaults to 10 and remains the timeout for short direct reads
FRAPI_DB_READ_TIMEOUT Read timeout in seconds for month-scale FRAPI reporting and aggregate queue-health queries; defaults to 60
FRAPI_QUEUE_STALE_AFTER_SECONDS Queue work age that changes the monitor to unhealthy; default 900, minimum 60
FRAPI_QUEUE_FAILURE_WINDOW_SECONDS Lookback window for terminal queue failures; default 1800, minimum 60
FRAPI_QUEUE_FAILURE_ALERT_THRESHOLD Recent failure count that changes the monitor from warning to unhealthy; default 3
FRAPI_QUEUE_MONITOR_INTERVAL_MINUTES Interval between LTI queue monitor tasks; default 15, minimum 1
FRAPI_QUEUE_MAX_GROUPS Maximum module/service/status aggregate groups returned; default 25, maximum 100
SSH_HOST, SSH_USERNAME, SSH_PRIVATE_KEY_PATH, SSH_SQL_DB Legacy direct database/reporting access used by payment gateway reports and FRAPI donation verification

Use FRAPI_BASE_URL for new HTTP-level checks. Use FRAPI_DB_* for direct Cloud SQL reads when a scoped read-only database user is available. Keep the SSH_* values until all reporting queries against FRAPI tables have been validated through the direct path. Monthly payment reports and the aggregate queue-health monitor opt into FRAPI_DB_READ_TIMEOUT; short reads, including express donation verification, retain FRAPI_DB_CONNECT_TIMEOUT as their hard read bound. Connections and writes retain the short connect-timeout budget.

The FRAPI queue monitor is stricter than legacy reports: it requires the direct FRAPI_DB_* path and queries the fully qualified frapi_cgo.request table. It does not fall back to SSH, retrieve request payloads, or write FRAPI data.

Asana Integration

Setting Description
ASANA_CLIENT_ID Asana API client ID
ASANA_SECRET Asana API secret
ASANA_ACCESS_TOKEN Personal access token (Profile > My Apps)
ASANA_PROJECT_ID Target project ID
ASANA_SECTION_ID Target section ID
ASANA_DUE_DAYS Days from now for task due dates (default: 1)
ASANA_ERROR_ASSIGNEE Asana user GID for the assignee of automated Asana failure reports
ASANA_ERROR_COLLABORATORS Comma-separated Asana user GIDs to add as followers for automated Asana failure reports

Cloud Run uses main.settings_cloudrun, where WEBSITE_MAIN_URL, ASANA_PROJECT_ID, ASANA_SECTION_ID, ASANA_ERROR_ASSIGNEE, and ASANA_ERROR_COLLABORATORS are required when building Asana failure reports. The dev deploy workflow preserves existing Asana routing runtime configuration and does not set these values during a normal image deploy. Keep Asana routing configured through the target Cloud Run runtime environment so missing or stale report routing is fixed as configuration, not hidden inside a routine deploy.

Salesforce

Setting Description
SALESFORCE_PROD_AUTH_MODE Production auth mode; set to jwt
SALESFORCE_PROD_AUTH_USERNAME Production integration username for JWT bearer auth
SALESFORCE_PROD_AUTH_JWT_CLIENT_ID Production External Client App client ID for JWT bearer auth
SALESFORCE_PROD_AUTH_JWT_PRIVATE_KEY Private key paired with the production External Client App certificate
SALESFORCE_PROD_AUTH_JWT_LOGIN_URL Production Salesforce login URL for JWT bearer auth
SALESFORCE_SANDBOX_AUTH_MODE Sandbox auth mode; set to jwt
SALESFORCE_SANDBOX_USERNAME Sandbox integration username for JWT bearer auth
SALESFORCE_SANDBOX_DOMAIN Sandbox Salesforce domain, e.g. citizengo--uat.sandbox.my
SALESFORCE_SANDBOX_JWT_CLIENT_ID Sandbox External Client App client ID for JWT bearer auth
SALESFORCE_SANDBOX_JWT_PRIVATE_KEY Private key paired with the sandbox External Client App certificate
SALESFORCE_SANDBOX_JWT_LOGIN_URL Sandbox Salesforce login URL for JWT bearer auth

LTI defaults Salesforce helpers to the sandbox target. Production Salesforce access must be requested explicitly in code and configured with the SALESFORCE_PROD_AUTH_* environment variables. LTI dev/prod runtimes are separate from Salesforce sandbox/production targets, so both LTI runtimes can verify both Salesforce targets when configured. The prod deploy workflow still owns the production runtime's Salesforce JWT deploy bindings, but deploy smoke is limited to /system_health/readiness. Salesforce target checks belong in diagnostics, scheduled QA, or manual QA. Normal Deploy LTI dev no longer mutates Salesforce env vars or secrets; it preserves existing Salesforce runtime configuration. Do not add password, refresh-token, session, or legacy production Salesforce variables to new deploy contracts. The production rollout intentionally keeps any already-present legacy Salesforce variables until a separate post-deploy cleanup verifies both JWT targets and scheduled checks in lti-prod. Dev Salesforce cleanup is a separate configuration task, not part of the normal dev deploy.

Migration fixture loading through Cloud Run loaddata jobs is not part of LTI. Fixture or Scenario Manager re-imports should use the import_scenario_manager management command above through a separately approved operational task.

Production Salesforce connectivity is a targeted diagnostic, not a default deploy gate. Confirm the Salesforce production integration user is least-privilege/read-only for the LTI query surface before relying on production Salesforce checks. Set HEALTH_CHECK_IDENTITY_EMAIL and HEALTH_CHECK_IDENTITY_SUBJECT as GitHub environment variables when the deploy identity should not be derived from the current service-account JSON key. The workflows still fall back to the key JSON for the existing key-based auth path, but explicit variables are the durable contract for future auth providers. LTI_DEPLOY_SHA identifies the exact source revision in readiness responses. Deploy workflows set it automatically. SCHEDULER_STALE_AFTER_SECONDS defaults to 900 and classifies the oldest due queue item without blocking a web revision. To check production Salesforce auth manually, call the read-only endpoint with a Google identity token for the Cloud Run service audience:

TOKEN="$(gcloud auth print-identity-token --audiences "${SERVICE_URL}")"
curl -fsS \
  -H "Authorization: Bearer ${TOKEN}" \
  "${SERVICE_URL%/}/system_health/salesforce?environment=production"

Do not add --include-email to normal deploy smoke token generation. The Cloud Run ID token needs the correct service audience and an identity claim that matches the configured health-check identity; the documented deploy path uses the token subject and does not require an email claim.

The response must report salesforce_environment=production and auth_mode=jwt. This is a targeted diagnostic check, not a default deploy gate.

Note: Salesforce password credentials can expire via forced password reset. JWT bearer auth avoids storing the Salesforce password in LTI and matches the current sandbox External Client App configuration.

Payment Gateways

Setting Description
stripe_api_key / STRIPE_API_KEY Stripe API key
PAYPAL_CLIENT_ID PayPal client ID
PAYPAL_SECRET_KEY_1 PayPal secret key
GOCARDLESS_ACCESS_TOKEN GoCardless API token
EXPRESS_CC_DR_TOKEN Express credit card DR token; configure as a Cloud Run Secret Manager env var
EXPRESS_CC_DR_EMAIL Email address for the Express credit card DR token
EXPRESS_GC_DR_TOKEN Express GoCardless DR token; configure as a Cloud Run Secret Manager env var
EXPRESS_GC_DR_EMAIL Email address for the Express GoCardless DR token

Sentry

Setting Description
SENTRY_AUTH_KEY Sentry authentication key
RG_SLUG Organization slug (default: citizengo)

Test Form Data

These values are used to fill donation forms during real donation tests. Use uppercase setting names in Django settings; lowercase aliases are kept for older local workflows.

Setting Description
FIRST_NAME, LAST_NAME Donor name
ADDRESS, POSTCODE, CITY, COUNTRY Donor address (country code should match bank account country)
EMAIL, MOBILE Contact info
CREDIT_CARD_NUMBER, CREDIT_CARD_CVC, CREDIT_CARD_EXPIRATION Test credit card
BANK_ACC_NR, BANK_ACC_NR_ES SEPA bank account numbers

Petition Settings

Setting Description
PETITION_EMAIL Base mailbox used by petition tests. Existing-signer checks use it directly; new-member checks derive +list+N aliases from it.
PETITION_EMAIL_NOTSHARED Same but with "do not share" flag
BEHAVE_MODULO Seconds between list rotation for BDD tests (default: 3599)

Preview the acceptance-only per-list signature inventory without creating work:

python manage.py prepare_acceptance_signature_scenarios

Create the missing pending scenarios only after choosing an execution window:

python manage.py prepare_acceptance_signature_scenarios \
  --apply \
  --schedule-at 2026-08-17T14:00:00Z

The apply timestamp must be timezone-aware and in the future. Past or naive timestamps fail before any scheduled-test row is created.

Rollback removes only unperformed rows owned by this command:

python manage.py prepare_acceptance_signature_scenarios --disable

Browser Settings

Setting Description
FIREFOX Use Firefox instead of Chrome (False by default)
HEADLESS Run browser in headless mode
CHROME_DRIVER Manual ChromeDriver path (leave empty for Selenium Manager)
CITIZENGO_URL Base CitizenGO URL
BROWSER_PAGE_LOAD_TIMEOUT_SECONDS Per-navigation browser page-load timeout (default: 60)
SCHEDULED_TEST_TIMEOUT_SECONDS Hard budget for one scheduled browser journey (default: 900)

Other

Setting Description
WEBSITE_MAIN_URL URL of this LTI instance
SCREENSHOT_MAXIMUM_AGE Days to keep screenshots (default: 28)
SSH_USERNAME SSH username for remote database access
ASANA_ERROR_ASSIGNEE Assignee Asana user GID for Asana error tasks
ASANA_ERROR_COLLABORATORS List of collaborator Asana user GIDs for Asana tasks