Appearance
Configuration
All configuration is done via environment variables. You can use a .env file — the application loads it automatically on startup.
Core
| Variable | Required | Default | Description |
|---|---|---|---|
DATABASE_URL | Yes | PostgreSQL connection string | |
KINDE_DOMAIN | Yes | Kinde tenant URL (e.g. https://your-tenant.kinde.com). Also served to the SPA via GET /api/public-config. | |
KINDE_CLIENT_ID | Yes | Kinde M2M client ID. Also served to the SPA via GET /api/public-config. | |
KINDE_CLIENT_SECRET | Yes | Kinde M2M client secret | |
KINDE_REDIRECT_URI | Yes | OAuth redirect URI the SPA returns to after login | |
KINDE_LOGOUT_URI | Yes | URI the SPA returns to after logout | |
PORT | No | 8080 | HTTP server port |
LOG_LEVEL | No | info | Log level: trace, debug, info, warn, error, fatal, panic |
LOG_REQUESTS | No | false | Enable HTTP request logging |
PROMETHEUS_ENABLED | No | true | Enable Prometheus metrics at /metrics |
Admin API
| Variable | Required | Default | Description |
|---|---|---|---|
ADMIN_API_TOKEN | No | Enables the admin API at /api/admin. Must be at least 50 characters |
When set, the following endpoints become available:
POST /api/admin/users— Create a userGET /api/admin/users— List usersDELETE /api/admin/users/:id— Suspend a user
All admin endpoints require the Authorization: Bearer <token> header.
Mailer
| Variable | Required | Default | Description |
|---|---|---|---|
MAILER_TRANSPORT | Yes | smtp or log | |
MAILER_FROM_NAME | Yes | Sender display name | |
MAILER_FROM_EMAIL | Yes | Sender email address |
SMTP transport
When MAILER_TRANSPORT=smtp:
| Variable | Required | Default | Description |
|---|---|---|---|
SMTP_HOST | Yes | SMTP server hostname | |
SMTP_PORT | No | 587 | SMTP server port |
SMTP_USERNAME | Yes | SMTP authentication username | |
SMTP_PASSWORD | Yes | SMTP authentication password |
Log transport
When MAILER_TRANSPORT=log, no additional variables are needed. Emails are printed to stdout. Useful for development and testing.
LLM provider
The grading pipeline calls a large language model to evaluate submissions. The provider is selected by LLM_PROVIDER and the rest of the configuration depends on which provider is chosen. The grader code is provider-agnostic — adding a new backend means implementing a single LLMClient interface and wiring a new branch into the factory; the grading pipeline itself does not change.
| Variable | Required | Default | Description |
|---|---|---|---|
LLM_PROVIDER | Yes | Provider id: anthropic or bedrock. |
Anthropic
When LLM_PROVIDER=anthropic:
| Variable | Required | Default | Description |
|---|---|---|---|
ANTHROPIC_API_KEY | No | API key. If omitted, the SDK falls back to its default credential discovery (e.g. the ANTHROPIC_API_KEY env var that the SDK itself reads). |
The model is pinned in code (claude-sonnet-4-20250514) and intentionally not configurable — the prompts are tuned for it.
Bedrock
When LLM_PROVIDER=bedrock:
| Variable | Required | Default | Description |
|---|---|---|---|
AWS_REGION | No | us-east-1 | AWS region to send Bedrock requests to. Must be a region where the us.anthropic.claude-sonnet-4-6 cross-region inference profile is supported. |
Authentication uses the standard AWS credential chain — on EC2 the instance IAM role is picked up automatically; no explicit key configuration is needed. The instance role must have bedrock:InvokeModel permission for the Anthropic foundation models (already granted by the CDK stack).
Grading worker
The grading worker pulls jobs from the jobs table and runs them against the configured LLM provider. Failed jobs are rescheduled with exponential backoff (base · 2^(attempt-1), capped at one hour) until they hit the configured maximum number of attempts, at which point they are marked failed and stop being retried. Successful jobs are marked completed with completed_at = now() so total processing time can be measured.
| Variable | Required | Default | Description |
|---|---|---|---|
WORKER_TYPE | No | db to enable the polling worker, empty to disable. Shared by the grading and calibration workers. | |
GRADING_WORKER_CONCURRENCY | No | 4 | Number of in-process worker goroutines. |
GRADING_WORKER_POLL_INTERVAL | No | 5s | How often each worker polls for new jobs (Go duration format). |
GRADING_WORKER_BATCH_LIMIT | No | 5 | Max jobs a single worker processes back-to-back before sleeping for GRADING_WORKER_POLL_INTERVAL. Prevents one worker from monopolizing a large backlog when others could share the load. |
GRADING_RETRY_MAX | No | 10 | Max attempts per job before it is marked failed. |
GRADING_RETRY_BASE_DELAY | No | 5m | Base delay for exponential backoff between retries (Go duration format). Capped at 1h. |
Calibration worker
The calibration worker runs alongside the grading worker (same WORKER_TYPE=db switch) and grades the teacher-supplied calibration items used to anchor the rubric. It is automatically disabled when no LLM provider is configured.
| Variable | Required | Default | Description |
|---|---|---|---|
CALIBRATION_WORKER_POLL_INTERVAL | No | 5s | How often the worker polls for new calibration jobs (Go duration format). |
CALIBRATION_RETRY_MAX | No | 5 | Max attempts per calibration item before it is marked failed. |
CALIBRATION_RETRY_BASE_DELAY | No | 5m | Base delay for exponential backoff between retries (Go duration format). |
CALIBRATION_BATCH_SIZE | No | 50 | Max calibration items the worker fetches per poll. |
Database connection pool
| Variable | Required | Default | Description |
|---|---|---|---|
DB_MAX_OPEN_CONNS | No | 25 | Maximum number of open connections |
DB_MAX_IDLE_CONNS | No | 10 | Maximum number of idle connections |
DB_CONN_MAX_LIFETIME | No | 30m | Maximum connection lifetime (Go duration format) |
DB_CONN_MAX_IDLE_TIME | No | 5m | Maximum idle connection lifetime (Go duration format) |
Security headers
All optional. Sensible defaults are applied.
| Variable | Required | Default | Description |
|---|---|---|---|
HELMET_NO_SNIFF | No | true | Set X-Content-Type-Options: nosniff |
HELMET_FRAME_GUARD | No | DENY | X-Frame-Options value |
HELMET_HSTS | No | true | Enable Strict-Transport-Security |
HELMET_HSTS_MAX_AGE | No | 15552000 | HSTS max-age in seconds |
HELMET_HSTS_INCLUDE_SUBDOMAINS | No | true | Include subdomains in HSTS |
HELMET_XSS_FILTER | No | true | Set X-XSS-Protection: 1; mode=block |
HELMET_DNS_PREFETCH_CONTROL | No | true | Set X-DNS-Prefetch-Control: off |
HELMET_NO_ROBOT_INDEX | No | false | Set X-Robots-Tag: noindex, nofollow |
HELMET_REFERRER_POLICY | No | strict-origin-when-cross-origin | Referrer-Policy value |
HELMET_CROSS_ORIGIN_OPENER_POLICY | No | same-origin | Cross-Origin-Opener-Policy value |
HELMET_CROSS_ORIGIN_EMBEDDER_POLICY | No | Cross-Origin-Embedder-Policy value | |
HELMET_CROSS_ORIGIN_RESOURCE_POLICY | No | same-origin | Cross-Origin-Resource-Policy value |