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:
- Deploy the code so migrations create the Agent API schema.
- Bind
API_CLIENT_TOKEN_HASH_KEYwith the manual runtime-config workflow. - Create/provision the read-scope smoke
ApiClienttoken in the target database after migrations are present. - Store the raw smoke token as
LTI_AGENT_API_SMOKE_TOKENwhere the smoke command or scheduled diagnostics can read it. - 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.
| 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 |