Configuration reference

Iskue is configured entirely through environment variables. Almost every variable maps to a property in backend/src/main/resources/application.yml, which declares the built-in default (the APP_RATELIMIT_* defaults are declared in the backend's rate-limit filter instead), which declares the built-in default — for example jwt-secret: ${JWT_SECRET:dev-only-secret-change-me-0123456789-0123456789}. Set a variable in the environment of the backend container (or in your .env file consumed by Docker Compose) and it overrides the default; leave it unset and the default applies. No configuration files need to be edited or mounted.

Where to set variables

  • Single-node stack (docker-compose.yml in the repo root): copy .env.example to .env next to the compose file and edit it. The backend service passes the relevant variables through.
  • Cluster stack (deploy/docker-compose.yml): values are set inline in the x-backend-env block. This file is a dev/e2e cluster — do not expose it publicly.
  • Production cluster (deploy/docker-compose.prod.yml): secrets are read from deploy/.env (git-ignored; keep it mode 0600). See the compose-level variables at the end of this page.
Minimal setup for the single-node stack
cp .env.example .env
# Generate a strong JWT secret (32+ characters required):
openssl rand -base64 48
# Paste the output into .env as JWT_SECRET, then:
docker compose --profile full up -d --build

The tables below list the defaults built into application.yml. The compose files override several of them with deployment-appropriate values (for example CORS_ALLOWED_ORIGINS: http://localhost:8088 in the root compose file, and STORAGE_DIR: /data/attachments inside containers). A value you set in the environment always wins.

Core settings

VariableDefaultPurpose
JWT_SECRETdev-only-secret-change-me-0123456789-0123456789HMAC secret used to sign login tokens. Must be at least 32 characters; the root compose file refuses to start without it (${JWT_SECRET:?Set JWT_SECRET (32+ chars) in .env}).
JWT_EXPIRATION_MINUTES480Lifetime of issued login tokens, in minutes.
DB_URLjdbc:postgresql://localhost:5432/issuehubJDBC URL of the PostgreSQL database. The root compose file sets jdbc:postgresql://postgres:5432/issuehub; the cluster files point at citus-coordinator.
DB_USERissuehubDatabase user.
DB_PASSWORDissuehubDatabase password. Change it in any real deployment.
CORS_ALLOWED_ORIGINShttp://localhost:5173Comma-separated browser origins allowed to call the API. Must exactly match the public origin the frontend is served from.
STORAGE_DIR./data/attachmentsDirectory where ticket attachments are stored. In containers this is /data/attachments, backed by the attachments volume — all backend replicas must share it.

The defaults for JWT_SECRET and DB_PASSWORD exist only for throwaway development environments — the backend fails fast at startup if the placeholder JWT_SECRET is used without APP_DEV_LOGIN_ENABLED=true. Never carry either default into a reachable deployment: anyone who knows the default JWT_SECRET can mint valid admin tokens.

Mail

Outbound email (notifications, customer-portal magic links, customer replies) is off by default. Enable it with MAIL_ENABLED=true and supply standard Spring Boot SMTP settings via the SPRING_MAIL_* variables.

VariableDefaultPurpose
MAIL_ENABLEDfalseMaster switch for outbound email. When false, Iskue sends nothing and no SMTP settings are needed.
SPRING_MAIL_HOSTunsetSMTP server hostname (Spring Boot spring.mail.host).
SPRING_MAIL_PORTunsetSMTP port. .env.example shows 587 as the typical submission port.
SPRING_MAIL_USERNAMEunsetSMTP username, if the server requires authentication.
SPRING_MAIL_PASSWORDunsetSMTP password.
MAIL_FROMissuehub@localhostFrom address on outgoing mail.
EMAIL_INBOUND_ADDRESSemptyBase address customers send to and reply to. When set, outbound customer mail carries a per-ticket Reply-To of the form support+KEY-123@domain, so replies thread back to the right ticket even if the subject line is edited.
SMTP settings for the backend container — on the single-node stack `docker-compose.yml` forwards only `MAIL_ENABLED`, so pass the `SPRING_MAIL_*` and `MAIL_FROM` variables through with a `docker-compose.override.yml` (see the Email page)
MAIL_ENABLED=true
SPRING_MAIL_HOST=smtp.example.com
SPRING_MAIL_PORT=587
SPRING_MAIL_USERNAME=notifier@example.com
SPRING_MAIL_PASSWORD=app-specific-password
MAIL_FROM=issuehub@example.com

Single sign-on (OIDC)

Google and Microsoft sign-in are optional. A provider activates only when its client ID is set; leave both unset to keep password-only login. Register these redirect URIs with the identity provider: <backend>/login/oauth2/code/google for Google and <backend>/login/oauth2/code/microsoft for Microsoft.

VariableDefaultPurpose
OAUTH2_SUCCESS_REDIRECThttp://localhost:5173/oauth2/callbackWhere the browser is sent after the backend mints the Iskue token. Must match the SPA origin.
OAUTH2_FAILURE_REDIRECThttp://localhost:5173/login?error=ssoWhere the browser is sent when SSO fails.
OAUTH2_GOOGLE_CLIENT_IDemptyGoogle OAuth client ID. Setting it activates Google sign-in.
OAUTH2_GOOGLE_CLIENT_SECRETemptyGoogle OAuth client secret.
OAUTH2_MICROSOFT_CLIENT_IDemptyMicrosoft (Azure AD) application client ID. Setting it activates Microsoft sign-in.
OAUTH2_MICROSOFT_CLIENT_SECRETemptyMicrosoft client secret.
OAUTH2_MICROSOFT_TENANTcommonAzure AD tenant: common, organizations, or a specific tenant ID to restrict which directory may sign in.

The backend honours X-Forwarded-* headers from a reverse proxy (server.forward-headers-strategy: framework in application.yml), so OAuth2 redirect URIs are built from the public URL when Iskue runs behind a proxy — no extra configuration required.

Development and test flags

VariableDefaultPurpose
APP_DEV_LOGIN_ENABLEDfalsePasswordless dev login: shows a "Dev login (no password)" button that issues a JWT for a seeded dev-admin@issuehub.local admin. When false, the endpoint returns 404.
APP_JIRA_IMPORT_ENABLEDfalseJira importer (live REST connection to Jira Cloud / Data Center). The import endpoints and admin screen exist only when enabled.

Keep APP_DEV_LOGIN_ENABLED=false in any real deployment — enabling it hands any visitor an admin token. Only the throwaway cluster in deploy/docker-compose.yml turns it on; the production file deploy/docker-compose.prod.yml explicitly sets it to false.

Security switches

VariableDefaultPurpose
APP_REGISTRATION_ENABLEDtrueSelf-service sign-up. On by default so a fresh install can bootstrap its first admin; any internet-reachable instance should set it to false once that admin exists. When off, the SPA hides the sign-up link (GET /api/v1/auth/registration reports the state) and POST /api/v1/auth/register returns 403. deploy/docker-compose.prod.yml ships it false.
APP_API_DOCS_ENABLEDtrueOpenAPI and Swagger UI (/v3/api-docs, /swagger-ui.html). These paths are reachable without authentication and publish a machine-readable map of every endpoint, so internet-facing instances should set false — the routes then 404 outright.

With registration off there is currently no other way to add a user — no invite flow, no password reset, and SSO provisions accounts only when an OIDC provider is configured — so re-enable it briefly when onboarding. On the single-host stack, pass both variables into the backend service's environment block yourself; the shipped docker-compose.yml does not forward them from .env.

Rate limits

Iskue applies in-memory, per-minute token-bucket rate limiting (greedy refill) as defence in depth: a tight per-IP cap on authentication, tighter-than-general per-IP caps on the public portal and webhook surfaces, and a generous per-user cap on everything else. Requests over the cap receive HTTP 429. Buckets are per backend instance — when running multiple replicas, each replica counts separately.

VariableDefaultPurpose
APP_RATELIMIT_AUTH_PER_MIN10Per-IP requests per minute on /api/v1/auth/** (brute-force protection on login/MFA).
APP_RATELIMIT_PORTAL_PER_MIN60Per-IP requests per minute on /api/v1/portal/**, the public token-authenticated customer portal surface.
APP_RATELIMIT_WEBHOOK_PER_MIN120Per-IP requests per minute on /api/v1/email/webhook/ and /api/v1/git/webhook/.
APP_RATELIMIT_API_PER_MIN240Requests per minute for everything else, keyed per authenticated user (per IP when anonymous).
APP_RATELIMIT_TRUST_FORWARDED_FORfalseWhen true, the limiter derives the client address from X-Forwarded-For instead of the socket address. Enable only when a trusted reverse proxy sets that header and the backends are unreachable except through it.
APP_RATELIMIT_TRUSTED_PROXY_HOPS1How many trailing X-Forwarded-For entries your own proxy chain appends. The client address is taken that many positions from the right of the header, so entries a client fabricates at the front can never displace it. With one appending proxy the default 1 selects the real client; raise it when another trusted hop (for example a CDN) appends after it.

The dev/e2e cluster file deploy/docker-compose.yml raises the caps to APP_RATELIMIT_AUTH_PER_MIN: "200" and APP_RATELIMIT_API_PER_MIN: "2000" so automated test traffic from a single IP does not trip 429s. The production file keeps the defaults above and sets APP_RATELIMIT_TRUST_FORWARDED_FOR: "true" because traffic arrives via the proxy chain. Long-lived event streams (paths ending in /live) are exempt from counting.

Instance identity, licensing and other settings

VariableDefaultPurpose
APP_INSTANCE_IDlocalThis replica's identity, surfaced by the admin cluster-status page. Set a distinct value per replica (the cluster files use backend1, backend2).
APP_NODE_NAMEIssueHub backendHuman-readable replica name shown alongside the instance ID.
APP_VERSION0.1.0-SNAPSHOTVersion string reported by the application.
APP_LICENSE_PUBLIC_KEYcommitted dev keyEd25519 SPKI public key used to verify licence keys offline. The default is a committed development keypair — real deployments must override it with their own key.
APP_LICENSE_GRACE_DAYS14Grace period, in days, applied by licence verification.
PORTAL_BASE_URLhttp://localhost:5173Public base URL of the customer portal SPA, used to build emailed magic links.
SPRING_FLYWAY_ENABLEDtrueWhether the backend runs database migrations at startup. The cluster files set "false" on the replicas because a one-shot flyway container migrates the coordinator exactly once.

Compose-level variables (deploy/.env)

The production cluster file deploy/docker-compose.prod.yml reads its secrets from deploy/.env rather than hard-coding them. It refuses to start when a required value is missing.

VariableDefaultPurpose
DB_PASSWORDrequiredPassword for the Citus nodes and the backend's database connection.
JWT_SECRETrequiredSigning secret passed to both backend replicas.
PUBLIC_ORIGINrequiredThe public origin of the deployment. Used for CORS_ALLOWED_ORIGINS, PORTAL_BASE_URL, OAUTH2_SUCCESS_REDIRECT (${PUBLIC_ORIGIN}/oauth2/callback) and OAUTH2_FAILURE_REDIRECT (${PUBLIC_ORIGIN}/login?error=sso).
LB_PORT8092Loopback port the nginx load balancer binds on the host (127.0.0.1:${LB_PORT:-8092}:80). Front it with your TLS-terminating web server.
Create deploy/.env for the production cluster
# Generate deploy/.env from your shell — dotenv files do not execute $() commands,
# so the secrets must be produced at write time (unquoted EOF expands them here).
install -m 600 /dev/null deploy/.env
cat > deploy/.env <<EOF
DB_PASSWORD=$(openssl rand -hex 24)
JWT_SECRET=$(openssl rand -base64 48)
PUBLIC_ORIGIN=https://issues.example.com
LB_PORT=8092
EOF

Upload size limits are fixed in application.yml rather than environment-driven: max-file-size: 10MB per attachment and max-request-size: 12MB per request.