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.ymlin the repo root): copy.env.exampleto.envnext 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 thex-backend-envblock. This file is a dev/e2e cluster — do not expose it publicly. - Production cluster (
deploy/docker-compose.prod.yml): secrets are read fromdeploy/.env(git-ignored; keep it mode0600). See the compose-level variables at the end of this page.
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 --buildThe 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
| Variable | Default | Purpose |
|---|---|---|
| JWT_SECRET | dev-only-secret-change-me-0123456789-0123456789 | HMAC 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_MINUTES | 480 | Lifetime of issued login tokens, in minutes. |
| DB_URL | jdbc:postgresql://localhost:5432/issuehub | JDBC URL of the PostgreSQL database. The root compose file sets jdbc:postgresql://postgres:5432/issuehub; the cluster files point at citus-coordinator. |
| DB_USER | issuehub | Database user. |
| DB_PASSWORD | issuehub | Database password. Change it in any real deployment. |
| CORS_ALLOWED_ORIGINS | http://localhost:5173 | Comma-separated browser origins allowed to call the API. Must exactly match the public origin the frontend is served from. |
| STORAGE_DIR | ./data/attachments | Directory 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.
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.
| Variable | Default | Purpose |
|---|---|---|
| MAIL_ENABLED | false | Master switch for outbound email. When false, Iskue sends nothing and no SMTP settings are needed. |
| SPRING_MAIL_HOST | unset | SMTP server hostname (Spring Boot spring.mail.host). |
| SPRING_MAIL_PORT | unset | SMTP port. .env.example shows 587 as the typical submission port. |
| SPRING_MAIL_USERNAME | unset | SMTP username, if the server requires authentication. |
| SPRING_MAIL_PASSWORD | unset | SMTP password. |
| MAIL_FROM | issuehub@localhost | From address on outgoing mail. |
| EMAIL_INBOUND_ADDRESS | empty | Base 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. |
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.comSingle 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.
| Variable | Default | Purpose |
|---|---|---|
| OAUTH2_SUCCESS_REDIRECT | http://localhost:5173/oauth2/callback | Where the browser is sent after the backend mints the Iskue token. Must match the SPA origin. |
| OAUTH2_FAILURE_REDIRECT | http://localhost:5173/login?error=sso | Where the browser is sent when SSO fails. |
| OAUTH2_GOOGLE_CLIENT_ID | empty | Google OAuth client ID. Setting it activates Google sign-in. |
| OAUTH2_GOOGLE_CLIENT_SECRET | empty | Google OAuth client secret. |
| OAUTH2_MICROSOFT_CLIENT_ID | empty | Microsoft (Azure AD) application client ID. Setting it activates Microsoft sign-in. |
| OAUTH2_MICROSOFT_CLIENT_SECRET | empty | Microsoft client secret. |
| OAUTH2_MICROSOFT_TENANT | common | Azure 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
| Variable | Default | Purpose |
|---|---|---|
| APP_DEV_LOGIN_ENABLED | false | Passwordless 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_ENABLED | false | Jira 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
| Variable | Default | Purpose |
|---|---|---|
| APP_REGISTRATION_ENABLED | true | Self-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_ENABLED | true | OpenAPI 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.
| Variable | Default | Purpose |
|---|---|---|
| APP_RATELIMIT_AUTH_PER_MIN | 10 | Per-IP requests per minute on /api/v1/auth/** (brute-force protection on login/MFA). |
| APP_RATELIMIT_PORTAL_PER_MIN | 60 | Per-IP requests per minute on /api/v1/portal/**, the public token-authenticated customer portal surface. |
| APP_RATELIMIT_WEBHOOK_PER_MIN | 120 | Per-IP requests per minute on /api/v1/email/webhook/ and /api/v1/git/webhook/. |
| APP_RATELIMIT_API_PER_MIN | 240 | Requests per minute for everything else, keyed per authenticated user (per IP when anonymous). |
| APP_RATELIMIT_TRUST_FORWARDED_FOR | false | When 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_HOPS | 1 | How 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
| Variable | Default | Purpose |
|---|---|---|
| APP_INSTANCE_ID | local | This replica's identity, surfaced by the admin cluster-status page. Set a distinct value per replica (the cluster files use backend1, backend2). |
| APP_NODE_NAME | IssueHub backend | Human-readable replica name shown alongside the instance ID. |
| APP_VERSION | 0.1.0-SNAPSHOT | Version string reported by the application. |
| APP_LICENSE_PUBLIC_KEY | committed dev key | Ed25519 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_DAYS | 14 | Grace period, in days, applied by licence verification. |
| PORTAL_BASE_URL | http://localhost:5173 | Public base URL of the customer portal SPA, used to build emailed magic links. |
| SPRING_FLYWAY_ENABLED | true | Whether 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.
| Variable | Default | Purpose |
|---|---|---|
| DB_PASSWORD | required | Password for the Citus nodes and the backend's database connection. |
| JWT_SECRET | required | Signing secret passed to both backend replicas. |
| PUBLIC_ORIGIN | required | The 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_PORT | 8092 | Loopback 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. |
# 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
EOFUpload size limits are fixed in application.yml rather than environment-driven: max-file-size: 10MB per attachment and max-request-size: 12MB per request.