Local Development
Hephaestus supports full-stack development across Java and TypeScript. This guide focuses on the development environment; production operations are covered in the Admin Guide, starting with Install (Self-Hosted).
Prerequisites
Install and configure the following tools before you attempt a local build:
- Java JDK 21 – Required for the Spring Boot application server.
- Docker and Docker Compose – Required for PostgreSQL and NATS. Verify both with
docker infoanddocker compose version. - Node.js LTS (>= 24) and pnpm (>= 11) (enable via
corepack enable) – For the React client and root scripts. - Bun – Runs the agent runtime and precompute tests (
pnpm run test:agents).corepackdoes not provide it; install it separately. Match the version the agent image pins — theARG BUN_VERSIONline indocker/agents/pi/Dockerfile, which is also what CI installs. Why Bun and not Node: ADR 0030. - NATS CLI (optional) – Helpful when inspecting the webhook/sync event stream (NATS is disabled by default locally). The agent job queue runs on PostgreSQL and needs no NATS.
Recommended IDE setup
Open the repository using the project.code-workspace file in VS Code and install the workspace recommendations (@recommended in the Extensions view). Key extensions include:
- Java Extension Pack
- Spring Boot tools
- Node.js + TypeScript tooling
- oxlint (lint) and Biome (format)
- Tailwind CSS IntelliSense
The Biome extension picks up both configurations automatically: biome.jsonc at the repository root
covers the Bun agent runtime, its specs, both precompute trees and scripts/**, and
webapp/biome.jsonc (declared "root": false) covers the SPA. Editing a file in either tree
formats it to that tree's rules on save. Both configs set "linter": { "enabled": false } — Biome
formats and sorts imports and lints nothing anywhere, which is why the oxlint extension is a
recommendation too.
Run the same checks from the shell with:
pnpm run check:agents # Biome format, then oxlint, then both typechecks, outside the webapp
pnpm run check:agents:fix # Apply every safe fix
pnpm run check:webapp # The SPA: Biome format check, then oxlint
pnpm run typecheck:webapp # The SPA's typecheck — a separate leg, not part of check:webapp
pnpm run check # Everything — a strict superset of CI
check is a superset, not a copy: PMD (lint:java), check:story-sort and check:diagrams run in
no workflow, so green CI is not evidence that check passes. The pre-push hook is the only thing
that runs them before a merge, and --no-verify skips it.
JetBrains alternatives such as IntelliJ (Java) and WebStorm (React/TypeScript) work equally well.
Application server
Spring profiles and local configuration
For development stick to the local profile, which is the default. The other shipped profiles each
have their own application-<name>.yml under server/src/main/resources/ — prod and specs, plus
the per-role and per-purpose overlays (worker, webhook, e2e, cds-training) documented where
they are used.
They are not Maven profiles — ./mvnw -Plocal warns that the profile does not exist and changes
nothing. Maven passes the active Spring profile through the app.profiles property, so the override
is -Dapp.profiles=… (this is what pnpm dev:server:e2e does).
Create server/src/main/resources/application-local.yml to override defaults. This file is gitignored.
:::caution Keep it local
Never commit application-local.yml. It may contain secrets and machine-specific configuration.
:::
Running the stack
-
Ensure the Docker daemon is running with
docker info. -
From the repo root, start everything with one command:
pnpm devThis launches an
mprocssession with the server and webapp in separate panes (switch with the arrow keys). The PostgreSQL container is brought up automatically.Prefer plain terminals? Run the two sides yourself:
pnpm dev:server # terminal 1 — brings up Postgres, then Spring Bootpnpm dev:webapp # terminal 2 — Vite dev server -
Access the API at
http://localhost:8080and the webapp athttp://localhost:4200.The OpenAPI description and Swagger UI are off unless you switch them on: the endpoints answer unauthenticated wherever springdoc registers them, so a deployment that left them on published its complete route list, admin routes included. For a local session, set
SPRINGDOC_API_DOCS_ENABLED=true(andSPRINGDOC_SWAGGER_UI_ENABLED=truefor the browser UI) and Swagger UI appears athttp://localhost:8080/swagger-ui/index.html. Committing a spec does not need either flag —pnpm run generate:api:application-server:specsboots under thespecsprofile, which turns them on itself.
To wipe the local database (e.g. after a bad migration or sync), run pnpm dev:reset. The Postgres data is a bind-mounted folder (server/postgres-data), so docker compose down -v alone does not clear it — dev:reset removes the folder and recreates the stack.
Port overrides
Every host-side port is configurable so multiple Hephaestus instances (or other services) can coexist on the same machine. Container-internal ports never change – only the localhost binding moves.
Default port map
| Service | Default | Variable | Config location |
|---|---|---|---|
| PostgreSQL | 5432 | POSTGRES_PORT | server/.env |
| Application server | 8080 | SERVER_PORT | server/.env |
| Webapp (Vite dev) | 4200 | WEBAPP_PORT | Shell env only |
:::tip Pre-flight check
Run pnpm run check:ports before starting the stack. It checks the three ports above; a NATS
container binds 4222 as well, and a conflict there shows up only at boot.
:::
How it works
The .env file in server/ is read by both Docker Compose (for container port mappings) and Spring Boot (via spring.config.import). This means a single file controls the entire local stack.
Ports flow through the system like this:
.env (POSTGRES_PORT=15432)
├─► Docker Compose → '15432:5432' host mapping
└─► Spring Boot → jdbc:postgresql://localhost:15432/hephaestus
Changing ports
Basic example – move PostgreSQL to a non-standard port:
# server/.env
POSTGRES_PORT=15432
The application server automatically picks up the new port from the same .env file.
Changing the application server port:
# server/.env
SERVER_PORT=18080
If the webapp also needs to reach the server at the new port, edit the runtime-environment stub the webapp loads before its bundle:
// webapp/public/env-config.js
window.__ENV__ = { APPLICATION_SERVER_URL: "http://localhost:18080" };
:::caution Keep it local
env-config.js is committed as an empty stub and overwritten by the Docker entrypoint in
production. Treat a local override the way you treat application-local.yml: do not commit it.
:::
The webapp has no Vite-level environment variables — vite.config.ts does not call loadEnv, sets
no envPrefix, and nothing in src/ reads import.meta.env beyond DEV. A webapp/.env file has
no effect on the application's configuration, and there is no VITE_-prefixed setting for the server
URL.
Changing the webapp port:
The Vite dev server reads WEBAPP_PORT from the shell environment (process.env, not a .env file
and not server/.env):
WEBAPP_PORT=5200 pnpm --filter webapp run dev
When changing the webapp port, the server's CORS origin must also be updated so API calls are not rejected:
# server/.env
APPLICATION_HOST_URL=http://localhost:5200
Cascading effects reference
Changing a port in one place may require updates elsewhere. This table shows the full picture:
| When you change... | Also update... | Why |
|---|---|---|
WEBAPP_PORT | APPLICATION_HOST_URL in .env | CORS origin must match |
SERVER_PORT | Webapp's APPLICATION_SERVER_URL (if connecting directly) | API base URL changes |
POSTGRES_PORT | Nothing (automatic) | Both Compose and Spring read from .env |
:::warning Webapp defaults
The webapp reads its configuration from window.__ENV__, falling back to hardcoded dev defaults in
webapp/src/environment/index.ts — APPLICATION_SERVER_URL defaults to http://localhost:8080. In
production the Docker entrypoint writes the real values into env-config.js. For local development
with non-standard ports, put the override in webapp/public/env-config.js as shown above. Nothing
here reads a Vite environment variable.
:::
GitHub configuration
Hephaestus supports two GitHub authentication modes. Pick the one that fits your workflow:
| Mode | Best for | Workspace creation | Repository monitors |
|---|---|---|---|
| PAT | Quick local development | Manual (from config) | Manual (you list them) |
| GitHub App | Testing webhooks, production | Automatic (from installations) | Automatic (from installation events) |
Option A: Personal Access Token (simpler)
-
Create a Personal Access Token with scopes:
repo,read:org,read:user. -
Create
application-local.yml:hephaestus:workspace:init-default: truedefault:login: your-github-org # e.g., "ls1intum" or "HephaestusTest"token: ghp_your_tokenrepositories-to-monitor:- your-github-org/repo1- your-github-org/repo2 -
Leave
hephaestus.integration.github.app.idunset or set to0.
Option B: GitHub App
-
Create a GitHub App with appropriate permissions.
-
Create
application-local.yml:hephaestus:integration:github:app:id: 12345privateKey: |-----BEGIN RSA PRIVATE KEY-----...-----END RSA PRIVATE KEY-----workspace:init-default: false # Workspaces created from installations -
Install the app on your organization. Workspaces and monitors are created automatically.
Limiting sync scope (development filters)
When using GitHub App mode, your app might have access to hundreds of repositories. Use filters to focus on specific orgs/repos during development:
hephaestus:
sync:
filters:
allowed-organizations:
- ls1intum
- HephaestusTest
allowed-repositories:
- ls1intum/Hephaestus
- ls1intum/Artemis
- HephaestusTest/demo-repository
Empty lists = no filtering (production behavior). Non-empty = only sync matching items.
Filters don't delete data – they just skip sync operations. Workspace and organization metadata is still created for all installations.
AI model (local)
Practice reviews and the mentor need at least one enabled model. Providers are registered at
runtime — either an instance-wide catalog entry (Instance admin → AI models) or a workspace's own
"bring your own AI provider" connection. Configure it through the same admin UI/API used in
production; there is deliberately no second env-var catalog or startup seeder that can drift from
the stored configuration. New connections and models start inactive. Test the connection, declare
its price (or explicitly mark it as having no metered API cost), then activate it and bind it to a
purpose (PRACTICE_REVIEW or MENTOR) on the workspace's AI models page.
For an automated local end-to-end setup, use scripts/e2e-setup.sh. It creates the connection,
model, and agent binding through the application API and keeps provider credentials out of source
control and command-line history by reading them from E2E_* environment variables.
Models always use the in-app LLM proxy: the upstream key is injected by the proxy and never reaches the sandbox.
Provider base URLs are validated by an SSRF egress guard (EgressPolicy) that, in production,
refuses anything but a public HTTPS host — a plain-HTTP localhost/127.0.0.1/::1 target is
rejected outright. To point a connection at a local OpenAI-compatible gateway during development,
set:
# server/.env or application-local.yml
hephaestus.llm.egress.allow-loopback: true
This is off by default everywhere except application-local.yml/application-e2e.yml; never enable it in a deployed environment.
Authentication (local)
Authentication is Hephaestus-native (no Keycloak — see ADR 0017). The server federates to GitHub (and optionally GitLab — gitlab.com or a self-hosted instance) via Spring Security oauth2Login, then issues its own short-lived ES256 cookie-session JWT. The webapp reads GET /user and redirects to /auth/login — there is no token in the browser.
Configure GitHub login
-
Create a GitHub OAuth application with the callback URL
http://localhost:8080/login/oauth2/code/github(use yourSERVER_PORTif you overrode it). -
Copy
server/.env.exampletoserver/.envand set the credentials:cp server/.env.example server/.env# Edit .env and set:# GITHUB_OAUTH_CLIENT_ID=<github-client-id># GITHUB_OAUTH_CLIENT_SECRET=<github-client-secret> -
(Optional) To enable GitLab login locally, register an OAuth application on your GitLab instance (callback
http://localhost:8080/login/oauth2/code/gitlab, scoperead_user), setGITLAB_OAUTH_CLIENT_ID/GITLAB_OAUTH_CLIENT_SECRET, and pointGITLAB_OAUTH_BASE_URLat the instance (defaults tohttps://gitlab.com; e.g.https://gitlab.lrz.de). Leave the client id blank to hide the GitLab button. -
Run the application server with
./mvnw spring-boot:run; Postgres + NATS start automatically via Spring Boot's Docker Compose support. Open the webapp and click Sign in with GitHub.
Super admin
App-level admin is the APP_ADMIN role on the account table (authority app_admin in the JWT — admin is the per-workspace role name and is stripped from instance tokens), managed from the /admin/users UI by an existing admin. The first admin is seeded by listing them in HEPHAESTUS_AUTH_BOOTSTRAP_ADMINS (e.g. github:@you) before first boot: the promotion runs on their first login, and is idempotent and promote-only. Hand-editing the DB is a last-resort fallback.
Troubleshooting auth
- 401 after login: ensure
GITHUB_OAUTH_CLIENT_ID/SECRETare set and the callback URL on the GitHub app matches yourSERVER_PORT. - Logins don't survive a server restart locally: set
HEPHAESTUS_AUTH_STATE_COOKIE_KEY(base64 32-byte;openssl rand -base64 32) inserver/.env— otherwise an ephemeral per-boot key is used. - Reset everything: run
pnpm dev:reset(Postgres is a bind-mounted folder, sodocker compose down -vdoes not clear it —dev:resetremovesserver/postgres-dataand recreates the stack); on next login the account + identity link are recreated.
Webhook receiver
The webhook receiver lives in the Java server (integration.core.webhook package). In local
development the monolith default boots all three runtime roles (server, worker, webhook),
so pnpm run dev:server exposes the unified /webhooks/{kind} endpoint (kinds: github,
gitlab, slack, outline) alongside the rest of the API.
In production the receiver is deployed as a separate webhook-server container from the same
image, activated with SPRING_PROFILES_ACTIVE=prod,webhook. The app-server's deploy cycle does
NOT interrupt webhook reception — push events on GitHub/GitLab are not manually redeliverable,
so restart independence is operationally required. See ADR 0008.
When you run docker/compose.core.yaml set the webhook secret up front so signature checks
succeed (the same secret is used by both auto-registration and the receiver):
cp docker/.env.example docker/.env
export WEBHOOK_SECRET=$(openssl rand -hex 32)
echo "WEBHOOK_SECRET=$WEBHOOK_SECRET" >> docker/.env
The value must match the secret configured on your GitHub and GitLab webhooks (validated via
X-Hub-Signature-256 for GitHub, X-Gitlab-Token for GitLab).
For Slack app setup, use the scope/event checklist in
docs/admin/production-setup.mdx and the manifest template in
docs/admin/slack-app-manifest-template.yml.
Web client (webapp)
-
Install dependencies:
pnpm install -
Start Vite (defaults to port 4200):
pnpm --filter webapp run dev -
Visit
http://localhost:4200(or your customWEBAPP_PORT).
UI foundations
- We ship Tailwind CSS 4 in JIT mode. Keep utility classes in JSX and rely on design tokens defined in
src/styles.css. - Ensure Tailwind IntelliSense is enabled in your IDE – typeahead prevents typos and invalid compositions.
- Avoid premature abstractions with
@apply; duplication is fine when it keeps components readable.
Component workflow (Storybook + Chromatic)
Storybook is the source of truth for presentational components.
pnpm --filter webapp run storybook # Local playground on http://localhost:6006
pnpm --filter webapp run build-storybook # Static bundle for Chromatic
pnpm --filter webapp run chromatic:ci # Visual regression in CI
- Every new UI component ships with at least one story covering empty, loading, and error states.
- Chromatic runs on every pull request – review visual diffs before merging.
- Reference ui.shadcn.com for composition patterns; stick to headless Radix primitives when you need new widgets.
OpenAPI client
The generated client in src/api is the only way the webapp talks to server services.
pnpm --filter webapp run openapi-ts
- Generated types drop the
Dtosuffix and mirror the Spring Boot API responses. - Pair every new endpoint with a typed TanStack Query hook for caching and invalidation.
- Never hand-roll fetch calls – shared interceptors handle auth tokens and error telemetry.