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 Kubernetes.
Prerequisites
Install and configure the following tools before you attempt a local build:
-
Java JDK 21 – Required for the Spring Boot application server. Gradle reads the target JDK from
.java-versionand uses its Java toolchain support. The checked-in wrapper downloads the pinned Gradle distribution and verifiesserver/gradle/wrapper/gradle-wrapper.properties' checksum. CI also validates the wrapper JAR. No global Gradle installation is needed. -
Docker and Docker Compose – Required for PostgreSQL. Verify both with
docker infoanddocker compose version. -
Vite+ (
vp) – The command surface for every repository task, and how you get the pinned Node.js. Install the launcher:curl -fsSL https://vite.plus | bashOn Windows:
irm https://vite.plus/ps1 | iex. The launcher's own version does not matter: it runs thevite-plusthis repository pins. Managed mode is on afterwards and provides the Node.jsdevEngines.runtimepins. Runvp env offif you manage Node.js yourself.vp installthen installs dependencies through the pinned pnpm, which Vite+ downloads itself: no global pnpm, no Corepack.The installer adds
vpto your login shell. Anything that does not start from one, an IDE task or a desktop app, needsvpon its PATH likenodeandgit.After
vp install,vp --versionprints the pinnedvite-plus. -
Helm CLI – Required by
vp run checkand the pre-push hook: the tooling tests render the chart with the realhelmon your PATH. Install the version that.github/actions/setup-helm/action.ymlpins. That file is the one home of the version and its checksums. CI installs the same version. Verify it withhelm version. -
Kubernetes dev tools – The host server runs its sandboxes in k3d. Install the tools in Kubernetes before you start
vp run dev.
If you push branches to the repository instead of a fork, also configure commit signing.
CONTRIBUTING.md § Signed Commits has the setup.
vp install warns while it is missing.
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
- TypeScript tooling
- oxlint (lint) and oxfmt (format)
- Tailwind CSS IntelliSense
The Oxc extension provides oxlint diagnostics. Workspace settings use it to format JavaScript, TypeScript, and JSON files on save.
Run the same checks from the shell with:
vp run check:agents # Format check, lint, and both typechecks outside the webapp
vp run fix:agents # check:agents, applying every safe fix
vp run check:webapp # The SPA's format and lint checks; the lint is type-aware and type-checks
vp run check # Complete local quality gate
Use the local verification contract for affected checks, the pre-push gate, and CI exclusions.
IntelliJ and WebStorm are also supported.
Java formatting
vp run format:java
vp run format:java:check
The repository commands are the source of truth. Optional editor integrations:
Other editors should run the repository command. VS Code does not format Java on save: a Java formatter with a different style would undo Spotless's Palantir formatting.
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/application/src/main/resources/ — prod and specs, plus
the per-purpose overlays (e2e, cds-training, migrate, playwright) documented where
they are used.
These are Spring profiles, not build profiles. Gradle’s bootRun passes -Pprofiles=… through to
Spring. vp run dev:server:e2e uses that interface.
Build-only profiles
specs and cds-training generate artifacts. They are not deployment modes.
They load the application contract without runtime bootstrap, container-image validation or image pulls.
They supply no shared placeholder secrets.
If an authentication component needs a key to initialize, the build uses the non-production ephemeral-key behavior, without a runtime warning.
Startup rejects either profile together with prod, even when production secrets are configured.
Production encryption requirements apply to Spring's expanded profiles.
Thus, a production profile group requires the same keys as prod itself.
vp run dev runs the server role, with its sandboxes, in a host JVM against one shared k3d cluster.
Each worktree uses its own sandbox namespace, derived from its port set, with its own ResourceQuota.
Its review and conversation sandboxes stay in that namespace.
Five worktrees share the cluster. They do not each install a full chart release.
The sandboxes reach the host JVM through its sandbox gateway. Each sandbox receives its own subject-scoped token.
The cluster has one full chart release for cluster qualification and live suites.
Run that release on demand with vp run dev:kubernetes.
Production and small resource requests also apply when you qualify those profiles.
Changing the dev release's requests requires separate measurements of dev peaks.
Create application-local.yml in server/application/src/main/resources to override defaults. This file is gitignored.
Before you start the local server, set HEPHAESTUS_SECURITY_ENCRYPTION_KEY and HEPHAESTUS_SECURITY_CREDENTIAL_ENCRYPTION_KEY in server/.env.
Each key must contain exactly 32 UTF-8 bytes.
For a new database, generate each value as Secret custody describes.
Keep these values across restarts.
For an existing database, keep its current keys and versions and follow the stored secret conversion procedure.
The server does not store plaintext credentials or JWT private keys.
Never commit application-local.yml. It may contain secrets and machine-specific configuration.
Running the stack
-
Check that the Docker daemon runs with
docker info. -
From the repo root, start everything with one command:
vp run 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:
vp run dev:server # terminal 1 — brings up Postgres, then Spring Bootvp run dev:webapp # terminal 2 — Vite dev server -
Open the configured server and webapp ports. See the port map. The defaults are
http://localhost:8080andhttp://localhost:4200.The OpenAPI description and Swagger UI are off by default. When enabled, these endpoints are unauthenticated and expose the route list, including admin routes. 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 —vp run generate:api:specsboots under thespecsprofile, which turns them on itself.
To wipe the local database (e.g. after a bad migration or sync), run vp run 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.
Database password
Each checkout has its own PostgreSQL password.
The first vp run dev, dev:compose or db:* command writes a random POSTGRES_PASSWORD to server/.env.
Compose creates the database with it.
Spring Boot and the database scripts read it from the same file.
A POSTGRES_PASSWORD in your shell overrides the file.
PostgreSQL reads the password only when it creates a database.
A checkout whose server/postgres-data already exists was created with the old fixed password.
The first command writes that password to server/.env and prints a warning, so your data keeps working.
Run vp run dev:reset to replace it with a random password.
This deletes the local data.
Compose project
Each checkout has its own Compose project, so a command in one checkout never acts on the containers of another.
The first vp run dev, dev:compose or db:* command writes COMPOSE_PROJECT_NAME to server/.env.
The name holds the checkout's folder name and a hash of its path.
A checkout whose server/postgres-data already exists keeps the name server, the name that its running containers have.
A COMPOSE_PROJECT_NAME in your shell or in the file wins.
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 |
| Sandbox gateway | 8081 | SANDBOX_API_PORT | server/.env |
| Mailpit SMTP | 1025 | MAILPIT_SMTP_PORT | server/.env |
| Mailpit UI | 8025 | MAILPIT_UI_PORT | server/.env |
| Webapp (Vite dev) | 4200 | WEBAPP_PORT | Shell env only |
Run vp run check:ports before starting the stack. It checks the ports above.
Email
The local profile sends every email to the Mailpit container from server/compose.yaml. Nothing
leaves your machine. Its SMTP and inbox ports bind to 127.0.0.1 only. Open http://localhost:8025 (MAILPIT_UI_PORT) to read
what the server sent, with the HTML part, the plain-text alternative and the headers side by side.
The quickest end-to-end check is the Send test email button on the instance settings page
(/admin/settings). It goes through the same gateway as every notification, including silent mode.
Templates live in server/application/src/main/resources/templates/email/ — html/ and text/
hold the two parts of one message, and messages.properties holds the subjects.
How it works
The .env file in server/ is read by both Docker Compose (for container port mappings and the database password) and Spring Boot (through spring.config.import). The webapp port is a separate shell setting, as listed above.
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" };
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 vp run dev:webapp
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 |
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. Hephaestus creates workspaces and monitors 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:
- hephaestus-build
- ls1intum
- HephaestusTest
allowed-repositories:
- hephaestus-build/Hephaestus
- ls1intum/Artemis
- HephaestusTest/demo-repository
Empty lists = no filtering (production behavior). Non-empty = only sync matching items.
Filters do not 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 administration → 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.
Activate it.
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 vp run dev:e2e:setup. It creates the connection, model, and agent binding through the application API.
It reads provider credentials from E2E_* environment variables.
Thus, they stay outside source control and command-line history.
Models always use the in-app LLM proxy: the upstream key is injected by the proxy and never reaches the sandbox.
An SSRF egress guard (EgressPolicy) validates provider base URLs.
In production, it accepts only a public HTTPS host, or a private host that the operator lists in hephaestus.llm.egress.private-hosts.
It rejects plain-HTTP localhost/127.0.0.1/::1 targets. To point a connection at a local OpenAI-compatible gateway during development,
set:
# application-local.yml
hephaestus.llm.egress.allow-loopback: true
or HEPHAESTUS_LLM_EGRESS_ALLOW_LOOPBACK=true in server/.env. It is off by default and
application-e2e.yml is the only shipped profile that turns it on. 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) through 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(replace8080with yourSERVER_PORT). The local auth issuer defaults tohttp://localhost:${SERVER_PORT:8080}. If you setHEPHAESTUS_AUTH_ISSUER, register its origin instead. AddHEPHAESTUS_AUTH_API_BASE_PATHbefore/loginif you set an API prefix. -
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 local GitLab login, register an OAuth application on your GitLab instance. Use callback
http://localhost:<SERVER_PORT>/login/oauth2/code/gitlaband scoperead_user. Apply the same issuer and API-prefix settings as for GitHub. SetGITLAB_OAUTH_CLIENT_ID/GITLAB_OAUTH_CLIENT_SECRET. PointGITLAB_OAUTH_BASE_URLat the instance. Its default ishttps://gitlab.com, but you can use, for example,https://gitlab.lrz.de. To hide the GitLab button, leave the client id blank. -
Run the application server with
vp run dev:server. It brings up the Compose services, installs the generated-clients module and starts the application module. Open the webapp and click Continue with GitHub.
GitLab workspaces are created only on GITLAB_DEFAULT_SERVER_URL, falling back to
GITLAB_OAUTH_BASE_URL. Set it consistently on every server. Additional GitLab login
providers permit sign-in and identity linking. They do not permit syncing another GitLab instance.
Super admin
App-level admin is the APP_ADMIN role on the account table, with authority app_admin in the JWT.
admin is the per-workspace role name, which instance tokens exclude.
An existing admin manages this role from the /admin/users UI.
Before first boot, list the first admin in HEPHAESTUS_AUTH_BOOTSTRAP_ADMINS, for example github:@you.
Promotion runs on their first login.
It is idempotent and promote-only.
Manual database edits are a last resort.
Troubleshooting auth
- 401 after login: check that
GITHUB_OAUTH_CLIENT_ID/SECRETare set. Check that the provider callback matchesHEPHAESTUS_AUTH_ISSUER, orhttp://localhost:<SERVER_PORT>when it is unset, plus the configured API prefix. - Logins do not 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:
vp run dev:reset(see Running the stack). On next login the account and identity link are recreated.
Kubernetes
vp run dev:kubernetes installs the chart on a local k3d cluster with the images of this checkout.
It needs Docker with at least 10 GiB of memory, k3d, kubectl, Helm 4, the pack CLI, openssl and curl.
The command checks the tools and memory budget first and names each missing item.
On macOS, brew install k3d helm kubectl installs three of the tools.
If the buildpacks/tap/pack formula fails, use the pack release archive.
The native stack (vp run dev) is the default inner loop.
It reloads the webapp at once.
The server stays on the host, where your IDE can debug it.
It runs its Pods in the shared cluster.
The dev launcher builds and pins the agent image, creates the worktree namespace, and applies the chart's sandbox quota, NetworkPolicy and server RBAC there. It gives the JVM a namespace-scoped kubeconfig with short-lived credentials. No cluster credential reaches a sandbox.
Set distinct SERVER_PORT and SANDBOX_API_PORT values in each worktree's server/.env.
The namespace is heph-dev-<SERVER_PORT>-<SANDBOX_API_PORT>.
The management port defaults to SERVER_PORT + 1010. Set MANAGEMENT_PORT when that port is in use.
The host loop runs one review and one conversation turn at a time.
The Pod requests stay the same as the chart's.
vp run dev:kubernetes:worktree:reset deletes only this worktree's namespace and PriorityClasses.
Stop its JVM first.
A port set that belongs to another worktree stops startup instead of changing that namespace.
Use the cluster before you push a change to one of these:
- The Helm chart (
deploy/chart/) or its script (scripts/kubernetes-cluster.ts). - The Kubernetes sandbox backend or the sandbox gateway of the server.
- A runtime role, or a setting that a role binds at start.
- The agent or PostgreSQL image.
CI runs the cluster jobs for these paths, and vp run check:affected reminds you of them.
A cold vp run dev:kubernetes took about 5 minutes on an Apple silicon Mac, and a warm run about 3.
Debug a defect in the native stack when you can.
In the cluster, follow the logs of a role with the kubectl command that dev:kubernetes prints.
vp run dev:kubernetes does these steps:
- It creates the k3d cluster
hephaestuswith two nodes and publishes HTTPS on the loopback address only. The Kubernetes API uses a free port on127.0.0.1. An older cluster with a non-loopback API binding requiresvp run dev:kubernetes:reset. Port 443 must be free. The check stops the command when a program holds it. It also creates a local registry, which the nodes use as a mirror ofghcr.io. - It builds the server, webapp, agent and PostgreSQL images, and pushes them to that registry.
- It creates the qualification namespaces, a Secret with generated values, and the certificates.
It keeps them in
~/.hephaestus/kubernetes-cluster, shared by the worktrees. Each worktree generates its private host-server kubeconfig at.context/kubernetes-host/<namespace>/kubeconfig.json. - It turns on the Gateway provider of the Traefik that k3s ships, and creates the Gateway
cluster. - It installs the chart with the dev profile and sandboxes on. It runs the sandbox isolation gate against the installed templates.
- It sends a request to the webapp and the API through the Gateway.
Then it prints the URL,
https://hephaestus.localhost, and how to sign in.
The local CA ca.crt in the kubernetes-cluster work folder signed the certificate.
The command prints its path and the commands that make your system trust it.
Below, $WORK is the work folder. On macOS:
security add-trusted-cert -r trustRoot -k ~/Library/Keychains/login.keychain-db "$WORK/ca.crt"
On Debian or Ubuntu:
sudo cp "$WORK/ca.crt" /usr/local/share/ca-certificates/hephaestus-dev.crt
sudo update-ca-certificates
Firefox, and Chrome on Linux, keep their own stores.
If you use mkcert, CAROOT="$WORK/mkcert" mkcert -install adds the CA to those stores too.
A certificate lasts 397 days. A run issues it again when fewer than 30 days remain.
The chart runs the prod profile, so the passwordless dev sign-in is off.
To sign in, create a GitHub OAuth app for the cluster.
Use the homepage https://hephaestus.localhost and the callback https://hephaestus.localhost/api/login/oauth2/code/github.
Then give its values to the command:
GITHUB_OAUTH_CLIENT_ID=... GITHUB_OAUTH_CLIENT_SECRET=... \
HEPHAESTUS_AUTH_BOOTSTRAP_ADMINS=github:@you vp run dev:kubernetes
Only one qualification release runs in the shared cluster.
Run it when you need chart or live-suite qualification. Then use vp run dev:kubernetes:down to release its compute resources.
Host edit loops keep their separate namespaces and keep running.
Five simultaneous full releases are outside the dev contract.
A second run reuses the cluster, the registry, the Secrets and the database.
It keeps every value of the Secrets, and adds only a key that a newer checkout needs.
It also keeps the OAuth app, so you give the variables only once.
Give both again to change the app, or HEPHAESTUS_AUTH_BOOTSTRAP_ADMINS alone to change the first admins.
vp run dev:kubernetes:reset removes the app with the cluster.
Every kubectl and helm call of the command uses the cluster k3d-hephaestus, whatever your current context is.
Each run applies the agent-sandbox controller of its checkout's deploy/platform/k3s-agent-sandbox.yaml.
The controller and its resource definitions are cluster-wide.
Thus, a checkout with an older pin downgrades them for every worktree.
They stay downgraded until a checkout with the newer pin runs again.
| Command | Does |
|---|---|
vp run dev:kubernetes:down | Drains the qualification servers and removes that release and its sandbox namespace. Keeps the shared cluster, host-loop namespaces, database volumes, Secrets and CA. The next dev:kubernetes installs the release again. |
vp run dev:kubernetes:reset | Deletes the shared cluster, its data, the registry and the work folder with the CA. Refuses while host-loop namespaces exist: stop those JVMs and reset each worktree namespace first. The next run makes a new CA, so trust it again. |
If Docker has no free address pool for the cluster network, the command stops before k3d starts.
Stopped Compose stacks of other worktrees keep their networks.
The command then tells you to run docker network prune, which removes the networks that no container uses.
The CI cluster jobs run the same script (scripts/kubernetes-cluster.ts) with the images of their run.
The k3d job also serves the release through the same Gateway, and sends the same HTTPS requests.
Webhook receiver
The webhook receiver lives in the Java server (integration.core.webhook package).
vp run dev:server exposes the unified /webhooks/{kind} endpoint (kinds: github and
slack) alongside the rest of the API. GitLab and Outline post to their connection routes.
Production runs the receiver in the server too — Server runtime.
Set the webhook secret before the first start, so that GitHub signature checks succeed.
In the native stack, add it to server/.env:
echo "WEBHOOK_SECRET=$(openssl rand -hex 32)" >> server/.env
vp run dev:kubernetes generates it in the release Secret.
The value must match the secret configured on your GitHub webhooks, which X-Hub-Signature-256 validates.
GitLab group hooks use WEBHOOK_ROUTING_SECRET instead — see WEBHOOK_ROUTING_SECRET.
Before enabling GitLab or Outline registration with a routing secret, set HEPHAESTUS_AUTH_ISSUER to your public HTTPS tunnel's root origin.
Forward /webhooks/ to the local receiver.
Registration uses the issuer even outside production.
The default loopback issuer cannot receive provider deliveries through that tunnel.
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:
vp install -
Start Vite (defaults to port 4200):
vp run dev:webapp -
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/theme-tokens.css. - Enable Tailwind IntelliSense 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.
vp run --filter webapp storybook:dev # Local playground on http://localhost:6006
vp run --filter webapp build-storybook # Static bundle for Chromatic
vp run --filter webapp chromatic:ci # Visual regression in CI
- Every new UI component ships with at least one story covering empty, loading, and error states.
- CI runs Chromatic for same-repository pull requests that change the webapp – review visual diffs before merging.
- Reference ui.shadcn.com for composition patterns. A new widget builds on the Base UI primitives already wrapped under
src/components/ui/**.
OpenAPI client
webapp/src/api/** is generated from server/openapi.yaml and is the only way the webapp talks to
the server. After an API change, from the repository root:
vp run generate:api # regenerates server/openapi.yaml from the packaged JAR, then the client
vp run generate:api:client regenerates the clients alone from the committed
spec: this one and the Chrome extension's extension/src/api/**. Commit the spec and both clients
with the change.
CI fails a change that breaks a client of the previous release.
See API compatibility.
Both clients use the generator in scripts/api-generator/.
That workspace supplies the TypeScript 6 compiler API required by the stable generator.
The applications and root tooling keep TypeScript 7.
The generator pin lives in scripts/api-generator/package.json.
Both configurations disable the generated root entry file.
The webapp's owned src/api.ts exports types without listing each schema.
Oxfmt runs on each generated directory with scripts/api-generator/oxfmt.generated.json,
so one API addition does not rewrite a single long import or export line.
All generated outputs stay committed. CI regenerates them and requires a clean tree.
- Schema names carry no
DTOsuffix. The server strips it when it publishes the spec. - Pair every new endpoint with its generated TanStack Query options. Never hand-roll
fetch— the generated client is configured once and carries the cookie session.
Updating Java dependencies
Edit server/gradle/libs.versions.toml for library and plugin versions.
Edit server/build.gradle.kts for plugin-classpath BOMs and transitive constraints.
Then resolve the graphs and exercise the changed tools.
Strict dependency locking rejects missing lock state as well as version drift. Do not delete a
lockfile or disable verification to make an update pass.
The root dependencies report covers both projects, including PMD's auxiliary classpath.
The buildEnvironment report covers the separately locked plugin classpath in
server/buildscript-gradle.lockfile. Renovate's standard lock and checksum commands refresh the
same graphs used by source builds.
Set GRADLE_USER_HOME to an empty temporary directory and disable configuration caching when
generating metadata, so cached resolution cannot omit parent POM or module-descriptor checksums:
./server/gradlew -p server --no-configuration-cache \
dependencies buildEnvironment \
--write-locks --write-verification-metadata sha256
./server/gradlew -p server --no-configuration-cache \
:application:check :application:bootJar --write-locks --write-verification-metadata sha256
Review the dependency-lock diffs and independently verify newly trusted artifacts against their
publisher before committing server/gradle/verification-metadata.xml. Metadata generation records
what was downloaded. It does not establish trust. Run the checks again without metadata-writing
flags, still without configuration caching and with another empty Gradle user home, to verify fresh
downloads. Exercise changed generators, formatters, Liquibase or mutation tooling as well: their
classpaths are separate from the application runtime. Restore your usual Gradle user home afterward.
Gradle does not remove the verification entry of a version that no build downloads any more. OSV Scanner reports such an entry as a dependency — Gradle dependency inventory. After an update that replaces versions, run:
node scripts/check-gradle-verification-metadata.ts --write
It resolves every server dependency in an empty Gradle home with strict verification.
Then it deletes each component that the resolution did not download.
The deletion adds no checksum to trust.
If a CI task reads a reported component, add that task to the script instead of deleting the entry.
Without --write, the command fails on such a component, as Quality → App Server does on a pull request.
Run --write on Linux, as CI does.
Another operating system can resolve a different set of components.
The script compares whole components.
It does not report an unused artifact inside a component that the build still downloads, such as the native JAR of another platform.
For a Gradle wrapper update, also run the build with --warning-mode all and review plugin
compatibility before accepting a new major. Do not suppress upstream deprecations to make the
upgrade appear clean. A JDK-line change updates .java-version and Renovate's matching
constraints.java. gate:toolchain rejects drift between them.