# 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](/admin/kubernetes.md).

## Prerequisites

Install and configure the following tools before you attempt a local build:

1. **Java JDK 21** – Required for the Spring Boot application server. Gradle reads the target JDK from `.java-version` and uses its Java toolchain support. The checked-in wrapper downloads the pinned Gradle distribution and verifies `server/gradle/wrapper/gradle-wrapper.properties`' checksum. CI also validates the wrapper JAR. No global Gradle installation is needed.

2. **Docker and Docker Compose** – Required for PostgreSQL and NATS. Verify both with `docker info` and `docker compose version`.

3. **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 | bash
   ```

   On Windows: `irm https://vite.plus/ps1 | iex`. The launcher's own version does not matter: it runs the `vite-plus` this repository pins. Managed mode is on afterwards and provides the Node.js `devEngines.runtime` pins. Run `vp env off` if you manage Node.js yourself. `vp install` then installs dependencies through the pinned pnpm, which Vite+ downloads itself: no global pnpm, no Corepack.

   The installer adds `vp` to your login shell. Anything that does not start from one, an IDE task or a desktop app, needs `vp` on its PATH like `node` and `git`.

   After `vp install`, `vp --version` prints the pinned `vite-plus`.

4. **Helm CLI** – Required by `vp run check` and the pre-push hook: the tooling tests render the chart with the real `helm` on your PATH. Install the version that [`.github/actions/setup-helm/action.yml`](https://github.com/hephaestus-build/Hephaestus/blob/main/.github/actions/setup-helm/action.yml) pins. That file is the one home of the version and its checksums. CI installs the same version. Verify it with `helm version`.

5. **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.

If you push branches to the repository instead of a fork, also configure commit signing. [CONTRIBUTING.md § Signed Commits](https://github.com/hephaestus-build/Hephaestus/blob/main/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](/contributor/local-verification.md) 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:

* [IntelliJ](https://plugins.jetbrains.com/plugin/13180-palantir-java-format)
* [Eclipse](https://github.com/palantir/palantir-java-format/tree/develop/eclipse_plugin)

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-role and per-purpose overlays (`worker`, `webhook`, `e2e`, `cds-training`) 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.

Create `application-local.yml` in `server/application/src/main/resources` to override defaults. This file is gitignored.

Keep it local

Never commit `application-local.yml`. It may contain secrets and machine-specific configuration.

### Running the stack

1. Check that the Docker daemon runs with `docker info`.

2. From the repo root, start everything with one command:

   ```
   vp run dev
   ```

   This launches an `mprocs` session 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 Boot

   vp run dev:webapp   # terminal 2 — Vite dev server
   ```

3. Open the configured server and webapp ports. See the [port map](#default-port-map). The defaults are `http://localhost:8080` and `http://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` (and `SPRINGDOC_SWAGGER_UI_ENABLED=true` for the browser UI) and Swagger UI appears at `http://localhost:8080/swagger-ui/index.html`. Committing a spec does not need either flag — `vp run generate:api:specs` boots under the `specs` profile, 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.

### 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  |

Pre-flight check

Run `vp run check:ports` before starting the stack. It checks the six ports above. A NATS container binds 4222 as well, and a conflict there shows up only at boot.

### 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 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" };
```

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 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)

1. [Create a Personal Access Token](https://github.com/settings/tokens) with scopes: `repo`, `read:org`, `read:user`.

2. Create `application-local.yml`:

   ```
   hephaestus:

     workspace:

       init-default: true

       default:

         login: your-github-org # e.g., "ls1intum" or "HephaestusTest"

         token: ghp_your_token

         repositories-to-monitor:

           - your-github-org/repo1

           - your-github-org/repo2
   ```

3. Leave `hephaestus.integration.github.app.id` unset or set to `0`.

#### Option B: GitHub App

1. [Create a GitHub App](https://docs.github.com/en/apps/creating-github-apps) with appropriate permissions.

2. Create `application-local.yml`:

   ```
   hephaestus:

     integration:

       github:

         app:

           id: 12345

           privateKey: |

             -----BEGIN RSA PRIVATE KEY-----

             ...

             -----END RSA PRIVATE KEY-----

     workspace:

       init-default: false # Workspaces created from installations
   ```

3. 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.

Tip

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 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. 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](https://github.com/hephaestus-build/Hephaestus/blob/main/docs/decisions/0017-replace-keycloak-with-spring-native-auth.md)). 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

1. [Create a GitHub OAuth application](https://github.com/settings/developers) with the callback URL `http://localhost:8080/login/oauth2/code/github` (replace `8080` with your `SERVER_PORT`). The local auth issuer defaults to `http://localhost:${SERVER_PORT:8080}`. If you set `HEPHAESTUS_AUTH_ISSUER`, register its origin instead. Add `HEPHAESTUS_AUTH_API_BASE_PATH` before `/login` if you set an API prefix.

2. Copy `server/.env.example` to `server/.env` and 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>
   ```

3. (Optional) To enable local GitLab login, register an OAuth application on your GitLab instance. Use callback `http://localhost:<SERVER_PORT>/login/oauth2/code/gitlab` and scope `read_user`. Apply the same issuer and API-prefix settings as for GitHub. Set `GITLAB_OAUTH_CLIENT_ID` / `GITLAB_OAUTH_CLIENT_SECRET`. Point `GITLAB_OAUTH_BASE_URL` at the instance. Its default is `https://gitlab.com`, but you can use, for example, `https://gitlab.lrz.de`. To hide the GitLab button, leave the client id blank.

4. 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 the server and workers. 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/SECRET` are set. Check that the provider callback matches `HEPHAESTUS_AUTH_ISSUER`, or `http://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`) in `server/.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](https://k3d.io) cluster with the images of this checkout. It needs Docker with at least 10 GiB of memory, `k3d`, `kubectl`, Helm 4, the [`pack` CLI](https://github.com/buildpacks/pack/releases), `openssl` and `curl`. The cluster uses about 6 GiB, and a practice review 3 GiB more. The command checks these 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. Your IDE starts and debugs the JVM. It has no NetworkPolicy, no Jobs, no Gateway and no Kubernetes sandbox backend.

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 agent gateway of the worker.
* 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:

1. It creates the k3d cluster `hephaestus` with two nodes and publishes HTTPS on the loopback address only. The Kubernetes API uses a free port on `127.0.0.1`. An older cluster with a non-loopback API binding requires `vp 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 of `ghcr.io`.
2. It builds the server, webapp, agent and PostgreSQL images, and pushes them to that registry.
3. It creates the namespaces, a Secret with generated values, and the certificates. It keeps them in the folder `kubernetes-cluster` of the gitignored `.context` folder.
4. It turns on the Gateway provider of the Traefik that k3s ships, and creates the Gateway `cluster`.
5. It installs the chart with the dev profile and two workers. It waits until each worker has passed its isolation probe.
6. 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](https://github.com/FiloSottile/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
```

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.

| Command                       | Does                                                                                                                                                                                      |
| ----------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `vp run dev:kubernetes:down`  | Stops the cluster and keeps its data. The next `dev:kubernetes` starts it again. It fails if no cluster exists.                                                                           |
| `vp run dev:kubernetes:reset` | Deletes the cluster, its data, the registry and the work folder with the CA. It prints what it removed. It succeeds if no cluster exists. 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). In local development the monolith default boots all three runtime roles (`server`, `worker`, `webhook`), so `vp run dev:server` exposes the unified `/webhooks/{kind}` endpoint (kinds: `github`, `gitlab`, `slack`, `outline`) alongside the rest of the API.

Production runs the receiver in its own container — [Runtime roles](/admin/runtime-roles.md#monolith-vs-split-pod) and [ADR 0008](https://github.com/hephaestus-build/Hephaestus/blob/main/docs/decisions/0008-webhook-runtime-role.md).

Set the webhook secret before the first start, so that signature checks succeed. Auto-registration and the receiver use the same secret. 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 and GitLab webhooks (validated through `X-Hub-Signature-256` for GitHub. `X-Gitlab-Token`, or `webhook-signature` for a hook with a signing token, for GitLab — see [`WEBHOOK_SECRET`](/admin/production-setup.md#gitlab-rollout-bundle)).

For Slack app setup, use the scope/event checklist in [`docs/admin/production-setup.mdx`](/admin/production-setup.md#slack-app-configuration) and the manifest template in [`docs/admin/slack-app-manifest-template.yml`](/assets/files/slack-app-manifest-template-56a3787c3edec8dc666456df49ba1fb7.yml).

## Web client (`webapp`)

1. Install dependencies:

   ```
   vp install
   ```

2. Start Vite (defaults to port 4200):

   ```
   vp run dev:webapp
   ```

3. Visit `http://localhost:4200` (or your custom `WEBAPP_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](https://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](/contributor/ci-cd.md#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`.

* Schema names carry no `DTO` suffix. 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](/contributor/supply-chain-security.md#gradle-dependency-inventory-and-the-vulnerabilities-exclusion). 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.
